"""Authentication routes + first-launch setup wizard.""" from __future__ import annotations import time from collections import defaultdict, deque from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlmodel import Session import auth as auth_mod from database import get_session from models.user import ( LoginRequest, RefreshRequest, TokenPair, User, UserCreate, UserRead, ) from services import audit_service router = APIRouter(prefix="/api/auth", tags=["auth"]) # Simple in-memory rate limiter for login (max 10 / minute / IP). _LOGIN_HITS: dict[str, deque] = defaultdict(deque) _RATE_LIMIT = 10 _RATE_WINDOW = 60.0 def _check_rate_limit(ip: str) -> None: now = time.monotonic() hits = _LOGIN_HITS[ip] while hits and now - hits[0] > _RATE_WINDOW: hits.popleft() if len(hits) >= _RATE_LIMIT: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many login attempts, slow down.", ) hits.append(now) def _tokens_for(user: User) -> TokenPair: return TokenPair( access_token=auth_mod.create_access_token(user), refresh_token=auth_mod.create_refresh_token(user), ) @router.get("/needs-setup") def needs_setup(session: Session = Depends(get_session)) -> dict: """First-launch wizard check: True if no users exist yet.""" return {"needs_setup": not auth_mod.users_exist(session)} @router.post("/setup", response_model=TokenPair) def setup( body: UserCreate, session: Session = Depends(get_session) ) -> TokenPair: if auth_mod.users_exist(session): raise HTTPException(status_code=400, detail="Setup already completed") user = User( username=body.username, hashed_password=auth_mod.hash_password(body.password), role="admin", ) session.add(user) session.commit() session.refresh(user) audit_service.record( session, user=user.username, action="user.setup", target=user.username ) return _tokens_for(user) @router.post("/login", response_model=TokenPair) def login( body: LoginRequest, request: Request, session: Session = Depends(get_session), ) -> TokenPair: ip = request.client.host if request.client else "unknown" _check_rate_limit(ip) user = auth_mod.authenticate(session, body.username, body.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", ) audit_service.record( session, user=user.username, action="auth.login", target=user.username, ip=ip ) return _tokens_for(user) @router.post("/refresh", response_model=TokenPair) def refresh( body: RefreshRequest, session: Session = Depends(get_session) ) -> TokenPair: payload = auth_mod.decode_token(body.refresh_token, "refresh") user = auth_mod.get_user(session, payload.get("sub", "")) if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" ) return _tokens_for(user) @router.get("/me", response_model=UserRead) def me(user: User = Depends(auth_mod.get_current_user)) -> User: return user