Files
social-app/backend/src/v1/users/dependencies.py
T

176 lines
5.5 KiB
Python

from __future__ import annotations
import asyncio
from typing import Annotated
from uuid import UUID
from fastapi import Depends, Header
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.http.errors import ApiProblemError
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 ApiProblemError(
status_code=503,
code="JWT_VERIFIER_NOT_CONFIGURED",
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 ApiProblemError(
status_code=401,
code="AUTH_UNAUTHORIZED",
detail="Unauthorized",
)
scheme, _, token = authorization.partition(" ")
if scheme.lower() != "bearer" or not token:
logger.warning("JWT validation failed: invalid authorization scheme")
raise ApiProblemError(
status_code=401,
code="AUTH_UNAUTHORIZED",
detail="Unauthorized",
)
try:
payload = get_jwt_verifier().verify(token)
except ApiProblemError:
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 ApiProblemError(
status_code=401,
code="AUTH_UNAUTHORIZED",
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 ApiProblemError(
status_code=401,
code="AUTH_UNAUTHORIZED",
detail="Unauthorized",
)
try:
user_id = UUID(subject)
except ValueError:
logger.warning("JWT validation failed: invalid UUID in subject")
raise ApiProblemError(
status_code=401,
code="AUTH_UNAUTHORIZED",
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,
)