feat(02-01): land Constants and MeResponse/User DTOs in shared
GREEN phase of TDD task 02-01-01. Adds the load-bearing Phase 2 contract that downstream plans compile against: - Constants.kt: OIDC_ISSUER (trailing slash, placeholder homelab host), OIDC_CLIENT_ID = recipe-app, OIDC_REDIRECT_URI = recipe://callback, API_BASE_URL, plus moved SERVER_PORT for one shared config object. - dto/User.kt: domain identity (id/sub/email/displayName), id is String to keep shared free of UUID library deps (D-19 / INFRA-06). - dto/MeResponse.kt: @Serializable wire DTO for GET /api/v1/me with a one-to-one toUser() mapper. Stable for Phase 3 to add householdId via ignoreUnknownKeys. - Removes the now-redundant shared/.gitkeep placeholder. Verification: - ./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata: PASS - ./tools/verify-shared-pure.sh: PASS - All grep acceptance criteria for Task 1 satisfied
This commit is contained in:
@@ -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 `<intent-filter>` 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 `<intent-filter>` 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"
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user