Files
recipe/.planning/phases/02-authentication-foundation/02-07-PLAN.md
2026-04-29 20:54:13 +02:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-authentication-foundation 07 execute 5
02-06
composeApp/src/commonMain/composeResources/values/strings.xml
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt
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
false
AUTH-01
AUTH-04
AUTH-05
truths artifacts key_links
Fresh launch in Loading shows SplashScreen with Recipe wordmark and progress indicator
Unauthenticated state shows LoginScreen with Polish Authentik sign-in button
Login errors render inline below the button and retry clears stale error
Authenticated state shows Witaj, {displayName}! and Wyloguj się
Wyloguj się returns to LoginScreen through AuthSession.logout()
All Phase 2 user-facing strings come from Compose Resources
path provides contains
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt Auth gate rendering Splash/Login/PostLogin by AuthState when
path provides contains
composeApp/src/commonMain/composeResources/values/strings.xml auth_app_name/auth_sign_in_button/auth_sign_out_button/auth_welcome_format/auth_error_* auth_sign_in_button
path provides contains
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt UI-SPEC login layout and inline error state auth_sign_in_button
from to via pattern
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt collectAsState over AuthSession.state collectAsState
from to via pattern
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt onSignOutClick delegates to logout logout
Deliver the user-facing Phase 2 auth experience and final validation gate.

Purpose: make end-to-end auth observable: login button, loading screen, welcome confirmation, logout button, and manual iOS Authentik UAT. Output: auth screens, auth gate, resource strings, UI ViewModels, and validation checklist execution.

<execution_context> @/Users/rwilk/.codex/get-shit-done/workflows/execute-plan.md @/Users/rwilk/.codex/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/REQUIREMENTS.md @.planning/ROADMAP.md @.planning/phases/02-authentication-foundation/02-CONTEXT.md @.planning/phases/02-authentication-foundation/02-UI-SPEC.md @.planning/phases/02-authentication-foundation/02-VALIDATION.md @.planning/phases/02-authentication-foundation/02-PATTERNS.md @.planning/phases/02-authentication-foundation/02-06-SUMMARY.md @AGENTS.md @composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt Task 1: Add Compose Resources, theme seed, and ViewModel tests - .planning/phases/02-authentication-foundation/02-UI-SPEC.md - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt composeApp/src/commonMain/composeResources/values/strings.xml, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt, composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt - String keys exist with exact Polish scaffold copy from UI-SPEC. - `RecipeTheme` uses Material 3 light/dark schemes with primary seed `#3B6939` / dark variant `#A2D597`. - LoginViewModel maps cancelled/network/unknown auth failures to the correct string resource keys. - Starting a new login clears previous inline error and sets loading. Create `strings.xml` keys: `auth_app_name`, `auth_sign_in_button`, `auth_sign_out_button`, `auth_welcome_format`, `auth_error_cancelled`, `auth_error_network`, `auth_error_unknown` with exact UI-SPEC copy.
Add `RecipeTheme(content)` with `lightColorScheme(primary = Color(0xFF3B6939))`, `darkColorScheme(primary = Color(0xFFA2D597))`, `isSystemInDarkTheme()`, and Material 3 typography defaults. Do not add Haze, blur, images, icons, Scaffold, or marketing copy.

Write `LoginViewModelTest` against a fake `AuthSession` result interface before implementing the ViewModel in Task 2.
./gradlew :composeApp:jvmTest --tests "*LoginViewModelTest*" - `grep -q 'name="auth_sign_in_button">Zaloguj się przez Authentik' composeApp/src/commonMain/composeResources/values/strings.xml` - `grep -q '0xFF3B6939' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` - `grep -q 'auth_error_cancelled' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModelTest.kt` - After Task 2, `./gradlew :composeApp:jvmTest --tests "*LoginViewModelTest*"` exits 0 Resource and theme foundations match UI-SPEC and login error mapping is tested. Task 2: Implement auth screens, ViewModels, and App auth gate - .planning/phases/02-authentication-foundation/02-UI-SPEC.md (Component Inventory, Layout Contract, Auth Gate Routing Contract) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.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/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt Replace template `App()` body with `RecipeTheme { val authState by authSession.state.collectAsState(); when(authState) { Loading -> SplashScreen(); Unauthenticated -> LoginScreen(koinViewModel()); Authenticated -> PostLoginPlaceholderScreen(user, koinViewModel()) } }`. State changes drive recomposition; no manual navigation or Scaffold.
Implement `SplashScreen`, `LoginScreen`, and `PostLoginPlaceholderScreen` exactly from UI-SPEC: centered column, `safeContentPadding`, horizontal 16.dp, displaySmall wordmark, Login button with loading indicator, inline bodyLarge error text below button, welcome `headlineSmall`, logout `OutlinedButton`. All strings must use `stringResource(Res.string.*)`.

Implement `LoginViewModel` with method `onSignInClick()` and immutable `LoginScreenState(isLoading: Boolean, errorKey: StringResource?)`. Implement `PostLoginViewModel.onSignOutClick()` delegating to `AuthSession.logout()`. Register ViewModels in `authModule` using existing Koin Compose ViewModel pattern.
./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 - `! grep -R 'Click me!' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` - `grep -q 'collectAsState' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` - `grep -q 'SplashScreen' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` - `grep -q 'auth_welcome_format' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt` - `! grep -R 'Zaloguj\\|Wyloguj\\|Witaj\\|Nie można\\|Coś poszło' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe --include='*.kt'` - `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64` exits 0 Auth gate UI compiles, uses resources, and has no dangling reference to a missing plan. Task 3: Manual iOS Authentik UAT - docs/authentik-setup.md - .planning/phases/02-authentication-foundation/02-VALIDATION.md (Manual-Only Verifications) docs/authentik-setup.md, .planning/phases/02-authentication-foundation/02-07-SUMMARY.md Run automated gate first: `./gradlew check`.
Then perform the manual UAT from `docs/authentik-setup.md` on iOS simulator/device with the real Authentik provider:
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 shortened 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 and 401 without token or with wrong-audience token.
./gradlew check - `./gradlew check` exits 0 - Manual UAT result recorded in `.planning/phases/02-authentication-foundation/02-07-SUMMARY.md` - If any UAT step fails, record exact step, observed behavior, logs with tokens redacted, and do not mark Phase 2 complete Phase 2 end-to-end auth flow: Authentik login, secure token persistence, server /me, and logout UI. Follow the six UAT steps in the action block using the real Authentik provider configured from docs/authentik-setup.md. Type "approved" if UAT passes, or describe the failing step and observed behavior. Automated tests are green and the user confirms fresh login, persisted session refresh, logout, and /api/v1/me behavior.

<threat_model>

Trust Boundaries

Boundary Description
UI -> AuthSession User taps login/logout and triggers token-bearing flows
AuthSession -> UI Auth errors are mapped to user-visible strings
Human UAT -> logs Manual validation may inspect logs while tokens exist

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-07-01 Information Disclosure UI/log validation mitigate UAT summary must redact tokens and Authorization headers
T-02-07-02 Information Disclosure logout UX mitigate Logout button delegates to AuthSession.logout; UAT verifies no silent restore after relaunch
T-02-07-03 Spoofing login UX mitigate Button explicitly opens Authentik; AppAuth handles browser flow and callback
T-02-07-04 Denial of Service refresh UX mitigate Reopen-after-expiry UAT verifies transparent refresh path
T-02-07-05 Tampering raw strings mitigate All auth copy comes from Compose Resources, preventing ad hoc UI string drift
</threat_model>
Run `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64`, then `./gradlew check`, then complete iOS Authentik UAT.

<success_criteria> The app visibly satisfies Phase 2 roadmap criteria: sign in, stay signed in, sign out, and prove server /api/v1/me works with valid/invalid tokens. </success_criteria>

After completion, create `.planning/phases/02-authentication-foundation/02-07-SUMMARY.md`.