fix(server,web): auth-gate client APK + version endpoints + per-user rate limit (#397)
Closes the bandwidth-abuse vector on the in-app update flow. Both
endpoints now sit inside the authed.Group; APK additionally gets a
60s/user rate limit to suppress accidental hammering or scripted
abuse.
### Server
- internal/api/client_assets.go:
- clientAPKAllowDownload(): in-memory map[userID]time.Time under
a mutex. Returns 0 (allow) or wait duration (block).
- handleClientAPK reads user from context, checks the limit,
returns 429 + Retry-After header if blocked.
- testResetClientAPKRateLimit() lets tests start clean.
- internal/api/api.go: routes moved from the root /api group into
the authed.Group block (alongside /quarantine, /requests, etc.).
- Tests: added TestClientAPK_401WhenUnauthenticated and
TestClientAPK_RateLimit_429OnRapidSecondCall (also verifies
different user gets a fresh slot). Existing tests updated to use
authedRequest() helper.
### Web
- MobileAppDownload.svelte: switched from bare fetch (no credentials)
to api.get<>() which carries the session cookie. 404 / 401 /
network errors all silently hide the download row.
- Removed the login-page mount entirely — pre-auth surfaces should
never show this. Settings → Mobile app section keeps it for
logged-in users.
Flutter unaffected: dio's Bearer interceptor already attaches the
token, and the polling only fires once the post-login shell mounts
the banner widget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -52,11 +52,6 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
api.Post("/auth/register", h.handleRegister)
|
||||
api.Post("/auth/forgot-password", h.handleForgotPassword)
|
||||
api.Post("/auth/reset-password", h.handleResetPassword)
|
||||
|
||||
// Self-hosted in-app update channel (#397). Unauthenticated so
|
||||
// the install flow doesn't depend on a live session.
|
||||
api.Get("/client/version", h.handleClientVersion)
|
||||
api.Get("/client/apk", h.handleClientAPK)
|
||||
api.Group(func(authed chi.Router) {
|
||||
authed.Use(auth.RequireUser(pool))
|
||||
authed.Post("/auth/logout", h.handleLogout)
|
||||
@@ -106,6 +101,12 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
authed.Delete("/quarantine/{track_id}", h.handleUnflag)
|
||||
authed.Get("/quarantine/mine", h.handleListMyQuarantine)
|
||||
|
||||
// Self-hosted in-app update channel (#397). Auth-gated to
|
||||
// prevent anonymous bandwidth abuse on the APK stream;
|
||||
// /apk additionally per-user rate-limited.
|
||||
authed.Get("/client/version", h.handleClientVersion)
|
||||
authed.Get("/client/apk", h.handleClientAPK)
|
||||
|
||||
authed.Route("/admin", func(admin chi.Router) {
|
||||
admin.Use(auth.RequireAdmin())
|
||||
admin.Get("/lidarr/config", h.handleGetLidarrConfig)
|
||||
|
||||
Reference in New Issue
Block a user