"""FastAPI dependencies for VoIdea. Common dependencies used across API endpoints. """ from typing import Annotated from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import get_settings from app.core.database import async_session_maker from app.core.security import decode_token from app.models.user import User settings = get_settings() security = HTTPBearer() async def get_db() -> AsyncSession: """Get database session.""" async with async_session_maker() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() async def get_current_user( credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)], ) -> User: """Get current authenticated user from JWT token. Args: credentials: Bearer token credentials Returns: User model instance Raises: HTTPException: If token invalid or expired """ try: payload = decode_token(credentials.credentials) if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token", headers={"WWW-Authenticate": "Bearer"}, ) if payload.get("type") != "access": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type", headers={"WWW-Authenticate": "Bearer"}, ) user_id = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token missing user ID", headers={"WWW-Authenticate": "Bearer"}, ) except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) async with async_session_maker() as db: result = await db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account is disabled", ) return user async def require_admin( user: Annotated[User, Depends(get_current_user)], ) -> User: """Require admin or owner role.""" if not user.is_superuser and not user.is_owner: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required", ) return user async def require_owner( user: Annotated[User, Depends(get_current_user)], ) -> User: """Require owner role (system-level operations).""" if not user.is_owner: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Owner access required", ) return user def require_permission(perm_name: str): """Factory: require a specific moderator permission or admin/owner. Usage: @router.get("/admin/feedback") async def list_feedback( user: Annotated[User, Depends(require_permission("can_manage_feedback"))], ): ... """ async def _check( user: Annotated[User, Depends(get_current_user)], ) -> User: if user.is_owner or user.role == "admin": return user if user.role == "moderator" and user.permissions: if user.permissions.get(perm_name, False): return user raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Permission '{perm_name}' required", ) return _check async def get_optional_user( credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(HTTPBearer(auto_error=False))], ) -> User | None: """Get current user if authenticated, None otherwise.""" if credentials is None: return None try: payload = decode_token(credentials.credentials) if payload is None or payload.get("type") != "access": return None user_id = payload.get("sub") if user_id is None: return None except JWTError: return None async with async_session_maker() as db: result = await db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if user and user.is_active: return user return None