"""Shared FastAPI dependencies — the composition root for request-scoped wiring. Concrete adapters are bound to ports here so routers and services stay decoupled from infrastructure. Each request gets its own repositories/services bound to the request-scoped DB session; stateless adapters (hasher, token service) are process-cached. """ from collections.abc import AsyncIterator from functools import lru_cache from typing import Annotated from fastapi import Depends from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.ext.asyncio import AsyncSession from app.application.auth_service import AuthService from app.application.user_service import UserService from app.core.config import get_settings from app.core.security import Argon2PasswordHasher, JwtTokenService from app.domain.entities import User from app.domain.errors import AuthenticationError, PermissionDeniedError from app.domain.ports import PasswordHasher, TokenService from app.infrastructure.db import get_sessionmaker from app.infrastructure.db.repositories import ( SqlAlchemyRefreshTokenRepository, SqlAlchemyUserRepository, ) async def get_session() -> AsyncIterator[AsyncSession]: """Request-scoped DB session. Commits on success, rolls back on exception.""" session = get_sessionmaker()() try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() SessionDep = Annotated[AsyncSession, Depends(get_session)] # -- stateless adapters (process-cached) --------------------------------------- @lru_cache def get_password_hasher() -> PasswordHasher: return Argon2PasswordHasher() @lru_cache def get_token_service() -> TokenService: return JwtTokenService(get_settings()) # -- request-scoped services --------------------------------------------------- def get_auth_service(session: SessionDep) -> AuthService: return AuthService( users=SqlAlchemyUserRepository(session), refresh_tokens=SqlAlchemyRefreshTokenRepository(session), hasher=get_password_hasher(), tokens=get_token_service(), ) def get_user_service(session: SessionDep) -> UserService: return UserService( users=SqlAlchemyUserRepository(session), refresh_tokens=SqlAlchemyRefreshTokenRepository(session), hasher=get_password_hasher(), ) AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)] UserServiceDep = Annotated[UserService, Depends(get_user_service)] # -- current user / authorization ---------------------------------------------- # auto_error=False: we raise domain AuthenticationError (mapped to 401) so the # error envelope stays consistent with the rest of the API. _bearer = HTTPBearer(auto_error=False) BearerDep = Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)] async def get_current_user(credentials: BearerDep, auth: AuthServiceDep) -> User: if credentials is None: raise AuthenticationError("Missing bearer token.") return await auth.authenticate_access(credentials.credentials) CurrentUser = Annotated[User, Depends(get_current_user)] async def get_current_superuser(user: CurrentUser) -> User: if not user.is_superuser: raise PermissionDeniedError("Administrator privileges required.") return user SuperUser = Annotated[User, Depends(get_current_superuser)]