feat(m3): native auth — login+TOTP, token storage, refresh, access-level menu
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m41s

Implements M3 (docs/android/PLAN.md §4): the functional Kotlin auth pass.

- Native username/password (+ single-request TOTP) login over the existing
  POST /auth/mobile/login; a 401 { totpRequired } reveals the code field, 429
  surfaces a backoff message (§4.1).
- Token pair in EncryptedSharedPreferences (TokenStore behind SessionManager,
  the single source of truth for the in-memory bearer + observable Session);
  base URL stays in plain DataStore (§4.3).
- OkHttp AuthInterceptor (bearer) + TokenAuthenticator: one-shot, mutex-
  serialized refresh-on-401 that replays the request, on its own bare client so
  it can never recurse; single-use rotation; dead refresh signs out, transient
  network keeps the session.
- Logout (POST /auth/mobile/logout, this session or all devices) tears down
  locally even on failure.
- GET /auth/me re-validates the role on every resume; a surviving 401 signs out
  (role stays advisory — backend is authority).
- Declarative access-level menu (visibleEntries: public/signed-in/player) with a
  Sign in / Sign out toggle + a My Account screen.
- Custom-Tab hand-offs (androidx.browser) to the website for register / forgot-
  password / SSO — no native screens (§4.2).
- Settings → Server switch now also clears the stored session (§3).

Biometric app-lock is deferred to M6 (tokens already encrypted at rest; it is
opt-in UX, not a v1 requirement — decided at M3).

JVM unit tests (18): auth-DTO decode (incl. totpRequired vs a plain credential
401), the SessionManager lifecycle over a fake store, and the menu access filter
+ role mapping. No backend/API change — a pure consumer of the existing mobile
bearer + /auth/me surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-19 19:08:00 -05:00
parent 199df6cd64
commit 1c56eda64b
29 changed files with 1625 additions and 21 deletions

View File

@@ -32,6 +32,9 @@ retrofitSerializationConverter = "1.0.0"
datastore = "1.1.1"
securityCrypto = "1.1.0-alpha06"
# Web hand-off (Chrome Custom Tabs) — §4.2
browser = "1.8.0"
# Images
coil = "2.7.0"
@@ -79,6 +82,9 @@ retrofit-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit",
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "securityCrypto" }
# Web hand-off
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
# Images
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }