v1.3
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
+969
-72
File diff suppressed because it is too large
Load Diff
+963
-33
File diff suppressed because it is too large
Load Diff
+1084
-309
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user