b1d5fbefcba02fdc6c19fef85cec1c4e82fc8dea
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8ae4d62ab |
fix(552): mint a short-lived JWT so the SPA reaches /iptv/* under JWT auth
Under a JWT-enabled deployment (JWT:IssuerSigningKey set), /iptv/* is gated by ConditionalIptvAuthorizeFilter and the "jwt" scheme does not accept the SPA's ctv-session cookie, and nothing minted a JWT for the browser. So the #60 channel preview was declared Unavailable and could not run at all. Add GET /api/v1/auth/iptv-token (session-gated, on the [IgnoreApi] AuthController): mints a short-lived global token via JwtHelper.GenerateBrowserToken (60 min default, JWT:BrowserTokenLifetimeMinutes override), 204 when JWT is disabled. The SPA's new withIptvToken(url) helper appends it as ?access_token= to the manifest URL (a no-op when JWT is off), used by the channel-preview panel and the troubleshooting screen. Mapper.GetPreview drops its iptvJwtEnabled -> Unavailable guard; preview is now JWT-agnostic. Live-E2E under JWT: /iptv manifest 401s without a token and passes with a valid one (garbage token -> 401); token endpoint 401s anonymous, mints with a session. Honest finding: the issue's point 2 (troubleshooting screen broken under JWT) does not reproduce -- its live.m3u8 is static-served (UseStaticFiles at /iptv/session), outside the JWT filter, so it was never gated. The withIptvToken call there is a harmless defensive no-op. Docs: security.iptv-browser-token (api-auth-security.md), amended api.channel-preview-capability, spa-conventions §5b. No OpenAPI change (IgnoreApi + unchanged Preview schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef2bd65c27 |
feat(api): #286 — mount the whole /api surface at /api/v1
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Version every /api route to /api/v1 (251 controller routes + ~24 Location
headers + the scanner callback URL + the Startup request-log literal),
uniform across the machine API, auth, scanner and scripted-build surfaces.
Add ApiVersionRewriteMiddleware: a legacy unversioned /api/* request is
rewritten (NOT redirected) to /api/v1/* in-pipeline — method, body, auth
headers and query survive — carrying RFC 8594 Deprecation/Sunset headers,
so curl / the future MCP server / bookmarks keep working. An already-
versioned path passes through; a future /api/v2 is never forced to v1.
Standardize the route convention (leading-slash absolute route per method,
no class-[Route] — except the two Scanner/Scripted controllers whose ~all
actions share a parametrized {id} prefix), enforced by ApiRouteVersioningTests
(^/api/v\d+/ over the whole Controllers.Api surface; browser-nav
/auth/oidc/login is out of scope).
Regenerate v1.json (160 paths, all /api/v1)/endpoint-index/v1.d.ts; sweep 945
SPA request literals + the test mocks (regex + positional URL parsers). /api/v1
is additive-only after freeze; the legacy-rewrite shim sunsets in ~2 releases
(owner decision) with removal tracked as a Phase-3 follow-up.
Docs: decisions.md 2026-07-13, api-conventions §1/§9, rest-api/spa-conventions/
blazor-route-parity/e2e-local/domain-model.
fixes #286
refs #197
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ec26e1be5b |
fix(api): #316 review — POST-ify graphics-elements refresh, LockedError→409, no-store machine-key
- GET /api/graphics-elements no longer side-effects; refresh moved to POST /api/graphics-elements/refresh (204), closing a CSRF vector on a GET. - PrepareTroubleshootingPlaybackHandler now returns a typed LockedError from both atomic lock-acquire failures; ApiResults.ToErrorResult maps it to 409 instead of falling through to 422, so a lock lost in the race between the controller's pre-check and the handler's atomic acquire still reports 409. - AuthController.MachineKey sets Cache-Control: no-store + Pragma: no-cache on the 200 response carrying the master API key. - Reworded the stale "subtitleId query parameter" endpoint description now that playback/start takes a JSON body. - Regenerated openapi/v1.json + docs/endpoint-index.md; docs/api-conventions.md updated with the LockedError pattern (§3a) and the ToErrorResult table row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
88d82266b6 |
feat(api): #301 POST-ify side-effecting troubleshoot GETs + GET /api/auth/machine-key
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e8c3481ea5 |
fix(api): #295 PR1 — fold in fix-commit re-review (2nd Codex round)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m9s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Fix-commit re-review confirmed the 1st-round fixes resolved and caught a 2nd round: - HIGH — env-seed vs. setup race: an attacker could claim admin in the startup window before LocalAdminSeedService runs, and the seed's insert would then be swallowed (attacker credential persists, defeating env recovery). Fixed structurally: the setup-claim endpoint is CLOSED (409) whenever Auth:LocalAdmin:Password is configured — the env seed owns the credential, so there's no claim to race (also strengthens the setup-claim TOFU posture). Config.setupRequired reflects it. - LOW — a concurrent setup race-loser now returns 409 (not 422); ClaimLocalAdmin's DbUpdateException catch re-checks existence and rethrows genuine/transient DB errors instead of masking them as "already configured". - MEDIUM (accepted, documented) — two simultaneous authenticated password changes are a non-serializable lost-update; accepted for a single-admin system (self-healing via re-login, implausible timing). +3 AuthController tests (env-seed closes setup / setupRequired gating). Full ErsatzTV.Tests green (1506); no generated drift. Docs updated. Refs #295 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9a9aaf0740 |
fix(api): #295 PR1 — logout ends the session server-side (E2E-caught)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Live E2E found that replaying a pre-logout cookie still authenticated (200, not 401): SignOutAsync only clears the CLIENT cookie, but the stateless encrypted cookie ticket stays valid server-side because its security stamp is unchanged — a captured cookie was replayable after logout until ticket expiry. Fix: logout now rotates the local-admin security stamp (RotateLocalAdminSecurityStamp), so every outstanding local session (old stamp) fails OnValidatePrincipal on its next request. For the single admin this is "log out everywhere". Gated on an authenticated local session so an unauthenticated caller can't force-revoke the admin. OIDC sessions (no stamp) are unaffected; SignOutAsync still clears the client cookie for UX. +2 handler tests (rotate-when-configured / no-op-when-unconfigured). Auth suite green (21). Docs: decisions.md note updated. Refs #295 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6ac5150fd0 |
fix(api): #295 PR1 — fold in cold-fork + Codex review findings
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent review (cold fork = MERGEABLE-WITH-NITS; Codex = BLOCKED, caught concurrency defects the fork missed). All actionable findings folded in: - HIGH (Codex) atomic first-claim-wins: ClaimLocalAdmin now writes the three credential rows in ONE transaction guarded by the unique ConfigElement.Key index (lost race -> DbUpdateException -> 409), so concurrent claims can't produce a mixed-state credential. - HIGH (Codex) consistent login snapshot: VerifyLocalAdminLogin reads hash+stamp in one query and drops rehash-on-verify, so a login racing a password change can't capture a stamp newer than the hash it verified (concurrent change -> old password fails, or the issued cookie carries the pre-change stamp -> revoked next request). - MEDIUM (Codex) env-seed migration race: LocalAdminSeedService is now a RunOnce BackgroundService that awaits SystemStartup.WaitForDatabase (the migrator is a BackgroundService; registration order didn't guarantee the schema) + try/catch. - MEDIUM (fork M1) ForwardedHeaders: reverted the strict-opt-in flip — it would regress /iptv M3U/XMLTV/HLS absolute-URL generation (Request.Scheme) behind a proxy without KnownProxies. Kept #285 behavior; KnownProxies still recommended. - LOW (Codex/fork) require X-CSRF on /api/auth/logout + /password (the [SkipApiAuthorization] surface isn't covered by the filter's CSRF check; closes forced-logout CSRF). - ChangeLocalAdminPassword also writes hash+stamp atomically. Input length caps on username/password. Deferred with a tracked gate: MEDIUM (Codex) side-effecting [RequiresAuthentication] GETs (troubleshoot playback/archive) aren't CSRF-covered -> #301, gates PR2 (latent in PR1: the SPA still uses the machine key). Verify: full ErsatzTV.Tests green (1501); no OpenAPI/generated drift. Docs updated (api-conventions §9, decisions.md). Refs #295 #301 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0b23d4b6b1 |
feat(api): #295 PR1 — browser SPA session auth (session-OR-key gate, server-only)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Implements the ratified #295 design (PR1, server-only, backward compatible). The /api surface now accepts a valid X-Api-Key (machine) OR an authenticated session (browser cookie, local login or OIDC), gated by the evolved ApiAuthorizationFilter (renamed from ApiKeyAuthorizationFilter; same fail-closed EndpointRequiresKey predicate). Machine/key behavior is byte-identical and the SPA keeps working via its stored key — the SPA login flow lands in PR2. - ApiAuthorizationFilter: key-first (CSRF-immune) then session; session-authed mutations require the X-CSRF header (403 otherwise). Attributes renamed [RequiresApiKey]->[RequiresAuthentication], [SkipApiKeyAuthorization]->[SkipApiAuthorization]. - Cookie scheme ctv-session always registered (Lax/SameAsRequest/14d sliding, 401 not redirect for /api); OIDC handler revived when configured (profile scope, userinfo, auth-method claim); UseAuthentication/UseAuthorization/UseRateLimiter revived in the legacy MapWhen branch. - Local admin = single credential in ConfigElement rows (username / PBKDF2 hash via Microsoft.Extensions.Identity.Core / rotating security stamp) — NO DB migration. Password change rotates the stamp; CookieSecurityStampValidator revokes stale local sessions. Env-seed recovery (Auth:LocalAdmin:*) via LocalAdminSeedService. - AuthController /api/auth/{config,session,setup,login,logout,password} + browser-nav GET /auth/oidc/login; excluded from OpenAPI (machine-audience spec). Per-IP rate limit on login/setup/password; dummy-hash verify (no user enumeration). - ForwardedHeaders now strict opt-in: X-Forwarded-* ignored unless KnownProxies/Networks configured (rate-limiter IP + cookie-Secure integrity). Deployment: operators behind a proxy must set ForwardedHeaders:KnownProxies. - Tests: session/CSRF filter cases + 17 Application/Auth handler tests; full ErsatzTV.Tests green (1499). No OpenAPI/generated-artifact drift. - Docs: api-conventions section 9 rewritten; decisions.md entry (supersedes #206 inert-OIDC note). Refs #295 #197 #206 #58 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |