@.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.
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
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`.