"""Dépendances d'authentification et d'autorisation.""" import uuid from collections.abc import Callable import jwt from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlmodel import Session from app.auth.tokens import decode_access_token from app.db import get_session from app.models import Account, Position, Role, Shift _bearer = HTTPBearer(auto_error=False) def _unauthorized() -> HTTPException: return HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={"code": "error.unauthorized"}, headers={"WWW-Authenticate": "Bearer"}, ) def _forbidden() -> HTTPException: return HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"code": "error.forbidden"}, ) def get_current_account( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(_bearer), session: Session = Depends(get_session), ) -> Account: if credentials is None or credentials.scheme.lower() != "bearer": raise _unauthorized() try: claims = decode_access_token(credentials.credentials) account_id = uuid.UUID(claims["sub"]) except (jwt.PyJWTError, KeyError, ValueError) as exc: raise _unauthorized() from exc account = session.get(Account, account_id) if account is None: raise _unauthorized() # Repris sur la ligne d'accès de la requête (`app/request_logging.py`) : # qui a rencontré l'erreur, par identifiant seulement. request.state.account_id = account.id return account def current_account_in_edition( edition_id: uuid.UUID, account: Account = Depends(get_current_account), ) -> Account: """Le compte doit appartenir à l'édition de l'URL.""" if account.edition_id != edition_id: raise _forbidden() return account def require_roles(*roles: Role) -> Callable[..., Account]: def dependency( account: Account = Depends(current_account_in_edition), ) -> Account: if account.role not in roles: raise _forbidden() return account return dependency # Singletons prêts à l'emploi (évite un appel de fonction dans un défaut d'arg). require_coordinator = require_roles(Role.COORDINATOR) require_staff = require_roles(Role.COORDINATOR, Role.LEAD) def ensure_can_write_position(account: Account, position: Position) -> None: """COORDINATION partout ; RÉFÉRENT uniquement sur ses propres postes.""" if account.role is Role.COORDINATOR: return if account.role is Role.LEAD and position.lead_account_id == account.id: return raise _forbidden() def ensure_can_write_shift( account: Account, session: Session, shift: Shift ) -> None: if account.role is Role.COORDINATOR: return position = session.get(Position, shift.position_id) if position is None: raise _forbidden() ensure_can_write_position(account, position)