from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings 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 import ldap as ldap_service from app.services.auth import RefreshError, issue_token_pair, rotate_refresh_token router = APIRouter() # Fallback default role name if no role is flagged as default in the DB. DEFAULT_USER_ROLE = "user" async def _default_role_ids(db: AsyncSession) -> list[int]: role = await role_crud.get_default(db) if role is None: role = await role_crud.get_by_name(db, DEFAULT_USER_ROLE) return [role.id] if role else [] @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" ) user = await user_crud.create( db, email=payload.email, password=payload.password, full_name=payload.full_name, role_ids=await _default_role_ids(db), ) 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. user = await user_crud.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("/ldap-login", response_model=Token) async def ldap_login( form: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db), ): """Authenticate against LDAP, provisioning the local profile on first login.""" if not settings.ldap_enabled: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="LDAP login is not enabled" ) user = await ldap_service.login(db, form.username, form.password) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username 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)