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 sqlalchemy import func, select from sqlalchemy.orm import Session from app.config import get_settings from app.database import get_db from app.models.user import USER_ROLE_ADMIN, User 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 _user_from_db(db: Session, username: str) -> User: normalized = username.strip() user = db.scalar(select(User).where(func.lower(User.username) == normalized.lower())) if user is None or not user.is_active: raise HTTPException(status_code=401, detail="Invalid token") return user def _decode_current_user(token: str, db: Session | None = None) -> 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") if db is not None: user = _user_from_db(db, str(username)) return CurrentUser(username=user.username, role=user.role) 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, db: Session | None = None) -> str: """Проверка JWT (в т.ч. query-параметр для SSE). Возвращает username.""" return _decode_current_user(token, db=db).username def get_current_user( credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), db: Session = Depends(get_db), ) -> 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, db=db) 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