v1.3
This commit is contained in:
121
server.py
121
server.py
@@ -6,8 +6,9 @@ from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from jose import jwt
|
||||
from jose.exceptions import JWTError
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import os
|
||||
@@ -23,8 +24,8 @@ import re
|
||||
|
||||
app = FastAPI(title="Rabota.Today API", version="2.0.0")
|
||||
|
||||
# Настройки
|
||||
SECRET_KEY = "your-secret-key-here-change-in-production"
|
||||
# Настройки JWT
|
||||
SECRET_KEY = "your-secret-key-here-change-in-production" # В продакшене использовать сложный ключ
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
|
||||
|
||||
@@ -492,24 +493,26 @@ def verify_password(password: str, hash: str) -> bool:
|
||||
|
||||
|
||||
def create_access_token(data: dict):
|
||||
"""Создание JWT токена с явным преобразованием ID в строку"""
|
||||
"""Создание JWT токена"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
# Исправление предупреждения о datetime.utcnow()
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
|
||||
# Убеждаемся, что user_id сохраняется как строка
|
||||
if "sub" in to_encode:
|
||||
to_encode["sub"] = str(to_encode["sub"])
|
||||
|
||||
# Используем jose.jwt.encode
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
"""Получение текущего пользователя из токена с улучшенной обработкой ошибок"""
|
||||
"""Получение текущего пользователя из токена"""
|
||||
token = credentials.credentials
|
||||
|
||||
try:
|
||||
# Декодируем токен
|
||||
# Декодируем токен с помощью jose.jwt.decode
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
|
||||
# Получаем user_id из поля sub
|
||||
@@ -519,7 +522,7 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(securit
|
||||
print("❌ No sub in token payload")
|
||||
raise HTTPException(status_code=401, detail="Невалидный токен: отсутствует идентификатор пользователя")
|
||||
|
||||
# Пробуем преобразовать в整数
|
||||
# Пробуем преобразовать в int
|
||||
try:
|
||||
user_id_int = int(user_id)
|
||||
except (ValueError, TypeError) as e:
|
||||
@@ -532,7 +535,7 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(securit
|
||||
except jwt.ExpiredSignatureError:
|
||||
print("❌ Token expired")
|
||||
raise HTTPException(status_code=401, detail="Токен истек")
|
||||
except jwt.InvalidTokenError as e:
|
||||
except jwt.JWTError as e:
|
||||
print(f"❌ Invalid token: {e}")
|
||||
raise HTTPException(status_code=401, detail=f"Невалидный токен: {str(e)}")
|
||||
except Exception as e:
|
||||
@@ -815,23 +818,17 @@ async def get_public_stats():
|
||||
"total_users": 0
|
||||
}
|
||||
|
||||
@app.post("/api/register", response_model=TokenResponse)
|
||||
async def register(user: UserRegister, request: Request):
|
||||
"""Регистрация нового пользователя с поддержкой мобильных устройств"""
|
||||
try:
|
||||
# Валидация email
|
||||
if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", user.email):
|
||||
raise HTTPException(status_code=400, detail="Неверный формат email")
|
||||
|
||||
# Валидация телефона
|
||||
phone_digits = re.sub(r"\D", "", user.phone)
|
||||
if len(phone_digits) < 10:
|
||||
raise HTTPException(status_code=400, detail="Неверный формат телефона")
|
||||
@app.post("/api/register", response_model=TokenResponse)
|
||||
async def register(user: UserRegister):
|
||||
"""Регистрация нового пользователя"""
|
||||
try:
|
||||
print(f"📝 Registering user: {user.email}")
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверка существования пользователя
|
||||
# Проверка существования
|
||||
cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,))
|
||||
if cursor.fetchone():
|
||||
raise HTTPException(status_code=400, detail="Email уже зарегистрирован")
|
||||
@@ -848,12 +845,13 @@ async def register(user: UserRegister, request: Request):
|
||||
user_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
# Создание токена с дополнительной информацией
|
||||
print(f"✅ User created with ID: {user_id}")
|
||||
|
||||
# Создаем данные для токена
|
||||
token_data = {
|
||||
"sub": str(user_id),
|
||||
"sub": str(user_id), # Явно преобразуем в строку
|
||||
"role": user.role,
|
||||
"is_admin": bool(is_admin),
|
||||
"device": "mobile" if request.state.is_mobile else "desktop"
|
||||
"is_admin": bool(is_admin)
|
||||
}
|
||||
token = create_access_token(token_data)
|
||||
|
||||
@@ -866,42 +864,42 @@ async def register(user: UserRegister, request: Request):
|
||||
"is_admin": bool(is_admin)
|
||||
}
|
||||
|
||||
# Добавляем заголовки для мобильных устройств
|
||||
if request.state.is_mobile:
|
||||
return JSONResponse(
|
||||
content=response_data,
|
||||
headers={
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Expose-Headers": "Authorization"
|
||||
}
|
||||
)
|
||||
|
||||
return response_data
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Registration error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера")
|
||||
print(f"❌ Registration error: {e}")
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/login", response_model=TokenResponse)
|
||||
async def login(user_data: UserLogin, request: Request):
|
||||
"""Вход в систему с поддержкой мобильных устройств"""
|
||||
async def login(user_data: UserLogin):
|
||||
"""Вход в систему"""
|
||||
try:
|
||||
print(f"🔑 Login attempt: {user_data.email}")
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user or not verify_password(user_data.password, user["password_hash"]):
|
||||
if not user:
|
||||
print(f"❌ User not found: {user_data.email}")
|
||||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||||
|
||||
if not verify_password(user_data.password, user["password_hash"]):
|
||||
print(f"❌ Wrong password for: {user_data.email}")
|
||||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||||
|
||||
print(f"✅ Login successful for user ID: {user['id']}")
|
||||
|
||||
# Создаем данные для токена
|
||||
token_data = {
|
||||
"sub": str(user["id"]),
|
||||
"sub": str(user["id"]), # Явно преобразуем в строку
|
||||
"role": user["role"],
|
||||
"is_admin": user["is_admin"],
|
||||
"device": "mobile" if request.state.is_mobile else "desktop"
|
||||
"is_admin": bool(user["is_admin"])
|
||||
}
|
||||
token = create_access_token(token_data)
|
||||
|
||||
@@ -914,22 +912,14 @@ async def login(user_data: UserLogin, request: Request):
|
||||
"is_admin": bool(user["is_admin"])
|
||||
}
|
||||
|
||||
if request.state.is_mobile:
|
||||
return JSONResponse(
|
||||
content=response_data,
|
||||
headers={
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Expose-Headers": "Authorization"
|
||||
}
|
||||
)
|
||||
|
||||
return response_data
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Login error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера")
|
||||
print(f"❌ Login error: {e}")
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/tags")
|
||||
@@ -1052,7 +1042,7 @@ async def get_all_vacancies(
|
||||
tags: str = None,
|
||||
min_salary: int = None,
|
||||
sort: str = "newest",
|
||||
company_id: int = None # Новый параметр
|
||||
company_id: int = None
|
||||
):
|
||||
"""Получение всех вакансий с фильтрацией"""
|
||||
with get_db() as conn:
|
||||
@@ -1061,7 +1051,8 @@ async def get_all_vacancies(
|
||||
query = """
|
||||
SELECT DISTINCT
|
||||
v.*,
|
||||
COALESCE(c.name, u.full_name) as company_name
|
||||
COALESCE(c.name, u.full_name) as company_name,
|
||||
c.id as company_id -- Добавляем ID компании
|
||||
FROM vacancies v
|
||||
JOIN users u ON v.user_id = u.id
|
||||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||||
@@ -1131,8 +1122,14 @@ async def get_all_vacancies(
|
||||
# Получаем общее количество
|
||||
count_query = "SELECT COUNT(*) FROM vacancies WHERE is_active = 1"
|
||||
if company_id:
|
||||
count_query += " AND user_id = (SELECT user_id FROM companies WHERE id = ?)"
|
||||
cursor.execute(count_query, (company_id,))
|
||||
# Находим user_id по company_id
|
||||
cursor.execute("SELECT user_id FROM companies WHERE id = ?", (company_id,))
|
||||
company_user = cursor.fetchone()
|
||||
if company_user:
|
||||
count_query += " AND user_id = ?"
|
||||
cursor.execute(count_query, (company_user["user_id"],))
|
||||
else:
|
||||
cursor.execute(count_query)
|
||||
else:
|
||||
cursor.execute(count_query)
|
||||
total = cursor.fetchone()[0]
|
||||
@@ -1140,7 +1137,8 @@ async def get_all_vacancies(
|
||||
return {
|
||||
"vacancies": result,
|
||||
"total_pages": (total + limit - 1) // limit,
|
||||
"current_page": page
|
||||
"current_page": page,
|
||||
"total": total
|
||||
}
|
||||
|
||||
|
||||
@@ -1202,11 +1200,12 @@ async def get_vacancy(vacancy_id: int, token: Optional[str] = None):
|
||||
# Увеличиваем счетчик просмотров
|
||||
cursor.execute("UPDATE vacancies SET views = views + 1 WHERE id = ?", (vacancy_id,))
|
||||
|
||||
# Получаем вакансию с данными компании
|
||||
# Получаем вакансию с данными компании - ДОБАВЛЯЕМ company_id
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
v.*,
|
||||
COALESCE(c.name, u.full_name) as company_name,
|
||||
c.id as company_id, -- ВАЖНО: добавляем ID компании
|
||||
c.description as company_description,
|
||||
c.website as company_website,
|
||||
c.logo as company_logo,
|
||||
|
||||
Reference in New Issue
Block a user