"""Auth + RBAC dependencies for FastAPI routes.""" from __future__ import annotations from collections.abc import Callable from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import decode_access_token from app.database import get_db from app.models.enums import Role from app.models.user import User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") # Privilege ordering: higher number = more privilege _ROLE_RANK = {Role.readonly: 0, Role.support: 1, Role.admin: 2} async def get_current_user( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db), ) -> User: creds_error = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) payload = decode_access_token(token) if not payload or "sub" not in payload: raise creds_error result = await db.execute(select(User).where(User.email == payload["sub"])) user = result.scalar_one_or_none() if user is None or not user.is_active: raise creds_error return user def require_role(minimum: Role) -> Callable: """Return a dependency that enforces a minimum role.""" async def _guard(user: User = Depends(get_current_user)) -> User: if _ROLE_RANK[user.role] < _ROLE_RANK[minimum]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Requires at least '{minimum.value}' role", ) return user return _guard require_admin = require_role(Role.admin) require_support = require_role(Role.support) require_readonly = require_role(Role.readonly)