This commit is contained in:
2026-03-16 23:55:04 +03:00
parent 668e62d652
commit f755cf9660
4 changed files with 3076 additions and 475 deletions

121
server.py
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff