Add authentication

This commit is contained in:
2026-04-27 19:28:57 +02:00
parent 6684b7179d
commit e0af5f4053
92 changed files with 8140 additions and 208 deletions

View File

@@ -0,0 +1,189 @@
---
phase: 02-authentication-foundation
plan: 07
subsystem: auth
tags: [kmp, compose-multiplatform, material3, koin-viewmodel, compose-resources, auth-gate]
requires:
- phase: 02-authentication-foundation
provides: 02-06 AuthSession StateFlow, AuthState model, authModule Koin singletons
provides:
- SplashScreen / LoginScreen / PostLoginPlaceholderScreen Phase 2 auth gate
- LoginViewModel + LoginScreenState + PostLoginViewModel mapping AuthSession results to Compose Resources
- Compose Resources strings for the seven Phase 2 auth keys
- RecipeTheme Material 3 light/dark seed with primary `#3B6939` / `#A2D597`
affects: [phase-03-households]
tech-stack:
added:
- kotlinx-coroutines-test (commonTest only) for the multiplatform `runTest` runtime
patterns:
- "App.kt observes AuthSession.state via collectAsStateWithLifecycle and renders one of three screens; no manual navigation."
- "LoginViewModel.onSignInClick() returns the launched Job so commonTest can join() deterministically without dragging in a TestDispatcher."
- "ViewModels registered in authModule via org.koin.core.module.dsl.viewModel; consumed via koinViewModel<T>()."
- "All commonTest coroutine tests use kotlinx.coroutines.test.runTest so wasmJs can compile (runBlocking is JVM/Native-only)."
key-files:
created:
- composeApp/src/commonMain/composeResources/values/strings.xml
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt
- .planning/phases/02-authentication-foundation/deferred-items.md
modified:
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt
- composeApp/build.gradle.kts
- gradle/libs.versions.toml
key-decisions:
- "ViewModels registered in authModule (alongside AuthSession) instead of a new uiModule — keeps the single Koin module that owns AuthSession also owning its UI consumers."
- "LoginViewModel.onSignInClick() returns Job rather than swallowing it so tests deterministically join without a TestDispatcher; production callers ignore the returned Job."
- "AuthSession.initialize() is launched from a LaunchedEffect in App.kt rather than a Koin lifecycle hook; keeps Phase 2 startup explicit and easy to trace."
- "Pre-existing ./gradlew check failures (Android JVM SecureAuthStateStoreContractTest, ios SecureAuthStateStore ktlint) are out of scope for 02-07 and tracked in deferred-items.md per scope-boundary rule."
requirements-completed: [AUTH-01, AUTH-04, AUTH-05]
duration: 10m
completed: 2026-04-28
---
# Phase 02 Plan 07: Auth UI Gate Summary
**Phase 2 auth UI gate — SplashScreen / LoginScreen / PostLoginPlaceholderScreen wired to AuthSession via koinViewModel, with externalized Polish strings and a Material 3 seed theme.**
## Performance
- **Duration:** ~10 min (automated tasks)
- **Started:** 2026-04-28T15:31:20Z
- **Automated work completed:** 2026-04-28T15:41:31Z
- **Tasks completed:** 2 of 3 (Task 3 awaits manual iOS Authentik UAT)
- **Files created:** 9
- **Files modified:** 5
## Accomplishments
- Added all seven Phase 2 Compose Resources keys with the Polish scaffold copy from `02-UI-SPEC.md`.
- Added `RecipeTheme` with light/dark Material 3 schemes seeded by `#3B6939` / `#A2D597` and `isSystemInDarkTheme()`.
- Replaced the JetBrains template `App()` body with the auth-gate `when` over `AuthSession.state`, observing via `collectAsStateWithLifecycle` and kicking `AuthSession.initialize()` from `LaunchedEffect`.
- Implemented `SplashScreen`, `LoginScreen`, and `PostLoginPlaceholderScreen` using Material 3 stdlib only — no Scaffold, no Haze, all strings via `stringResource(Res.string.*)`.
- Implemented `LoginViewModel` (mapping AuthSession failures → `auth_error_*` `StringResource` keys, clearing stale errors on retry) and trivial `PostLoginViewModel.onSignOutClick()` delegating to `AuthSession.logout()`.
- Registered both ViewModels in `authModule` via `org.koin.core.module.dsl.viewModel`.
- Added `kotlinx-coroutines-test` to `commonTest` so the wasmJs target can compile coroutine tests (replacing JVM-only `runBlocking` with multiplatform `runTest` in both `LoginViewModelTest` and the existing `AuthSessionTest`).
## Task Commits
1. **Task 1 (RED): Compose Resources, theme seed, failing LoginViewModel tests**`466e4c7` (test)
2. **Task 2 (GREEN): Auth screens, ViewModels, App auth gate**`88f4898` (feat)
3. **Task 2 follow-up: switch commonTest to runTest for wasmJs compatibility**`570652c` (fix)
4. **Task 3 (manual UAT): pending — see Awaiting User UAT below**
## Files Created/Modified
### Created
- `composeApp/src/commonMain/composeResources/values/strings.xml` — Phase 2 auth strings (Polish scaffold).
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` — Material 3 seed theme.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt` — wordmark + progress.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt` — wordmark + button + inline error.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt``LoginScreenState` + `onSignInClick()` mapping.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt` — welcome + logout.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt``onSignOutClick()``AuthSession.logout()`.
- `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt` — five tests covering cancelled/network/unknown/success and clear-error-on-retry.
- `.planning/phases/02-authentication-foundation/deferred-items.md` — log of pre-existing failures.
### Modified
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` — auth-gate `when` body.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt``viewModel { ... }` registrations.
- `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt``runBlocking``runTest`.
- `composeApp/build.gradle.kts``commonTest` `kotlinx-coroutines-test` dependency.
- `gradle/libs.versions.toml` — added `kotlinx-coroutinesTest` library entry.
## Decisions Made
See frontmatter `key-decisions`.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 — Blocking] commonTest coroutine tests must use `runTest`, not `runBlocking`**
- **Found during:** Task 2 verification, when `./gradlew check` ran the wasmJs test target for the first time.
- **Issue:** `kotlinx.coroutines.runBlocking` is JVM/Native-only and breaks `:composeApp:compileTestKotlinWasmJs`. The pre-existing `AuthSessionTest` (committed in Plan 02-06) used the same pattern and was never wasmJs-tested — `02-06` only ran `:composeApp:jvmTest`. Phase 02-07's verification gate is the first one to catch it.
- **Fix:** Added `org.jetbrains.kotlinx:kotlinx-coroutines-test` to `commonTest`, switched both `AuthSessionTest` and the new `LoginViewModelTest` from `runBlocking` to `runTest`.
- **Files modified:** `composeApp/build.gradle.kts`, `gradle/libs.versions.toml`, `AuthSessionTest.kt`, `LoginViewModelTest.kt`.
- **Verification:** `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` exits 0.
- **Committed in:** `570652c`.
### Out-of-scope discoveries (not fixed; logged)
See `deferred-items.md`:
- `SecureAuthStateStoreContractTest` (Android JVM unit) fails on `master` HEAD before any 02-07 change — Android Keystore unavailable in plain JVM unit tests; needs Robolectric or `androidTest`.
- `composeApp/src/iosMain/.../SecureAuthStateStore.ios.kt:L31` ktlint `property-naming` violation pre-exists on `master`.
Both originate in Plans 02-04 / 02-05 and are out of scope for this UI plan per the executor scope-boundary rule.
## Issues Encountered
- `./gradlew spotlessApply` reformatted many pre-existing files unrelated to 02-07 (because the repo had pre-existing format drift). Those reformats were reverted before commit so the 02-07 commits stay scope-clean. Spotless's failure on the unrelated `SecureAuthStateStore.ios.kt` ktlint rule is logged in `deferred-items.md`.
## Known Stubs
None. The auth gate is fully wired end to end; all rendered text is sourced from Compose Resources, and ViewModels delegate to the real `AuthSession` Koin singleton.
The `PostLoginPlaceholderScreen` itself is a Phase 2 placeholder by design — Phase 3's `HouseholdGate` replaces it. This is documented in `02-UI-SPEC.md` and `02-CONTEXT.md` and is not a stub.
## Threat Flags
None beyond the plan's threat model. The new UI surfaces only render strings and dispatch to `AuthSession`; tokens are never logged or rendered (T-02-07-01). Logout (T-02-07-02) is the only state-changing action wired in `PostLoginViewModel`. Login button explicitly mentions Authentik (T-02-07-03). Refresh failures route silently to `LoginScreen` per `02-UI-SPEC.md`'s refresh-failure UX (T-02-07-04). All copy comes from `composeResources/values/strings.xml` (T-02-07-05).
## User Setup Required
**Manual iOS Authentik UAT (Task 3 — checkpoint:human-verify, blocking):**
Per `02-VALIDATION.md` § Manual-Only Verifications and `docs/authentik-setup.md`:
1. Fresh install opens Splash then `LoginScreen`.
2. Tap `Zaloguj się przez Authentik` — hosted Authentik login opens and returns through `recipe://callback`.
3. App shows `Witaj, {displayName}!`.
4. Restart after access-token expiry (or short token lifetime) — app returns to authenticated screen without credentials.
5. Tap `Wyloguj się` — app returns to LoginScreen; restart does not silently authenticate.
6. `GET /api/v1/me` returns 200 with valid token; 401 without token or with wrong-audience token.
Reply with `approved` to mark Phase 2 complete, or describe the failing step (with tokens redacted) so the gate can be re-opened.
## Verification
### Automated — passing
- `./gradlew :composeApp:jvmTest --tests "*LoginViewModelTest*"` — PASS (5 tests).
- `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` — PASS.
- Acceptance grep checks: `auth_sign_in_button` Polish copy present, `0xFF3B6939` in `RecipeTheme`, `auth_error_cancelled` referenced in `LoginViewModelTest`, no `Click me!` in `App.kt`, `collectAsState` + `SplashScreen` present in `App.kt`, `auth_welcome_format` in `PostLoginPlaceholderScreen`, no raw Polish strings in any `.kt` source under `dev/ulfrx/recipe/`.
### Automated — pre-existing failures (not introduced by 02-07; tracked in deferred-items.md)
- `:composeApp:testDebugUnitTest` — 2 failures in `SecureAuthStateStoreContractTest`.
- `:composeApp:spotlessKotlinCheck` — 1 ktlint violation in `SecureAuthStateStore.ios.kt`.
### Manual — pending
- iOS Authentik UAT (Task 3 — see User Setup Required above).
## Next Phase Readiness
Phase 3 (households) can now extend `AuthState.Authenticated.householdId` and replace `PostLoginPlaceholderScreen` with `HouseholdGate` without touching `AuthSession` or the auth-gate `when` (it already handles the `is Authenticated` branch).
## Self-Check: PASSED
- All listed created/modified files exist on disk.
- Commits `466e4c7`, `88f4898`, `570652c` exist in `git log`.
- Acceptance grep checks all pass (run inline above).
- `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileTestKotlinWasmJs` exits 0.
- Pre-existing failures unrelated to 02-07 are documented in `deferred-items.md` (verified via `git stash` reproduction on `master` HEAD).
---
*Phase: 02-authentication-foundation*
*Status: Tasks 1+2 complete; Task 3 (manual iOS Authentik UAT) awaiting user verification.*