from __future__ import annotations import asyncio 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 services.base.supabase import supabase_service 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 async def _verify_user_with_supabase(token: str) -> CurrentUser | None: try: client = supabase_service.get_client() except Exception as exc: # noqa: BLE001 logger.warning("Supabase fallback unavailable", reason=str(exc)) return None try: response = await asyncio.to_thread(client.auth.get_user, token) except Exception as exc: # noqa: BLE001 logger.warning("Supabase token fallback validation failed", reason=str(exc)) return None user = getattr(response, "user", None) if user is None: return None user_id = getattr(user, "id", None) if not isinstance(user_id, str) or not user_id: return None try: parsed_id = UUID(user_id) except ValueError: return None phone = getattr(user, "phone", None) role = getattr(user, "role", None) return CurrentUser( id=parsed_id, phone=phone if isinstance(phone, str) else None, role=role if isinstance(role, str) else None, ) async 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), ) fallback_user = await _verify_user_with_supabase(token) if fallback_user is None: raise HTTPException(status_code=401, detail="Unauthorized") from exc logger.info("JWT fallback validation succeeded", user_id=str(fallback_user.id)) return fallback_user 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)) phone = payload.get("phone") if isinstance(payload.get("phone"), str) else None role = payload.get("role") if isinstance(payload.get("role"), str) else None return CurrentUser(id=user_id, phone=phone, 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, )