Backend of #197 Bundle A (auth posture). Owner decisions: single API key;
Api:RequireKeyForReads defaults true (whole /api surface gated; /iptv streaming
+ guide unaffected — outside the filter's /api scope).
- #280 S1: writes are fail-closed. New IApiKeyProvider resolves the key once
(Api:WriteKey config, else persisted /config/api.key, else a generated 256-bit
key written 0600). The empty-key open branch is gone; there is no open mode.
- #282 S3/S5: reads under /api require the key when Api:RequireKeyForReads (default
true) or the endpoint carries the new [RequiresApiKey]. Applied [RequiresApiKey]
to Troubleshoot/Logs/Settings/Maintenance so the sensitive tier stays gated even
if reads are opened. OPTIONS preflight is exempt.
- #281 S2: delete SortController (dead Blazor SortableJS residue; SPA uses PUT
/api/collections/{id}/custom-order) and AccountController (dead OIDC logout) —
both non-/api persistent surfaces that bypassed the key.
- #284 S6: replace CORS AllowAll with an opt-in exact-origin allowlist
(Api:CorsAllowedOrigins; permits X-Api-Key/If-Match, exposes ETag). Default is
no cross-origin (SPA is same-origin).
- #285 S7/S10: gc GET->POST (spec regenerated); ForwardedHeaders trust configurable
via ForwardedHeaders:KnownProxies/KnownNetworks (warns when unrestricted);
ScannerController gains [LocalhostOnly] (scanner always calls back over localhost).
Filter unit tests rewritten for fail-closed + read-gating + tier + OPTIONS;
ApiControllerSecurityTests assert the sensitive tier + scanner-loopback reflectively.
search/all-items paging deferred (SPA add-all coupling) — exposure closed by read-gating.
Refs #197#280#281#282#284#285
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blazor parity for the remaining #213 conveniences:
- GET /api/logs gains sortField (timestamp|level) and sortDirection
(asc|desc) query params, allow-listed and normalized (unrecognized
values fall back to the pre-existing timestamp-desc default) rather
than rejected with a 422. LogsScreen.tsx renders clickable, sortable
column headers with a chevron direction indicator.
- LogsScreen.tsx now persists the chosen page size to localStorage
(ctv-logs-page-size) and restores it on mount, following the
existing designSystem.ts localStorage-preference pattern. This is a
client-local UI preference, not the Blazor ConfigElement-backed
server setting — see docs/decisions.md.
- TrashScreen.tsx adds a per-kind "See all N ..." affordance that
pages past the 100/kind /api/search cap using the already-paginated
GET /api/library/browse (mediaType + pageNum), appending results
client-side. No new API surface was needed since that endpoint
already supports the paging the trash screen needed.
docs/decisions.md, docs/blazor-route-parity.md, docs/spa-conventions.md
and docs/api-conventions.md updated in this same commit. OpenAPI spec
regenerated (v1.d.ts unchanged: query params aren't part of the
generated components/schemas surface).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add GET /api/logs (thin wrapper over GetRecentLogEntries; paged, filtered,
clamped pageSize like LibraryBrowseController) and GET /api/troubleshoot/info
(wraps GetTroubleshootingInfo; General section serialized to the same JSON
shape the legacy Blazor Troubleshooting page renders, plus per-platform
capability dumps). Also adds [EndpointGroupName("general")] to the existing
troubleshoot playback endpoints so they surface in the OpenAPI spec (#158
item 3).