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
+5 -5
View File
@@ -27,7 +27,7 @@ from app.models import (
Video,
VideoState,
)
from app.utils import valid_email
from app.utils import iso, valid_email
router = APIRouter(prefix="/api/admin", tags=["admin"])
@@ -38,8 +38,8 @@ def _serialize(inv: Invite) -> dict:
"email": inv.email,
"status": inv.status,
"note": inv.note,
"requested_at": inv.requested_at.isoformat() if inv.requested_at else None,
"decided_at": inv.decided_at.isoformat() if inv.decided_at else None,
"requested_at": iso(inv.requested_at),
"decided_at": iso(inv.decided_at),
"decided_by": inv.decided_by,
}
@@ -147,7 +147,7 @@ def _serialize_user(u: User) -> dict:
"is_demo": u.is_demo,
"has_password": u.password_hash is not None,
"has_google": u.google_sub is not None,
"created_at": u.created_at.isoformat() if u.created_at else None,
"created_at": iso(u.created_at),
}
@@ -280,7 +280,7 @@ def _serialize_demo(row: DemoWhitelist) -> dict:
"email": row.email,
"note": row.note,
"added_by": row.added_by,
"created_at": row.created_at.isoformat() if row.created_at else None,
"created_at": iso(row.created_at),
}