diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/.gitkeep b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt new file mode 100644 index 0000000..8e0af28 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt @@ -0,0 +1,52 @@ +package dev.ulfrx.recipe.shared + +/** + * Phase 2 OIDC + API configuration shared by client and server (D-11, D-12). + * + * Hardcoded for the v1 single-environment per `PITFALLS.md` tech-debt acceptance: + * the homelab Authentik is the only target, so `BuildConfig`-style Gradle injection + * is deliberately deferred. The placeholder issuer host (`auth.example.invalid`) is + * substituted at deploy time by overriding the build at the call site, never by + * mutating this file in user installs. + * + * **Invariants the rest of Phase 2 depends on:** + * - `OIDC_ISSUER` ends with `/` so JWKS / authorization endpoints can append paths + * without double-slash bugs (PITFALLS.md #8 — Authentik is byte-sensitive here). + * - `OIDC_REDIRECT_URI` is exactly `recipe://callback` — both `iosApp/Info.plist` + * `CFBundleURLTypes` and the Android `` registration must match + * byte-for-byte (D-09). + * - `OIDC_CLIENT_ID` doubles as the JWT `aud` claim value (D-07: single string, + * not array). Server-side `withAudience(OIDC_CLIENT_ID)` validates against it. + */ +public object Constants { + /** Reserved by `composeApp/server` for the local Ktor port (Phase 1). */ + public const val SERVER_PORT: Int = 8080 + + /** + * Base URL the client uses for `/api/v1/...` calls. v1 single environment; + * staging support is deferred per PITFALLS.md tech-debt acceptance. + */ + public const val API_BASE_URL: String = "http://localhost:8080/" + + /** + * Authentik OIDC issuer. Trailing slash is required (D-11, PITFALLS.md #8). + * Replace `auth.example.invalid` with the homelab Authentik hostname before + * shipping a build; the placeholder keeps tests/CI deterministic without + * leaking real infrastructure into the repo. + */ + public const val OIDC_ISSUER: String = "https://auth.example.invalid/application/o/recipe/" + + /** + * OAuth2 client_id registered with Authentik. The same value MUST appear as + * the single-string `aud` claim per D-07. + */ + public const val OIDC_CLIENT_ID: String = "recipe-app" + + /** + * Custom URL scheme for the OAuth redirect (D-09). Must match `Info.plist` + * `CFBundleURLTypes` on iOS and the Android manifest `` byte + * for byte. PKCE S256 + AppAuth state (D-05) make custom-scheme interception + * non-exploitable; Universal Links / App Links are explicitly deferred. + */ + public const val OIDC_REDIRECT_URI: String = "recipe://callback" +} diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt new file mode 100644 index 0000000..fa79c94 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt @@ -0,0 +1,33 @@ +package dev.ulfrx.recipe.shared.dto + +import kotlinx.serialization.Serializable + +/** + * Wire-format DTO for `GET /api/v1/me` (D-27). + * + * Mirrors [User] verbatim today; kept as a separate type so Phase 3 can extend the + * response with `householdId` (and any future fields) without changing the client + * domain model. Decoders MUST be configured with `ignoreUnknownKeys = true` so a + * Phase 2 client keeps parsing Phase 3+ responses (see + * `MeResponseSerializationTest.decoder ignores future fields like householdId without failing`). + * + * Wire keys are camelCase to stay consistent with kotlinx.serialization defaults; + * `sub` is left raw because it's an opaque OIDC claim and renaming it on the wire + * is a recipe for drift. + */ +@Serializable +public data class MeResponse( + public val id: String, + public val sub: String, + public val email: String, + public val displayName: String, +) { + /** One-to-one mapping to the client domain [User]; no fallback / coercion. */ + public fun toUser(): User = + User( + id = id, + sub = sub, + email = email, + displayName = displayName, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt new file mode 100644 index 0000000..ebebcab --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt @@ -0,0 +1,26 @@ +package dev.ulfrx.recipe.shared.dto + +import kotlinx.serialization.Serializable + +/** + * Authenticated user identity shared by client and server (D-25, D-27). + * + * Phase 2 always emits `User` in the [dev.ulfrx.recipe.shared.dto.MeResponse] payload; + * Phase 3 will extend the response with a household lookup but `User` itself stays + * stable. `id` is a server-issued UUID serialized as a string so the shared module + * stays free of Exposed / SQLDelight / kotlin.uuid dependencies (D-19 / INFRA-06). + * + * Field semantics: + * - [id] — server primary key (`users.id` UUID per D-24). + * - [sub] — opaque OIDC subject claim from Authentik; the only stable identity key + * for JIT provisioning per D-25. + * - [email] — most recent `email` claim. May change between logins (D-25 upserts). + * - [displayName] — most recent `name` / `preferred_username` claim (D-25). + */ +@Serializable +public data class User( + public val id: String, + public val sub: String, + public val email: String, + public val displayName: String, +)