refactor(api): shared serializer + ownership + write-scope helpers (R6 S1a)

Establish the R6 API-contract infra before the S1b payload:dict migration:
- iso(dt) in utils.py replaces the ~16 hand-rolled `x.isoformat() if x else None`
  serializer idioms across the routes.
- get_or_404 / owned_or_404 in routes/_common.py — one 404 + existence-leak policy;
  plex's _own_playlist_or_404 and saved_views' _own_view now delegate to it.
- require_write_scope dependency in auth.py collapses the inline has_write_scope→403
  checks (channels subscribe/unsubscribe, playlists push/push-plan) that had drifted
  into three different messages into one; the conditional delete_playlist guard stays
  inline (its message was already canonical).

No response-shape change (iso == isoformat for a present value). Gate: ruff clean,
pytest 163 green (+ test_utils for iso).
This commit is contained in:
2026-07-26 04:17:08 +02:00
parent a711b51224
commit c5533b0ac5
14 changed files with 132 additions and 60 deletions
+3 -2
View File
@@ -27,6 +27,7 @@ from app.ratelimit import RateLimiter
from app.realtime import manager
from app.security import verify_password
from app.userscope import is_messageable_user, messageable_clauses
from app.utils import iso
router = APIRouter(prefix="/api/messages", tags=["messages"])
@@ -97,8 +98,8 @@ def _serialize_msg(m: Message) -> dict:
"body": m.body, # only set for system messages
"ciphertext": m.ciphertext,
"iv": m.iv,
"read_at": m.read_at.isoformat() if m.read_at else None,
"created_at": m.created_at.isoformat() if m.created_at else None,
"read_at": iso(m.read_at),
"created_at": iso(m.created_at),
}