"""Anmeldung, Abmeldung, Token-Erneuerung und Passwortwechsel.""" from fastapi import APIRouter, Request, Response, status from app.api.deps import CurrentUser, DbSession from app.core.cookies import ( REFRESH_COOKIE, clear_auth_cookies, set_auth_cookies, warn_if_cookie_unusable, ) from app.core.errors import AuthError from app.schemas.auth import ChangePasswordRequest, LoginRequest, UserOut from app.schemas.common import ErrorResponse, MessageResponse from app.services.auth import ( Credentials, change_password, get_auth_provider, issue_tokens, revoke_refresh_token, rotate_tokens, ) router = APIRouter(prefix="/auth", tags=["auth"]) UNAUTHORIZED = {status.HTTP_401_UNAUTHORIZED: {"model": ErrorResponse}} @router.post( "/login", response_model=UserOut, responses=UNAUTHORIZED, summary="Anmelden", description="Prüft die Zugangsdaten und legt Access- und Refresh-Token als " "httpOnly-Cookies ab.", ) async def login( payload: LoginRequest, request: Request, response: Response, session: DbSession ) -> UserOut: # Meldet die Kombination, bei der der Browser das Cookie stillschweigend verwirft. warn_if_cookie_unusable(request) provider = get_auth_provider("local") user = await provider.authenticate( session, Credentials(username=payload.username, password=payload.password) ) tokens = await issue_tokens(session, user) await session.commit() set_auth_cookies( response, tokens.access_token, tokens.access_expires_at, tokens.refresh_token, tokens.refresh_expires_at, ) return UserOut.model_validate(user) @router.post( "/refresh", response_model=UserOut, responses=UNAUTHORIZED, summary="Sitzung erneuern", description="Tauscht das Refresh-Token gegen ein neues Paar. Das alte Token " "wird dabei gesperrt (Rotation).", ) async def refresh(request: Request, response: Response, session: DbSession) -> UserOut: token = request.cookies.get(REFRESH_COOKIE) if not token: clear_auth_cookies(response) raise AuthError("Nicht angemeldet.", code="not_authenticated") user, tokens = await rotate_tokens(session, token) await session.commit() set_auth_cookies( response, tokens.access_token, tokens.access_expires_at, tokens.refresh_token, tokens.refresh_expires_at, ) return UserOut.model_validate(user) @router.post( "/logout", response_model=MessageResponse, summary="Abmelden", description="Sperrt das aktuelle Refresh-Token und löscht beide Cookies.", ) async def logout(request: Request, response: Response, session: DbSession) -> MessageResponse: await revoke_refresh_token(session, request.cookies.get(REFRESH_COOKIE)) await session.commit() clear_auth_cookies(response) return MessageResponse(detail="Abgemeldet.") @router.post( "/change-password", response_model=MessageResponse, responses=UNAUTHORIZED, summary="Passwort ändern", description="Ändert das Passwort und beendet dabei alle bestehenden Sitzungen.", ) async def change_own_password( payload: ChangePasswordRequest, response: Response, user: CurrentUser, session: DbSession, ) -> MessageResponse: await change_password(session, user, payload.current_password, payload.new_password) await session.commit() clear_auth_cookies(response) return MessageResponse(detail="Passwort geändert. Bitte neu anmelden.")