from typing import AsyncGenerator, Callable from uuid import UUID from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from app.config import settings security = HTTPBearer() _engine = create_async_engine( settings.DATABASE_URL, echo=False, pool_size=20, max_overflow=10, pool_pre_ping=True, ) _async_session_factory = async_sessionmaker( bind=_engine, class_=AsyncSession, expire_on_commit=False, ) async def get_db() -> AsyncGenerator[AsyncSession, None]: """Yield an async database session.""" async with _async_session_factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> dict: """Decode JWT from Authorization header and return user claims.""" token = credentials.credentials try: payload = jwt.decode( token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM], ) user_id: str | None = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: missing subject", ) return { "user_id": UUID(user_id), "email": payload.get("email", ""), "role": payload.get("role", ""), "name": payload.get("name", ""), } except JWTError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate credentials: {exc}", ) def require_role(roles: list[str]) -> Callable: """Dependency factory that enforces role-based access.""" async def _check_role( current_user: dict = Depends(get_current_user), ) -> dict: if current_user["role"] not in roles: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Role '{current_user['role']}' not permitted. Required: {roles}", ) return current_user return _check_role