diff --git a/.gitignore b/.gitignore index adfa9bf..ce6213a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings node_modules/ + +# Generated by Kotlin CocoaPods plugin (Phase 2 D-01); regenerated on every Gradle sync. +*.podspec diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d5bc908..fd5b3ca 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -6,9 +6,21 @@ plugins { alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.composeHotReload) + alias(libs.plugins.kotlinSerialization) + // CocoaPods is shipped inside the Kotlin Gradle plugin already on the classpath via + // `recipe.kotlin.multiplatform`. Applying via `alias(libs.plugins.kotlinCocoapods)` + // would request a fresh version and fail with "already on the classpath", so we + // apply it by id only. The catalog still owns the shared Kotlin version. + id("org.jetbrains.kotlin.native.cocoapods") id("recipe.quality") } +// Top-level project version is required by the Kotlin CocoaPods plugin when no explicit +// `version` is set inside the `cocoapods { ... }` block. Mirrors `server/build.gradle.kts` +// — Gradle artifact metadata only, NOT a library/plugin pin (per `verify-no-version-literals.sh`). +group = "dev.ulfrx.recipe" +version = "1.0.0" + android { namespace = "dev.ulfrx.recipe" compileSdk = libs.versions.android.compileSdk.get().toInt() @@ -19,6 +31,14 @@ android { targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 1 versionName = "1.0" + + // AppAuth-Android (D-01) bundles a manifest entry for its + // `RedirectUriReceiverActivity` that requires `${appAuthRedirectScheme}` to be + // resolved at merge time. Pin it to the Phase 2 redirect scheme so simply + // pulling AppAuth into the classpath (Plan 02-01) doesn't break AGP's manifest + // merger before Plan 02-04 lands the full `` registration. + // Must match `dev.ulfrx.recipe.shared.Constants.OIDC_REDIRECT_URI` byte-for-byte. + manifestPlaceholders["appAuthRedirectScheme"] = "recipe" } packaging { resources { @@ -37,12 +57,22 @@ android { } kotlin { - // Create the iOS framework Swift imports as `ComposeApp`. - listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> - iosTarget.binaries.framework { + // The Kotlin CocoaPods plugin (D-01) configures the iOS framework on the iOS targets + // declared by `recipe.kotlin.multiplatform`. `baseName = "ComposeApp"` / `isStatic = true` + // keep existing Swift `import ComposeApp` working. The AppAuth iOS pod version comes + // from the version catalog so this build file stays free of literal pins. + cocoapods { + summary = "Recipe Compose Multiplatform shared framework" + homepage = "https://github.com/ulfrxdev/recipe" + ios.deploymentTarget = "15.0" + podfile = project.file("../iosApp/Podfile") + framework { baseName = "ComposeApp" isStatic = true } + pod("AppAuth") { + version = libs.versions.appauth.ios.get() + } } sourceSets { @@ -61,15 +91,45 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(projects.shared) + + // Phase 2: Ktor client + serialization + secure settings (D-13, D-16, D-17). + // The MPP variant of `ktor-serialization-kotlinx-json` is required here; the + // server module keeps the `-jvm` variant via `libs.ktor.serializationKotlinxJson`. + implementation(libs.ktor.clientCore) + implementation(libs.ktor.clientAuth) + implementation(libs.ktor.clientContentNegotiation) + implementation(libs.ktor.clientLogging) + implementation(libs.ktor.serializationKotlinxJsonMpp) + implementation(libs.kotlinx.serializationJson) + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.coroutines) } androidMain.dependencies { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.activity.compose) implementation(libs.koin.android) + + // Phase 2 Android: AppAuth-Android + AndroidX Security Crypto for the + // SecureAuthStateStore actual (D-01, D-13). EncryptedSharedPreferences is + // accepted technical debt per Open Question #1; the Keystore-backed + // implementation can replace it without touching AuthSession. + implementation(libs.appauth) + implementation(libs.androidx.security.crypto) + implementation(libs.ktor.clientOkhttp) + } + iosMain.dependencies { + // Phase 2 iOS: Darwin engine for Ktor; AppAuth-iOS arrives via the + // CocoaPods block above so the shared framework links it directly. + implementation(libs.ktor.clientDarwin) } jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) + + // Phase 2 Desktop: CIO is the JVM Ktor engine for the dev-mode auth stub + // (D-02). The full stub lives in Plan 02-04; this just makes the engine + // available so `composeApp:run` still compiles in Phase 2. + implementation(libs.ktor.clientCio) } } } @@ -77,3 +137,12 @@ kotlin { dependencies { debugImplementation(libs.compose.uiTooling) } + +// Adding `group = "dev.ulfrx.recipe"` (required by the Kotlin CocoaPods plugin to render +// the podspec) shifts the Compose Resources `Res` class package from +// `recipe.composeapp.generated.resources` to `dev.ulfrx.recipe.composeapp.generated.resources`, +// breaking the Phase 1 `App.kt` import. Lock the historical package so this plan's wiring +// changes don't cascade into UI code; Plan 02-04+ replaces `App.kt`'s template body anyway. +compose.resources { + packageOfResClass = "recipe.composeapp.generated.resources" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8435f0..3387a19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,10 +8,15 @@ androidx-appcompat = "1.7.1" androidx-core = "1.18.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.10.0" +androidx-security-crypto = "1.1.0" androidx-testExt = "1.3.0" +appauth = "0.11.1" +appauth-ios = "2.0.0" composeHotReload = "1.0.0" composeMultiplatform = "1.10.3" +exposed = "0.55.0" flyway = "12.4.0" +hikari = "6.2.1" junit = "4.13.2" kermit = "2.1.0" koin = "4.2.1" @@ -21,8 +26,10 @@ kotlinx-serialization = "1.7.3" ktor = "3.4.1" logback = "1.5.32" material3 = "1.10.0-alpha05" +multiplatformSettings = "1.3.0" postgresql = "42.7.10" spotless = "8.4.0" +testcontainers = "1.21.4" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -68,6 +75,40 @@ flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +# Phase 2 — Server: Ktor auth + JWT + call logging + status pages (D-21..D-23) +ktor-serverAuth = { module = "io.ktor:ktor-server-auth-jvm", version.ref = "ktor" } +ktor-serverAuthJwt = { module = "io.ktor:ktor-server-auth-jwt-jvm", version.ref = "ktor" } +ktor-serverCallLogging = { module = "io.ktor:ktor-server-call-logging-jvm", version.ref = "ktor" } +ktor-serverStatusPages = { module = "io.ktor:ktor-server-status-pages-jvm", version.ref = "ktor" } + +# Phase 2 — Client: Ktor client core + auth + content-negotiation + logging + engines (D-16..D-18) +# `ktor-serializationKotlinxJsonMpp` is the multiplatform variant (no `-jvm` classifier) for +# commonMain consumption; the `-jvm` variant above stays available to the server module. +ktor-clientCore = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-clientAuth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } +ktor-clientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-clientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-clientOkhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-clientDarwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-clientCio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-serializationKotlinxJsonMpp = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } + +# Phase 2 — Client: AppAuth + Android secure storage + multiplatform-settings (D-01, D-13, AUTH-02) +appauth = { module = "net.openid:appauth", version.ref = "appauth" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidx-security-crypto" } +multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } +multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } + +# Phase 2 — Server: Exposed DSL + Hikari (D-26) +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } +hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } + +# Phase 2 — Server tests: Testcontainers (D-21..D-25) +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } +testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -78,5 +119,6 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } flywayPlugin = { id = "org.flywaydb.flyway", version.ref = "flyway" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index bd093f9..a941470 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -29,13 +29,32 @@ dependencies { implementation(libs.ktor.serverNetty) implementation(libs.ktor.serverContentNegotiation) implementation(libs.ktor.serializationKotlinxJson) + implementation(libs.kotlinx.serializationJson) implementation(libs.logback) implementation(libs.flyway.core) implementation(libs.flyway.database.postgresql) implementation(libs.postgresql) implementation(projects.shared) + + // Phase 2: Ktor auth + JWT validation + observability (D-21..D-23). + implementation(libs.ktor.serverAuth) + implementation(libs.ktor.serverAuthJwt) + implementation(libs.ktor.serverCallLogging) + implementation(libs.ktor.serverStatusPages) + + // Phase 2: Exposed DSL + Hikari connection pool (D-26). + implementation(libs.exposed.core) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.java.time) + implementation(libs.hikari) + testImplementation(libs.ktor.serverTestHost) testImplementation(libs.kotlin.testJunit) + + // Phase 2: Testcontainers for JIT user provisioning + JWT auth integration tests + // (AUTH-03, AUTH-06). Wired here so Plan 02-02 only needs to write tests. + testImplementation(libs.testcontainers.postgresql) + testImplementation(libs.testcontainers.junit.jupiter) } flyway {