"""FastAPI dependencies for Mantimon TCG API. This module provides dependency injection functions for authentication, database access, and service layer access in API endpoints. Usage: from app.api.deps import CurrentUser, DbSession, DeckServiceDep @router.get("/decks") async def get_decks( user: CurrentUser, db: DbSession, deck_service: DeckServiceDep, ): return await deck_service.get_user_decks(user.id) Dependencies: - get_db: Async database session - get_current_user: Authenticated user from JWT (required) - get_optional_user: Authenticated user or None - get_current_premium_user: User with active premium - verify_admin_token: Admin API key validation - get_deck_service: DeckService with repositories - get_collection_service: CollectionService with repository """ from typing import Annotated from fastapi import Depends, HTTPException, status from fastapi.security import APIKeyHeader, OAuth2PasswordBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.db import get_session from app.db.models import User from app.repositories.postgres.collection import PostgresCollectionRepository from app.repositories.postgres.deck import PostgresDeckRepository from app.repositories.postgres.linked_account import PostgresLinkedAccountRepository from app.repositories.postgres.user import PostgresUserRepository from app.services.card_service import CardService, get_card_service from app.services.collection_service import CollectionService from app.services.deck_service import DeckService from app.services.game_service import GameService, game_service from app.services.game_state_manager import GameStateManager, game_state_manager from app.services.jwt_service import verify_access_token from app.services.user_service import UserService # OAuth2 scheme for extracting Bearer token from Authorization header # tokenUrl is for OpenAPI docs - points to where tokens are obtained oauth2_scheme = OAuth2PasswordBearer( tokenUrl="/api/auth/token", # For OpenAPI docs auto_error=True, # Raise 401 if no token ) oauth2_scheme_optional = OAuth2PasswordBearer( tokenUrl="/api/auth/token", auto_error=False, # Return None if no token ) # Admin API key header for admin-only endpoints admin_api_key_header = APIKeyHeader( name="X-Admin-API-Key", auto_error=True, description="Admin API key for privileged operations", ) async def verify_admin_token( api_key: Annotated[str, Depends(admin_api_key_header)], ) -> None: """Verify the admin API key. Validates the X-Admin-API-Key header against the configured admin key. Admin key must be configured via ADMIN_API_KEY environment variable. Args: api_key: API key from X-Admin-API-Key header. Raises: HTTPException: 403 if admin key is not configured or doesn't match. Example: @router.post("/admin/grant-cards") async def grant_cards( _: None = Depends(verify_admin_token), ): # Only accessible with valid admin key ... """ configured_key = settings.admin_api_key if configured_key is None: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin API key not configured", ) if api_key != configured_key.get_secret_value(): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid admin API key", ) async def get_db() -> AsyncSession: """Get async database session. Yields: AsyncSession for database operations. Example: @router.get("/items") async def get_items(db: AsyncSession = Depends(get_db)): ... """ async with get_session() as session: yield session async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(get_db)], ) -> User: """Get the current authenticated user from JWT token. Validates the access token and fetches the user from database. Args: token: JWT access token from Authorization header. db: Database session. Returns: The authenticated User. Raises: HTTPException: 401 if token is invalid or user not found. Example: @router.get("/me") async def get_me(user: User = Depends(get_current_user)): return {"email": user.email} """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) # Verify token and extract user ID user_id = verify_access_token(token) if user_id is None: raise credentials_exception # Fetch user from database (direct query for auth - not business logic) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user is None: raise credentials_exception return user async def get_optional_user( token: Annotated[str | None, Depends(oauth2_scheme_optional)], db: Annotated[AsyncSession, Depends(get_db)], ) -> User | None: """Get the current user if authenticated, or None. Useful for endpoints that work both with and without authentication, but may provide additional features for authenticated users. Args: token: JWT access token or None. db: Database session. Returns: The authenticated User, or None if not authenticated. Example: @router.get("/cards") async def get_cards(user: User | None = Depends(get_optional_user)): if user: # Show user's collection else: # Show public cards """ if token is None: return None user_id = verify_access_token(token) if user_id is None: return None # Direct query for auth - not business logic result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() async def get_current_premium_user( user: Annotated[User, Depends(get_current_user)], ) -> User: """Get the current user and verify they have premium. Args: user: The authenticated user. Returns: The authenticated User with active premium. Raises: HTTPException: 403 if user doesn't have active premium. Example: @router.post("/decks") async def create_unlimited_decks( user: User = Depends(get_current_premium_user) ): # Only premium users can have unlimited decks ... """ if not user.has_active_premium: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Premium subscription required", ) return user # ============================================================================= # Service Dependencies # ============================================================================= def get_collection_service( db: Annotated[AsyncSession, Depends(get_db)], ) -> CollectionService: """Get CollectionService with PostgreSQL repository. Creates a CollectionService instance with the current database session. Args: db: Database session from request. Returns: CollectionService configured for PostgreSQL. Example: @router.get("/collections/me") async def get_collection( service: CollectionService = Depends(get_collection_service), ): ... """ repo = PostgresCollectionRepository(db) card_service = get_card_service() return CollectionService(repo, card_service) def get_deck_service( db: Annotated[AsyncSession, Depends(get_db)], ) -> DeckService: """Get DeckService with PostgreSQL repositories. Creates a DeckService instance with deck and collection repositories. Args: db: Database session from request. Returns: DeckService configured for PostgreSQL. Example: @router.post("/decks") async def create_deck( service: DeckService = Depends(get_deck_service), ): ... """ deck_repo = PostgresDeckRepository(db) collection_repo = PostgresCollectionRepository(db) card_service = get_card_service() return DeckService(deck_repo, card_service, collection_repo) def get_card_service_dep() -> CardService: """Get the CardService singleton. CardService is a singleton that loads card definitions from JSON files. This dependency provides consistent access for endpoints that need direct card lookup functionality. Returns: The CardService singleton instance. Example: @router.post("/validate") async def validate( card_service: CardService = Depends(get_card_service_dep), ): card = card_service.get_card("a1-001-bulbasaur") """ return get_card_service() def get_game_service_dep() -> GameService: """Get the GameService singleton. GameService orchestrates game lifecycle operations between WebSocket/REST layers and the core GameEngine. Returns: The GameService singleton instance. Example: @router.post("/games") async def create_game( game_service: GameService = Depends(get_game_service_dep), ): result = await game_service.create_game(...) """ return game_service def get_game_state_manager_dep() -> GameStateManager: """Get the GameStateManager singleton. GameStateManager handles game state persistence across Redis and Postgres. Returns: The GameStateManager singleton instance. Example: @router.get("/games/me/active") async def list_active_games( state_manager: GameStateManager = Depends(get_game_state_manager_dep), ): games = await state_manager.get_player_active_games(user_id) """ return game_state_manager def get_user_service( db: Annotated[AsyncSession, Depends(get_db)], ) -> UserService: """Get UserService with PostgreSQL repositories. Creates a UserService instance with user and linked account repositories. Args: db: Database session from request. Returns: UserService configured for PostgreSQL. Example: @router.post("/auth/google/callback") async def google_callback( user_service: UserService = Depends(get_user_service), ): user, created = await user_service.get_or_create_from_oauth(oauth_info) """ user_repo = PostgresUserRepository(db) linked_repo = PostgresLinkedAccountRepository(db) return UserService(user_repo, linked_repo) # ============================================================================= # Type Aliases for Cleaner Endpoint Signatures # ============================================================================= # User dependencies CurrentUser = Annotated[User, Depends(get_current_user)] OptionalUser = Annotated[User | None, Depends(get_optional_user)] PremiumUser = Annotated[User, Depends(get_current_premium_user)] # Database session DbSession = Annotated[AsyncSession, Depends(get_db)] # Service dependencies DeckServiceDep = Annotated[DeckService, Depends(get_deck_service)] CollectionServiceDep = Annotated[CollectionService, Depends(get_collection_service)] CardServiceDep = Annotated[CardService, Depends(get_card_service_dep)] GameServiceDep = Annotated[GameService, Depends(get_game_service_dep)] GameStateManagerDep = Annotated[GameStateManager, Depends(get_game_state_manager_dep)] UserServiceDep = Annotated[UserService, Depends(get_user_service)] # Admin authentication AdminAuth = Annotated[None, Depends(verify_admin_token)]