117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
"""
|
|
===============================================================================
|
|
FILE: modules/web_api/routers/auth.py
|
|
ROLE: Аутентификация, валидация JWT-токенов и управление паролями.
|
|
===============================================================================
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, Optional
|
|
|
|
import jwt
|
|
from passlib.context import CryptContext
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from pydantic import BaseModel
|
|
|
|
from core.connection import get_connection
|
|
|
|
JWT_SECRET = "scud_jwt_secret_key_2026_orion_ai_super_secure"
|
|
ALGORITHM = "HS256"
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
security = HTTPBearer()
|
|
|
|
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
|
|
|
|
|
def get_db():
|
|
return get_connection(row_factory=True)
|
|
|
|
|
|
def create_access_token(user_id: int, username: str, is_admin: bool) -> str:
|
|
payload = {
|
|
"sub": str(user_id),
|
|
"username": username,
|
|
"is_admin": is_admin,
|
|
"exp": datetime.utcnow() + timedelta(days=30)
|
|
}
|
|
return jwt.encode(payload, JWT_SECRET, algorithm=ALGORITHM)
|
|
|
|
|
|
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
|
|
try:
|
|
token = credentials.credentials
|
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
|
|
user_id = int(payload.get("sub"))
|
|
username = payload.get("username")
|
|
is_admin = bool(payload.get("is_admin", False))
|
|
return {"id": user_id, "username": username, "is_admin": is_admin}
|
|
except Exception as e:
|
|
logging.warning(f"Auth error: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Недействительный или просроченный токен авторизации",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
class AuthRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
old_password: str
|
|
new_password: str
|
|
|
|
|
|
@router.post("/login")
|
|
def login(req: AuthRequest):
|
|
username = req.username.strip().lower()
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT id, username, password_hash, full_name, is_admin FROM users WHERE username = ?", (username,))
|
|
user = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if not user or not pwd_context.verify(req.password, user["password_hash"]):
|
|
raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль")
|
|
|
|
is_admin = bool(user["is_admin"]) or (user["username"] == "puh")
|
|
token = create_access_token(user["id"], user["username"], is_admin)
|
|
|
|
# Возвращаем full_name (если не задано — отдаем username)
|
|
full_name = user["full_name"] if user["full_name"] else user["username"]
|
|
|
|
return {
|
|
"status": "success",
|
|
"token": token,
|
|
"username": user["username"],
|
|
"full_name": full_name,
|
|
"user_id": user["id"],
|
|
"is_admin": is_admin
|
|
}
|
|
|
|
|
|
@router.post("/change-password")
|
|
def change_password(req: ChangePasswordRequest, current_user: Dict[str, Any] = Depends(get_current_user)):
|
|
if not req.new_password or len(req.new_password) < 4:
|
|
raise HTTPException(status_code=400, detail="Новый пароль должен содержать минимум 4 символа")
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT password_hash FROM users WHERE id = ?", (current_user["id"],))
|
|
user = cursor.fetchone()
|
|
|
|
if not user or not pwd_context.verify(req.old_password, user["password_hash"]):
|
|
conn.close()
|
|
raise HTTPException(status_code=400, detail="Неверный старый пароль")
|
|
|
|
new_hash = pwd_context.hash(req.new_password)
|
|
cursor.execute("UPDATE users SET password_hash = ? WHERE id = ?", (new_hash, current_user["id"]))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"status": "success", "message": "Пароль успешно изменен"} |