"""Wiederverwendbare FastAPI-Dependencies.""" from typing import Annotated from fastapi import Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from app.core.cookies import ACCESS_COOKIE from app.core.errors import AppError, AuthError from app.core.security import decode_token from app.db.session import get_session from app.models import AppUser from app.services.auth import get_user from app.services.settings import month_start_day DbSession = Annotated[AsyncSession, Depends(get_session)] class PasswordChangeRequiredError(AppError): """Der Zugriff ist erst nach dem erzwungenen Passwortwechsel möglich.""" status_code = 403 code = "password_change_required" async def get_current_user(request: Request, session: DbSession) -> AppUser: """Liest das Access-Token aus dem Cookie und lädt den Benutzer.""" token = request.cookies.get(ACCESS_COOKIE) if not token: raise AuthError("Nicht angemeldet.", code="not_authenticated") payload = decode_token(token, "access") try: user_id = int(payload["sub"]) except (KeyError, TypeError, ValueError) as exc: raise AuthError("Ungültiges Token.", code="invalid_token") from exc user = await get_user(session, user_id) if user is None: raise AuthError("Der Benutzer existiert nicht mehr.", code="invalid_token") return user CurrentUser = Annotated[AppUser, Depends(get_current_user)] async def get_active_user(user: CurrentUser) -> AppUser: """Wie `get_current_user`, verlangt aber einen abgeschlossenen Passwortwechsel.""" if user.must_change_password: raise PasswordChangeRequiredError( "Das Passwort muss zuerst geändert werden.", ) return user ActiveUser = Annotated[AppUser, Depends(get_active_user)] async def get_month_start_day(session: DbSession) -> int: """Der eingestellte Monatsbeginn – jede monatsbezogene Auswertung richtet sich danach.""" return await month_start_day(session) MonthStartDay = Annotated[int, Depends(get_month_start_day)]