from dataclasses import dataclass from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from app.config import get_settings from app.models.user import USER_ROLE_ADMIN bearer_scheme = HTTPBearer(auto_error=False) @dataclass(frozen=True) class CurrentUser: username: str role: str @property def is_admin(self) -> bool: return self.role == USER_ROLE_ADMIN def create_access_token(subject: str, role: str) -> str: settings = get_settings() expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes) payload = {"sub": subject, "role": role, "exp": expire} return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) def _decode_current_user(token: str) -> CurrentUser: settings = get_settings() try: payload = jwt.decode( token, settings.jwt_secret, algorithms=[settings.jwt_algorithm], ) username = payload.get("sub") if not username: raise HTTPException(status_code=401, detail="Invalid token") role = str(payload.get("role") or USER_ROLE_ADMIN) return CurrentUser(username=str(username), role=role) except JWTError as exc: raise HTTPException(status_code=401, detail="Invalid token") from exc def verify_access_token(token: str) -> str: """Проверка JWT (в т.ч. query-параметр для SSE). Возвращает username.""" return _decode_current_user(token).username def get_current_user( credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), ) -> CurrentUser: if credentials is None or credentials.scheme.lower() != "bearer": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", ) return _decode_current_user(credentials.credentials) def require_admin(user: CurrentUser = Depends(get_current_user)) -> CurrentUser: if not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required", ) return user