2026-02-26 13:33:02 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
|
|
|
from fastapi import Depends, Header, HTTPException
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
2026-03-09 18:03:04 +08:00
|
|
|
from core.auth.jwt_verifier import (
|
|
|
|
|
JwtVerifier,
|
|
|
|
|
TokenValidationError,
|
|
|
|
|
TokenVerifierUnavailableError,
|
|
|
|
|
)
|
2026-02-26 13:33:02 +08:00
|
|
|
from core.auth.models import CurrentUser
|
|
|
|
|
from core.config.settings import config
|
|
|
|
|
from core.db import get_db
|
|
|
|
|
from core.logging import get_logger
|
2026-02-27 15:22:42 +08:00
|
|
|
from v1.auth.gateway import SupabaseAuthGateway
|
2026-02-26 13:33:02 +08:00
|
|
|
from v1.users.repository import SQLAlchemyUserRepository
|
2026-02-27 15:22:42 +08:00
|
|
|
from v1.users.service import AuthLookupAdapter, UserService
|
2026-02-26 13:33:02 +08:00
|
|
|
|
|
|
|
|
logger = get_logger("v1.users.dependencies")
|
|
|
|
|
|
2026-02-27 15:22:42 +08:00
|
|
|
_auth_gateway: SupabaseAuthGateway | None = None
|
2026-03-09 18:03:04 +08:00
|
|
|
_jwt_verifier: JwtVerifier | None = None
|
2026-02-27 15:22:42 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_auth_gateway() -> SupabaseAuthGateway:
|
|
|
|
|
global _auth_gateway
|
|
|
|
|
if _auth_gateway is None:
|
|
|
|
|
_auth_gateway = SupabaseAuthGateway()
|
|
|
|
|
return _auth_gateway
|
|
|
|
|
|
2026-02-26 13:33:02 +08:00
|
|
|
|
2026-03-09 18:03:04 +08:00
|
|
|
def get_jwt_verifier() -> JwtVerifier:
|
|
|
|
|
global _jwt_verifier
|
|
|
|
|
if _jwt_verifier is None:
|
|
|
|
|
jwks_url = config.supabase.jwks_url
|
|
|
|
|
issuer = config.supabase.jwt_issuer
|
|
|
|
|
audience = config.supabase.jwt_audience
|
|
|
|
|
if not jwks_url or not issuer or not audience:
|
|
|
|
|
logger.error("JWT validation failed: verifier config not configured")
|
|
|
|
|
raise HTTPException(status_code=503, detail="JWT verifier not configured")
|
|
|
|
|
_jwt_verifier = JwtVerifier(jwks_url=jwks_url, issuer=issuer, audience=audience)
|
|
|
|
|
return _jwt_verifier
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 13:33:02 +08:00
|
|
|
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:
|
2026-03-09 18:03:04 +08:00
|
|
|
payload = get_jwt_verifier().verify(token)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except TokenVerifierUnavailableError:
|
|
|
|
|
logger.error("JWT validation failed: verifier unavailable")
|
|
|
|
|
raise HTTPException(status_code=503, detail="JWT verifier unavailable")
|
|
|
|
|
except TokenValidationError as exc:
|
2026-02-26 13:33:02 +08:00
|
|
|
logger.warning(
|
2026-03-09 18:03:04 +08:00
|
|
|
"JWT validation failed",
|
|
|
|
|
error_type=type(exc).__name__,
|
2026-02-26 13:33:02 +08:00
|
|
|
)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 13:36:34 +08:00
|
|
|
async def get_user_repository(
|
|
|
|
|
session: Annotated[AsyncSession, Depends(get_db)],
|
|
|
|
|
) -> SQLAlchemyUserRepository:
|
|
|
|
|
return SQLAlchemyUserRepository(session)
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 13:33:02 +08:00
|
|
|
def get_user_service(
|
|
|
|
|
session: Annotated[AsyncSession, Depends(get_db)],
|
|
|
|
|
user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> UserService:
|
|
|
|
|
repository = SQLAlchemyUserRepository(session)
|
2026-02-27 15:22:42 +08:00
|
|
|
auth_gateway = AuthLookupAdapter(get_auth_gateway())
|
|
|
|
|
return UserService(
|
|
|
|
|
repository=repository,
|
|
|
|
|
session=session,
|
|
|
|
|
current_user=user,
|
|
|
|
|
auth_gateway=auth_gateway,
|
|
|
|
|
)
|