feat(phase-4-05): PATCH /api/auth/me/preferences for pdf_open_mode (DOC-01)

- Add PreferencesUpdate Pydantic model with Literal['in_app', 'new_tab'] validation
- Add GET /api/auth/me/preferences — returns current pdf_open_mode
- Add PATCH /api/auth/me/preferences — validates + stores + returns updated value
- Both endpoints use get_current_user (admin can set own prefs, D-10)
- Add 7 preference tests: default GET, in_app, new_tab, invalid 422, persist,
  and two unauthenticated 401 tests
This commit is contained in:
curo1305
2026-05-25 18:50:52 +02:00
parent f89f787656
commit 2a0df32e92
2 changed files with 124 additions and 1 deletions
+51 -1
View File
@@ -21,7 +21,7 @@ from __future__ import annotations
import re
import uuid
from typing import Optional
from typing import Literal, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import BaseModel, EmailStr
@@ -632,3 +632,53 @@ async def password_reset_confirm(
# Do NOT issue tokens (AUTH-05 — user must pass TOTP gate on next login)
return {"message": "Password updated. Please sign in."}
# ── Preferences models ────────────────────────────────────────────────────────
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"]
# ── GET /api/auth/me/preferences ─────────────────────────────────────────────
@router.get("/me/preferences")
async def get_my_preferences(
current_user: User = Depends(get_current_user),
):
"""Return the current user's PDF open mode preference (D-10).
Both regular users and admins can read their own preferences.
Falls back to 'in_app' if the column is absent (migration not yet run).
"""
try:
pdf_open_mode = current_user.pdf_open_mode
except AttributeError:
pdf_open_mode = "in_app"
return {"pdf_open_mode": pdf_open_mode}
# ── PATCH /api/auth/me/preferences ───────────────────────────────────────────
@router.patch("/me/preferences")
async def update_my_preferences(
body: PreferencesUpdate,
session: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update the current user's PDF open mode preference (D-10).
Both regular users and admins can update their own preferences.
Pydantic Literal["in_app", "new_tab"] enforces strict allowlist (T-04-05-05).
"""
user = await session.get(User, current_user.id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
user.pdf_open_mode = body.pdf_open_mode
session.add(user)
await session.commit()
return {"pdf_open_mode": user.pdf_open_mode}