diff --git a/server.py b/server.py index 573c7b0..73107f0 100644 --- a/server.py +++ b/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, diff --git a/templates/company_detail.html b/templates/company_detail.html index bb9616f..4eb99ba 100644 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -5,6 +5,8 @@