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 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,

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