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>
- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
command (mirrors CollectionController.AddItems); controller pre-checks
playlist existence for a real 404, and the handler now rejects adds to
system (generated) playlists, matching the guard already applied to
rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
query, returning a new SearchResultAllItemsResponseModel (never expose
the VM directly) so the SPA's shared "add all to collection/playlist"
component can materialize ids before calling the add endpoints, same
two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
libraryId, title, and mediaSourceKind (serialized as a string enum via
the global StringEnumConverter) - no changes needed.
Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
Backend slice for the ChicoryTV playouts and collections screens.
PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
playout item's stored context by row id via a new
GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)
CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
when the collection's UseCustomPlaybackOrder is set
Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add three GET endpoints to TroubleshootController for the SPA port of the
playback troubleshooting page:
- /api/troubleshoot/playback/stream-selectors (List<string>)
- /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via
GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel)
- /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel:
idle/running/completed/failed + exitCode/speed + logs.txt tail)
Regenerate v1.json, endpoint-index.md, and the web API types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add write endpoints to PlaylistController for the playlist editor: group
create/rename/delete, playlist create/read/update/delete, item list read,
and draft playout preview. Introduces a RenamePlaylistGroup command/handler
(the one missing Application-layer operation) plus request/response DTOs.
Endpoints (verb / route / route-Name):
- POST /api/playlists/groups CreatePlaylistGroup
- PUT /api/playlists/groups/{id} UpdatePlaylistGroup (rename)
- DELETE /api/playlists/groups/{id} DeletePlaylistGroup
- GET /api/playlists/{id} GetPlaylistById
- GET /api/playlists/{id}/items GetPlaylistItems
- POST /api/playlists CreatePlaylistInGroup
- PUT /api/playlists/{id} UpdatePlaylist (rename + replace items)
- DELETE /api/playlists/{id} DeletePlaylist
- POST /api/playlists/preview PreviewPlaylist
404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a
controller-side existence pre-check (mirrors TemplateController.DeleteGroup),
leaving existing shared handlers untouched; validation failures return 422.
RenamePlaylistGroup returns NotFoundError -> 404 for a missing group.
Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA
client types (web/src/api/generated/v1.d.ts). No SPA screen in this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds MultiCollectionController + RerunCollectionController (list/get/create/update/
delete) over the existing MediatR handlers, with response/request DTOs, controller +
contract tests, regenerated OpenAPI + TS types, and thin SPA api-client modules. SPA
editor screens are a follow-up. Rerun request DTO resolves the chosen entity id into
the minimal VM the handler reads (documented inline).
Refs #151, #152
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds docs/testing.md as the authoritative testing map (consolidated
from docs/contributing.md §8, now shrunk to a pointer), and a
generated docs/endpoint-index.md via scripts/generate-endpoint-index.py
(hooked into scripts/update-openapi.sh). Updates docs/README.md's
reading order and removes the "still to come" placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>