""" Auth API — shared helpers, Pydantic request models, and the Limiter instance. This module is imported by every auth sub-module (tokens.py, totp.py, password.py). The Limiter instance is re-exported from api/auth/__init__.py so that: from api.auth import limiter continues to work in main.py and the 5 test files without modification. """ from __future__ import annotations from typing import Literal, Optional from fastapi import Response from pydantic import BaseModel, EmailStr from slowapi import Limiter from config import settings from deps.utils import get_client_ip # IP-level rate limiter (SEC-02 — 10 req/min on register/login/refresh) # Re-exported from api/auth/__init__.py via: from api.auth.shared import limiter limiter = Limiter(key_func=get_client_ip) # ── Request models ──────────────────────────────────────────────────────────── class RegisterRequest(BaseModel): handle: str email: EmailStr password: str class LoginRequest(BaseModel): email: EmailStr password: str totp_code: Optional[str] = None backup_code: Optional[str] = None remember_me: bool = False class ChangePasswordRequest(BaseModel): current_password: str new_password: str class TotpEnableRequest(BaseModel): code: str class PasswordResetRequest(BaseModel): email: EmailStr class PasswordResetConfirmRequest(BaseModel): token: str new_password: str class PreferencesUpdate(BaseModel): """Request body for PATCH /api/auth/me/preferences. Validates pdf_open_mode strictly via Literal (T-04-05-05 — no mass assignment). """ pdf_open_mode: Literal["in_app", "new_tab"] # ── Helper: set httpOnly refresh cookie ────────────────────────────────────── def _set_refresh_cookie( response: Response, raw_token: str, remember_me: bool = False ) -> None: """Set the httpOnly Secure SameSite=Strict refresh cookie (CLAUDE.md constraint). remember_me=False (default): Max-Age = refresh_token_expire_hours * 3600 (16h, D-11, RM-03) remember_me=True: Max-Age = refresh_token_expire_days * 86400 (30d, D-11, RM-03) """ max_age = ( settings.refresh_token_expire_days * 86400 if remember_me else settings.refresh_token_expire_hours * 3600 ) response.set_cookie( key="refresh_token", value=raw_token, httponly=True, secure=True, samesite="strict", path="/api/auth/refresh", max_age=max_age, ) def _user_dict(user: object) -> dict: """Return serialisable user metadata (no password_hash, no credentials_enc).""" return { "id": str(user.id), "handle": user.handle, "email": user.email, "role": user.role, "totp_enabled": user.totp_enabled, "created_at": user.created_at.isoformat() if user.created_at else None, }