from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.crud import role as role_crud from app.crud import token as token_crud from app.crud import user as user_crud from app.deps import get_current_user from app.models.user import User from app.schemas.token import LogoutRequest, Token, TokenRefreshRequest from app.schemas.user import RegisterRequest, UserRead from app.services.auth import ( RefreshError, authenticate, issue_token_pair, rotate_refresh_token, ) router = APIRouter() DEFAULT_USER_ROLE = "user" @router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED) async def register(payload: RegisterRequest, db: AsyncSession = Depends(get_db)): if await user_crud.get_by_email(db, payload.email): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Email already registered" ) default_role = await role_crud.get_by_name(db, DEFAULT_USER_ROLE) role_ids = [default_role.id] if default_role else [] user = await user_crud.create( db, email=payload.email, password=payload.password, full_name=payload.full_name, role_ids=role_ids, ) return user @router.post("/login", response_model=Token) async def login( form: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db), ): # OAuth2 form uses ``username``; we treat it as the email (or LDAP login). user = await authenticate(db, form.username, form.password) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) access, refresh = await issue_token_pair(db, user) return Token(access_token=access, refresh_token=refresh) @router.post("/refresh", response_model=Token) async def refresh(payload: TokenRefreshRequest, db: AsyncSession = Depends(get_db)): try: access, refresh, _ = await rotate_refresh_token(db, payload.refresh_token) except RefreshError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc) ) from exc return Token(access_token=access, refresh_token=refresh) @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) async def logout( payload: LogoutRequest, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): if payload.refresh_token: try: from app.core.security import decode_token jti = decode_token(payload.refresh_token).get("jti") if jti: await token_crud.revoke_by_jti(db, jti) except Exception: # noqa: BLE001 - best-effort revocation of a single token pass else: await token_crud.revoke_all_for_user(db, user.id)