"""Management authentication: login/logout, session cookies (spec section 8).""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from sqlalchemy import select from app import audit from app.config import get_settings from app.db import get_db from app.deps import SESSION_COOKIE, CurrentUser from app.models import ManagementSession, ManagementUser from app.schemas import LoginRequest, PasswordChangeRequest, SessionInfo from app.security import ( generate_session_id, hash_password, session_expiry, verify_password, ) router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) settings = get_settings() @router.post("/login", response_model=SessionInfo) def login(body: LoginRequest, request: Request, response: Response, db=Depends(get_db)): user = db.scalar( select(ManagementUser).where(ManagementUser.username == body.username) ) ok = user is not None and user.enabled and verify_password( user.password_hash, body.password ) if not ok: audit.record( db, "LOGIN_FAILED", request=request, user=body.username, result="failure", ) raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid credentials") session = ManagementSession( id=generate_session_id(), user_id=user.id, expires_at=session_expiry(settings.session_ttl_seconds), ) db.add(session) audit.record(db, "LOGIN", request=request, user=user.username) db.flush() response.set_cookie( SESSION_COOKIE, str(session.id), httponly=True, samesite="strict", secure=settings.is_production, max_age=settings.session_ttl_seconds, ) return SessionInfo( username=user.username, role=user.role, must_change_password=user.must_change_password, ) @router.post("/logout") def logout(request: Request, response: Response, user: CurrentUser, db=Depends(get_db)): token = request.cookies.get(SESSION_COOKIE) if token: mgmt_session = db.get(ManagementSession, token) if mgmt_session is not None: mgmt_session.revoked = True audit.record(db, "LOGOUT", request=request, user=user.username) response.delete_cookie(SESSION_COOKIE) return {"status": "logged_out"} @router.get("/session", response_model=SessionInfo) def whoami(user: CurrentUser) -> SessionInfo: return SessionInfo( username=user.username, role=user.role, must_change_password=user.must_change_password, ) @router.post("/password") def change_password( body: PasswordChangeRequest, request: Request, response: Response, user: CurrentUser, db=Depends(get_db), ): if not verify_password(user.password_hash, body.current_password): raise HTTPException(status.HTTP_403_FORBIDDEN, "current password incorrect") user.password_hash = hash_password(body.new_password) user.must_change_password = False # Revoke all other sessions as a precaution after credential change. for mgmt_session in db.scalars( select(ManagementSession).where(ManagementSession.user_id == user.id) ): mgmt_session.revoked = True audit.record(db, "PASSWORD_CHANGED", request=request, user=user.username) response.delete_cookie(SESSION_COOKIE) return {"status": "password_changed"}