feat: auth & admin
This commit is contained in:
+74
-2
@@ -1,17 +1,31 @@
|
||||
"""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. Repository/service providers are added in
|
||||
later steps as the domain grows.
|
||||
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]:
|
||||
@@ -28,3 +42,61 @@ async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user