3 Commits

Author SHA1 Message Date
bvandeusen 154f415f92 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>
2026-05-10 20:28:27 -04:00
bvandeusen 14d7678624 fix: CI lint errors after #397 commits
- flutter analyze: Riverpod 3 dropped StateProvider; replaced
  _dismissedVersionsProvider with a small Notifier<Set<String>>
  + NotifierProvider, mutating via an `add(version)` method.
  Public API (shouldShowUpdateBannerProvider, dismissUpdateProvider)
  unchanged.
- golangci errcheck: defer f.Close() now wrapped in func() { _ = f.Close() }();
  os.Setenv calls in test helper switched to t.Setenv (cleaner — auto-restores
  on test cleanup, no manual Unsetenv needed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:01:43 -04:00
bvandeusen a2bea8601a feat(server): /api/client/version + /api/client/apk endpoints (#397 phase 1)
Phase 1 of the in-app update flow — server side. Endpoints serve the
bundled Android APK + sidecar version file from /app/client/.
Returns 404 gracefully when files aren't present, so dev environments
and pre-CI-wiring images degrade cleanly to "no update available."

- internal/api/client_assets.go: handleClientVersion + handleClientAPK.
  Both unauthenticated (matches /healthz) so install flow doesn't
  depend on a live session. APK served with proper
  application/vnd.android.package-archive Content-Type +
  http.ServeContent so Range requests work for resumable downloads
  on flaky networks.
- Path resolves to /app/client/ by default; MINSTREL_CLIENT_APK_DIR
  env var overrides for dev.
- Dockerfile creates /app/client/ + commented COPY hooks for the CI
  sequencing phase.
- Tests cover all four states: missing apk, apk-but-no-version,
  both present (200 with correct shape), apk stream (200 with
  correct Content-Type + body bytes).

Phases 2 (Flutter client provider + banner + install intent + Android
manifest changes) and 3 (CI sequencing to bake the APK into the image)
land in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:35:24 -04:00