| Age | Commit message (Collapse) | Author |
|
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>
|
|
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>
|
|
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
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>
|
|
- 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>
|
|
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>
|
|
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.
|
|
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>
|
|
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.
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|