"""User settings + per-sender image allowlist (SPEC §9.1 Settings, §9.4).""" from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_session from app.identity.deps import CurrentUser, require_csrf from app.models import User router = APIRouter(prefix="/settings", tags=["settings"]) DbSession = Annotated[AsyncSession, Depends(get_session)] _ALLOWED_ORDER = {"asc", "desc"} class SettingsOut(BaseModel): thread_order: str = "asc" tz: str = "UTC" allowed_image_senders: list[str] = [] class SettingsPatch(BaseModel): thread_order: str | None = None tz: str | None = None def allowed_image_senders(user: User) -> set[str]: value = user.settings.get("allowed_image_senders", []) if isinstance(value, list): return {str(e).lower() for e in value} return set() def add_allowed_image_sender(user: User, email: str) -> None: current = sorted(allowed_image_senders(user)) if email.lower() not in current: current.append(email.lower()) # Reassign the whole dict so SQLAlchemy detects the JSONB change. user.settings = {**user.settings, "allowed_image_senders": current} def _settings_out(user: User) -> SettingsOut: s = user.settings return SettingsOut( thread_order=str(s.get("thread_order", "asc")), tz=str(s.get("tz", "UTC")), allowed_image_senders=sorted(allowed_image_senders(user)), ) @router.get("", response_model=SettingsOut) async def get_settings_endpoint(user: CurrentUser) -> SettingsOut: return _settings_out(user) @router.patch("", response_model=SettingsOut, dependencies=[Depends(require_csrf)]) async def patch_settings(body: SettingsPatch, user: CurrentUser, db: DbSession) -> SettingsOut: updates = dict(user.settings) if body.thread_order is not None and body.thread_order in _ALLOWED_ORDER: updates["thread_order"] = body.thread_order if body.tz is not None: updates["tz"] = body.tz user.settings = updates await db.commit() return _settings_out(user)