from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.core.rate_limit import limiter from app.core.security import create_access_token, verify_password_konstante_zeit from app.db.session import get_db from app.models.auth import Benutzer router = APIRouter() class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" class MeResponse(BaseModel): id: int name: str login: str rollen: list[str] @router.post("/auth/login", response_model=TokenResponse) @limiter.limit("5/minute") async def login( request: Request, form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db), ) -> TokenResponse: result = await db.execute(select(Benutzer).where(Benutzer.login == form_data.username)) benutzer = result.scalar_one_or_none() # Passwort-Vergleich läuft IMMER (auch bei unbekanntem Login), damit die # Antwortzeit nicht verrät, ob ein Login existiert (jwt-expert-Review-Fund). passwort_ok = verify_password_konstante_zeit( form_data.password, benutzer.passwort_hash if benutzer else None ) if benutzer is None or not benutzer.aktiv or not passwort_ok: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Login oder Passwort falsch", headers={"WWW-Authenticate": "Bearer"}, ) token = create_access_token(subject=benutzer.login) return TokenResponse(access_token=token) @router.get("/auth/me", response_model=MeResponse) async def me(current_user: Benutzer = Depends(get_current_user)) -> MeResponse: return MeResponse( id=current_user.id, name=current_user.name, login=current_user.login, rollen=current_user.rollen_namen, )