from __future__ import annotations from typing import Annotated from uuid import UUID from fastapi import Depends, Header, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from core.auth.jwt_verifier import ( JwtVerifier, TokenValidationError, ) from core.auth.models import CurrentUser from core.config.settings import config from core.db import get_db from core.logging import get_logger from v1.auth.gateway import SupabaseAuthGateway from v1.users.repository import SQLAlchemyUserRepository from v1.users.service import AuthLookupAdapter, UserService logger = get_logger("v1.users.dependencies") _auth_gateway: SupabaseAuthGateway | None = None _jwt_verifier: JwtVerifier | None = None def get_auth_gateway() -> SupabaseAuthGateway: global _auth_gateway if _auth_gateway is None: _auth_gateway = SupabaseAuthGateway() return _auth_gateway def get_jwt_verifier() -> JwtVerifier: global _jwt_verifier if _jwt_verifier is None: issuer = config.supabase.jwt_issuer jwt_secret = ( config.supabase.jwt_secret.get_secret_value() if config.supabase.jwt_secret is not None else None ) if not issuer or not jwt_secret: logger.error("JWT validation failed: verifier config not configured") raise HTTPException(status_code=503, detail="JWT verifier not configured") _jwt_verifier = JwtVerifier( issuer=issuer, jwt_secret=jwt_secret, jwt_algorithm=config.supabase.jwt_algorithm, ) return _jwt_verifier def get_current_user(authorization: str | None = Header(default=None)) -> CurrentUser: if not authorization: logger.warning("JWT validation failed: missing authorization header") raise HTTPException(status_code=401, detail="Unauthorized") scheme, _, token = authorization.partition(" ") if scheme.lower() != "bearer" or not token: logger.warning("JWT validation failed: invalid authorization scheme") raise HTTPException(status_code=401, detail="Unauthorized") try: payload = get_jwt_verifier().verify(token) except HTTPException: raise except TokenValidationError as exc: logger.warning( "JWT validation failed", error_type=type(exc).__name__, reason=str(exc), ) raise HTTPException(status_code=401, detail="Unauthorized") from exc subject = payload.get("sub") if not isinstance(subject, str) or not subject: logger.warning("JWT validation failed: missing or invalid subject claim") raise HTTPException(status_code=401, detail="Unauthorized") try: user_id = UUID(subject) except ValueError: logger.warning("JWT validation failed: invalid UUID in subject") raise HTTPException(status_code=401, detail="Unauthorized") logger.debug("JWT validation successful", user_id=str(user_id)) email = payload.get("email") if isinstance(payload.get("email"), str) else None role = payload.get("role") if isinstance(payload.get("role"), str) else None return CurrentUser(id=user_id, email=email, role=role) async def get_user_repository( session: Annotated[AsyncSession, Depends(get_db)], ) -> SQLAlchemyUserRepository: return SQLAlchemyUserRepository(session) def get_user_service( session: Annotated[AsyncSession, Depends(get_db)], user: Annotated[CurrentUser, Depends(get_current_user)], ) -> UserService: repository = SQLAlchemyUserRepository(session) auth_gateway = AuthLookupAdapter(get_auth_gateway()) return UserService( repository=repository, session=session, current_user=user, auth_gateway=auth_gateway, )