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,
|
||||
|
||||
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
Reference in New Issue
Block a user