summaryrefslogtreecommitdiff
path: root/player-server/internal/api
AgeCommit message (Collapse)Author
2026-05-21Add GET /api/v1/auth/count endpoint and first-run routing in Android appPaul Buetow
Server: expose a public countUsers endpoint (GET /api/v1/auth/count) so mobile clients can detect first-run (count=0) without a session. Android: wire countUsers via DioPlayerApiClient, add firstRunProvider (FutureProvider), update go_router redirect to drive /bootstrap vs /login based on the count, rework LoginScreen to handle loading/error states, and add widget tests for the new login screen and smoke-test updates. Fix review issues: correct FutureProvider cache-lifetime comment in first_run_provider.dart; add TestServer_CountUsers covering zero-users and users-exist cases to handlers_test.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20Extract app wiring from main.go into internal/app package (9a)Paul Buetow
Move all dependency wiring, background worker startup, server lifecycle (Wire, StartBackgroundWorkers, RunServer, RunWithSignal, BuildLogger) into internal/app so cmd/player/main.go becomes thin: parse flags, load config, delegate to app.RunWithSignal. Updated main_test.go to call app.Wire and app.StartBackgroundWorkers directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Move serveFileResult and serveRemuxed to api/stream.go (3a)Paul Buetow
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Validate AbsPath in streamer and clean up partial podcast downloads (l9+k9)Paul Buetow
l9: Add mediaRoot field to mediaStreamer. NewMediaStreamer now takes a mediaRoot string parameter; when non-empty, Open() rejects any path that does not reside under that directory (filepath.Clean prefix check), returning ErrForbidden to prevent filepath-traversal via a compromised AbsPath in the DB. Production wiring passes cfg.MediaRoot; tests that don't exercise path traversal pass "". Added TestMediaStreamerOpenRejectsPathOutsideRoot to cover the rejection path. k9: Add a defer-based cleanup guard in DownloadEpisode. After the enclosure file is written, a succeeded flag gates a deferred closure that calls dbCleanup() (undoes DB row + removes file) when persistDownloadedEpisode succeeded, or removeAndLog(path) when it did not. This ensures that any failure after the file is written — including UpdateEpisodeMedia — leaves no orphaned files on disk. The guard is disarmed by setting succeeded=true on the happy path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Merge j9+i9: LIKE wildcard escaping and CreateUser password validationPaul Buetow
- escapeLike() helper in repository/media.go for LIKE search safety - ErrWeakPassword sentinel in service.go (min 8 chars, HTTP 400) - CreateUser rejects empty/short passwords in service/user.go - ErrWeakPassword auto-dispatched via HTTPStatuser (no switch needed) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Block bootstrap.html access after first user created (o9)Paul Buetow
serveBootstrap now calls CountUsers() and redirects to /login.html when the user count is non-zero, preventing the bootstrap form from being reachable on an already-configured instance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Dispatch handleError via HTTPStatuser interface (4a)Paul Buetow
handleError previously hard-coded a switch mapping every service sentinel to its HTTP status. Adding a new sentinel required editing handleError (OCP violation). Move the status onto the sentinel itself: introduce api.HTTPStatuser { HTTPStatus() int }, convert the 10 dispatched service sentinels to a typed *apiError that implements it, and turn handleError into a thin errors.As-based dispatcher. ErrShareExpired stays a plain sentinel because it is handled inline by share handlers and never reaches handleError. Sentinel messages are aligned with what handleError used to emit (e.g. ErrForbidden is now "forbidden" instead of "access denied", ErrAlreadyBootstrapped is "bootstrap already complete") so the dispatcher can use err.Error() uniformly. Wrapped errors (fmt.Errorf("%w: ...", ErrNotFound, ...)) keep their context in the response body, which is a minor improvement over the previous behaviour that collapsed everything to the bare sentinel message. errors.Is keeps working via pointer equality on the *apiError sentinels.
2026-05-20Inject clock.Clock into api.Server and replace time.Now() in share/auth handlersPaul Buetow
handlers_share.go (handleCreateShare share-expiry) and handlers_auth.go (setSessionCookie and apiTokenExpiresAt) previously called time.Now() directly, which made time-dependent semantics impossible to assert deterministically in tests. They now use s.clk.Now(), where s.clk is a clock.Clock injected through ServerDeps.Clock (nil-default to clock.RealClock{} so existing callers keep working unchanged). apiTokenExpiresAt is promoted to a method on *Server so it can reach the injected clock. Production wiring in cmd/player/main.go passes deps.clk so handlers share the same time source as the rest of the services. Two new unit tests (handlers_share_test.go) use clock.MockClock to assert handleCreateShare and setSessionCookie compute their expiry timestamps from the injected clock rather than the wall clock. While propagating, this commit also includes a mechanical fix-up for the pathID(...) signature change (now returns int64 + error) across the API handler files so the package still builds; the additional err-check makes malformed path variables produce 400 instead of silently parsing as zero. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Buffer JSON before writing response in writeJSON (y9)Paul Buetow
Previously writeJSON wrote the status header before encoding, so an encode failure produced a misleading 200 (or other caller status) with a truncated body. Marshal into a bytes.Buffer first; on error emit 500 with a JSON error envelope instead.
2026-05-19Inject narrow api.Authenticator into NewMiddlewarePaul Buetow
The middleware previously took the full service.AuthService, but only called three methods on it. Define a narrow api.Authenticator interface (AuthenticateBearer, CountUsers, GetUserByID) and inject that instead, fixing the ISP violation. service.AuthService satisfies the new interface structurally, so callers and tests need no changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Remove nil streamer fallback in api.serveFileResult (DIP)Paul Buetow
The serveFileResult handler used to fabricate a default MediaStreamer at request time when s.streamer was nil. That silently masked wiring mistakes and violated the Dependency Inversion Principle — the handler was inventing its own dependency instead of demanding one from the caller. Removed the fallback so the handler now uses s.streamer directly. Construction-time validation of deps.MediaStreamer was added to api.NewServerWithLogger in commit 4215db6, which makes the runtime nil case unreachable. Mirrors the explicit-deps pattern set in commits 622827c (http.Client in podcast service) and 92edb83 (TokenManager in auth service). Test helpers updated to inject a service.NewMediaStreamer(nil) when one isn't otherwise supplied so the existing suite still constructs Servers through the validating constructor. Refs agent task 6a. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Replace hardcoded public-path whitelist with route registryPaul Buetow
Previously internal/api/middleware.go contained an isBootstrapPublic function with a hardcoded switch over public paths plus three /css/ /js/ /images/ prefix checks. Whenever a new public route was added in server.go a developer also had to remember to extend the switch — easy to miss, and the symptom is a silent 307 to /bootstrap.html. Public-route metadata now lives on the Middleware itself: * Middleware gets publicPaths map[string]bool and publicPrefixes []string * RegisterPublic(path) / RegisterPublicPrefix(prefix) populate the registry * BootstrapRedirect consults isPublic(path) instead of a hardcoded list Server.routes() registers each public route through new helpers (handlePublic / handlePublicFunc / handlePublicPrefix) so the mux pattern and the bypass set are declared together — there is no separate whitelist to keep in sync. Routes migrated: 14 exact (bootstrap/login {/api,/api/v1/auth} variants, /healthz, /readyz, /login.html, /bootstrap.html, /favicon.{ico,svg}, /logo.{png,svg}, /manifest.json, /sw.js) and 4 prefixes (/css/, /js/, /images/, /s/ for tokenised share URLs). TestMiddleware_BootstrapRedirect now seeds the registry explicitly (middleware-level unit test, no full server). A new TestServer_PublicRouteRegistry asserts the full set of public paths is registered after NewServer(). Refs: agent task ha. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Complete s9 + aa: NewServerWithLogger returns error; share-page renderer ↵Paul Buetow
extracted Two related refactors that converged on internal/api/server.go. s9 — NewServerWithLogger no longer panics on nil deps.Config. Returns (*Server, error) so cmd/player/main.go can log a clear message and exit cleanly when wiring is misconfigured. All callers updated, including api unit tests that construct a Server directly. aa — Share-page HTML rendering moved out of handlers_share.go into a new internal/web package. internal/web/sharepage.go owns SharePageRenderer and the private injectShareMedia helper; the api handler now calls s.shareRenderer.Render(...) and routes errors via http.Error. injectShareMedia coverage was moved alongside it in internal/web/sharepage_test.go; the duplicate tests in handlers_more_test.go were removed (placeholder comment left so the reference is greppable). Server gained one new field (shareRenderer *web.SharePageRenderer) and one new constructor line; production wiring uses deps.StaticFS. Background: both tasks were initially worked by separate sub-agents that stopped mid-edit when usage limits hit; their WIP was preserved in a stash and finalized in this commit. Tests rerun from a clean state: full Go unit suite green, 25/25 LLM e2e, 22/22 Playwright. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Require explicit TokenManager in auth service (DIP)Paul Buetow
Remove the silent nil-fallback in NewAuthService that fabricated a default auth.TokenManager when callers passed nil. The service now panics on nil, forcing callers to inject their own TokenManager and keeping construction at the composition root. Production wiring (cmd/player/main.go) already constructs the TokenManager explicitly via auth.NewTokenManager(); test call sites in internal/api/handlers_test.go and handlers_podcast_test.go that used to pass nil now pass auth.NewTokenManager() to honour the new contract. Mirrors the pattern set in commit 622827c (http.Client in podcast service). Refs task 7a. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Fix four defects flagged by S19/S20/S24; tighten scenariosPaul Buetow
1. tagService.AssignTag and RemoveTag now use verifyModifyAccess (owner role required) instead of verifyAccess. Tags are global state visible to every user with access to a media item, so a viewer must not be able to add or remove them. Favorites and notes stay on verifyAccess because they're per-user data (favorites.user_id, media_notes.user_id) and don't affect anyone else. Verified via curl: viewer POST /media/{id}/tags now 403, admin still 200. 2. serveFileResult now emits a strong ETag header ("<size>-<mtime-nanos>") before calling http.ServeContent. Go's ServeContent honours If-None-Match when ETag is set, so iOS audio clients and podcast apps can revalidate cached downloads with conditional GETs. Verified via curl: ETag present on /stream; If-None-Match matching the ETag returns 304. 3. MediaFilter gains IncludeDeleted flag; ListMedia skips the implicit `deleted_at IS NULL` predicate when it is set. FSScanner.loadExistingMedia now passes IncludeDeleted=true so the dedup map includes soft-deleted rows. Previously a re-scan of a soft-deleted file tried to CreateMedia and hit the UNIQUE(set_id, rel_path) constraint, failing the whole scan and setting progress.last_error. Now the rescan skips the row cleanly; soft-delete sticks. 4. FSScanner.reconcileOrphans soft-deletes media rows whose underlying file disappeared between scans. The scanner used to only walk files that exist and never compare against the DB, leaving phantom rows in GET /api/v1/media that 404'd on stream. Verified via curl: rm /testdata/.../orphan.mp3, rescan, row now has deleted_at != NULL. Scenarios updated to lock in the fixed behaviour: S19 step 15 — viewer tag-add now asserts 403, not 200. S20 step 14 — asserts ETag is present and If-None-Match → 304. S24 step 13 — asserts clean rescan (no last_error from UNIQUE). S24 step 20 — asserts orphan rows are soft-deleted by rescan. Verified: full Go unit suite passes; 25/25 LLM e2e scenarios; 22/22 Playwright e2e-web. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19Map service errors to correct HTTP status codes (was 500)Paul Buetow
Three more handlers had the same pattern as the RevokeShare bug fixed in 0848488: the service returned a plain errors.New(...) for an expected condition, so handleError fell through to its default 500 branch instead of mapping to 404/400. - browse.GetThumbnail → ErrNotFound (404 instead of 500) when the media row has no thumbnail path. - tag.RemoveTag → ErrNotFound (404 instead of 500) when the tag name does not exist. DELETE /media/{id}/tags/{unknown} now returns 404. - write.RegenerateSetCover → new sentinel ErrEmptySetForCover, mapped to 400 in handleError, when the target set contains no media files eligible to be promoted to a cover. S15 (auth-boundary negatives) is tightened: the share-revoke step used to accept {404, 500} as a workaround for the bug; it now requires 404. A new section C2 locks in the three regressions above so they cannot silently return 500 again. Verified end-to-end via curl: DELETE /media/{id}/tags/unknown → 404, DELETE /shares/unknown-token → 404. Full LLM e2e suite 15/15 still passes against the rebuilt server. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18Add /api/v1/ integration test suitePaul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18Add playback hints endpoint for client codec/container decisionsPaul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17Add bulk progress sync endpoint for offline mobile clientsPaul Buetow
2026-05-17Add /api/v1/auth/tokens endpoints for API token managementPaul Buetow
2026-05-17Add Bearer token authentication alongside session cookiesPaul Buetow
2026-05-17Add api_tokens table, model, and repositoryPaul Buetow
2026-05-17Add CORS middleware for multi-origin clientsPaul Buetow
2026-05-17Add /api/v1/ versioning aliasPaul Buetow
2026-05-17Restructure repo: move Go server into player-server/Paul Buetow