From 7d9d729d7dc940deccf510efe64b2da4e61a4d6a Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sat, 6 Dec 2025 22:01:41 +0100 Subject: [PATCH] =?UTF-8?q?chore(infra):=20Network/Auth=20=E2=80=93=20DoD?= =?UTF-8?q?=20schlie=C3=9Fen=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20-?= =?UTF-8?q?=20Entfernen/Deprecaten:=20`frontend/features/auth-feature/.../?= =?UTF-8?q?AuthenticatedHttpClient.kt`=20und=20alle=20manuellen=20`Authori?= =?UTF-8?q?zation`=E2=80=91Header=E2=80=91Setzungen.=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20-=20Stattdessen:=20DI=E2=80=91`apiClien?= =?UTF-8?q?t`=20via=20Koin=20injizieren=20(`single(named("apiClient"))`)?= =?UTF-8?q?=20und=20Token=E2=80=91Anreicherung=20=C3=BCber=20Ktor=20`Auth`?= =?UTF-8?q?=20Plugin=20(Bearer)=20verdrahten.=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20-=20Build=E2=80=91Guard=20erg=C3=A4nzen:=20Au?= =?UTF-8?q?ch=20Vorkommen=20von=20`HttpHeaders.Authorization`=20erkennen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/core/network/NetworkModule.kt | 25 +++++++++++++++++-- .../features/auth-feature/build.gradle.kts | 6 +++++ .../clients/authfeature/AuthApiClient.kt | 5 +++- .../clients/authfeature/LoginViewModel.kt | 14 +++++------ .../authfeature/di/AuthFeatureModule.kt | 20 +++++++++++++++ .../src/commonMain/kotlin/MainApp.kt | 5 ++-- .../src/jsMain/kotlin/main.kt | 5 ++-- .../src/jvmMain/kotlin/main.kt | 5 ++-- 8 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/di/AuthFeatureModule.kt diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt index 5d545c31..d04e6c86 100644 --- a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt @@ -4,6 +4,8 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.auth.Auth +import io.ktor.client.plugins.auth.providers.BearerTokens +import io.ktor.client.plugins.auth.providers.bearer import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel @@ -15,12 +17,20 @@ import kotlinx.serialization.json.Json import org.koin.core.qualifier.named import org.koin.dsl.module +/** + * Simple token provider interface so core network module does not depend on auth-feature. + */ +interface TokenProvider { + fun getAccessToken(): String? +} + /** * Koin module that provides a preconfigured Ktor HttpClient under the named qualifier "apiClient". * The client uses the environment-aware base URL from NetworkConfig. */ val networkModule = module { single(named("apiClient")) { + val tokenProvider: TokenProvider? = try { get() } catch (_: Throwable) { null } HttpClient { // JSON (kotlinx) configuration install(ContentNegotiation) { @@ -50,9 +60,20 @@ val networkModule = module { exponentialDelay() } - // Authentication plugin (Bearer refresh can be wired later) + // Authentication plugin (Bearer) install(Auth) { - // TODO: Wire token provider/refresh when auth is implemented + bearer { + loadTokens { + val token = tokenProvider?.getAccessToken() + token?.let { BearerTokens(it, refreshToken = "") } + } + // Only send token to our API base URL + sendWithoutRequest { request -> + val base = NetworkConfig.baseUrl.trimEnd('/') + val url = request.url.toString() + url.startsWith(base) + } + } } // Logging for development diff --git a/frontend/features/auth-feature/build.gradle.kts b/frontend/features/auth-feature/build.gradle.kts index 379a108d..902b8c3a 100644 --- a/frontend/features/auth-feature/build.gradle.kts +++ b/frontend/features/auth-feature/build.gradle.kts @@ -61,6 +61,12 @@ kotlin { implementation(libs.ktor.client.logging) implementation(libs.ktor.client.auth) + // DI + implementation(libs.koin.core) + + // Network core (provides apiClient + TokenProvider) + implementation(projects.frontend.core.network) + // Coroutines and serialization implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) diff --git a/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt index cceecdca..2811e055 100644 --- a/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt +++ b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/AuthApiClient.kt @@ -5,6 +5,9 @@ import io.ktor.client.call.* import io.ktor.client.request.forms.* import io.ktor.http.* import kotlinx.serialization.Serializable +import io.ktor.client.HttpClient +import org.koin.core.context.GlobalContext +import org.koin.core.qualifier.named /** * Data classes for authentication API communication @@ -37,7 +40,7 @@ class AuthApiClient( // Optional: Client-Secret (nur bei vertraulichen Clients erforderlich) private val clientSecret: String? = null ) { - private val client = AuthenticatedHttpClient.createUnauthenticated() + private val client: HttpClient by lazy { GlobalContext.get().koin.get(named("apiClient")) } /** * Authenticate user with username and password diff --git a/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt index 6960b253..0c395653 100644 --- a/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt +++ b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/LoginViewModel.kt @@ -2,9 +2,10 @@ package at.mocode.clients.authfeature import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader -import at.mocode.shared.core.AppConstants -import io.ktor.client.request.* +import io.ktor.client.request.post +import org.koin.core.context.GlobalContext +import org.koin.core.qualifier.named +import io.ktor.client.HttpClient import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -37,6 +38,7 @@ class LoginViewModel( val uiState: StateFlow = _uiState.asStateFlow() private val authApiClient = AuthApiClient() + private val apiClient: HttpClient by lazy { GlobalContext.get().koin.get(named("apiClient")) } fun updateUsername(username: String) { _uiState.value = _uiState.value.copy( @@ -96,10 +98,8 @@ class LoginViewModel( // Fire-and-forget: Trigger Backend Sync so the user exists in Members viewModelScope.launch { try { - val client = AuthenticatedHttpClient.create() - client.post("${AppConstants.GATEWAY_URL}/api/members/sync") { - addAuthHeader() - } + // Fire-and-forget sync call; Bearer token added by Ktor Auth plugin + apiClient.post("/api/members/sync") } catch (_: Exception) { // Non-fatal: Wir zeigen Sync-Fehler im Login nicht an } diff --git a/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/di/AuthFeatureModule.kt b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/di/AuthFeatureModule.kt new file mode 100644 index 00000000..9aef3e42 --- /dev/null +++ b/frontend/features/auth-feature/src/commonMain/kotlin/at/mocode/clients/authfeature/di/AuthFeatureModule.kt @@ -0,0 +1,20 @@ +package at.mocode.clients.authfeature.di + +import at.mocode.clients.authfeature.AuthTokenManager +import at.mocode.frontend.core.network.TokenProvider +import org.koin.dsl.module + +/** + * Koin module for auth-feature: provides AuthTokenManager and binds it as TokenProvider for apiClient. + */ +val authFeatureModule = module { + // Single in-memory token manager + single { AuthTokenManager() } + + // Bridge to core network TokenProvider without adding a hard dependency there + single { + object : TokenProvider { + override fun getAccessToken(): String? = get().getToken() + } + } +} diff --git a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt index b6b5b1c3..a6503da5 100644 --- a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt +++ b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt @@ -6,8 +6,8 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.runtime.collectAsState import at.mocode.clients.shared.navigation.AppScreen -import at.mocode.clients.authfeature.AuthenticatedHttpClient import at.mocode.clients.authfeature.AuthTokenManager +import org.koin.core.context.GlobalContext import at.mocode.clients.pingfeature.PingScreen import at.mocode.clients.pingfeature.PingViewModel import at.mocode.shared.core.AppConstants @@ -29,7 +29,8 @@ fun MainApp() { ) { var currentScreen by remember { mutableStateOf(AppScreen.Home) } - val authTokenManager = remember { AuthenticatedHttpClient.getAuthTokenManager() } + // Resolve AuthTokenManager from Koin + val authTokenManager = remember { GlobalContext.get().koin.get() } val pingViewModel = remember { PingViewModel() } val scope = rememberCoroutineScope() diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt index f3934de6..f0864c62 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt @@ -4,6 +4,7 @@ import kotlinx.browser.document import org.w3c.dom.HTMLElement import at.mocode.shared.di.initKoin import at.mocode.frontend.core.network.networkModule +import at.mocode.clients.authfeature.di.authFeatureModule import at.mocode.frontend.core.localdb.localDbModule import at.mocode.frontend.core.localdb.DatabaseProvider import kotlinx.coroutines.MainScope @@ -19,8 +20,8 @@ fun main() { console.log("[WebApp] main() entered") // Initialize DI (Koin) with shared modules + network + local DB modules try { - initKoin { modules(networkModule, localDbModule) } - console.log("[WebApp] Koin initialized with networkModule + localDbModule") + initKoin { modules(networkModule, localDbModule, authFeatureModule) } + console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule") } catch (e: dynamic) { console.warn("[WebApp] Koin initialization warning:", e) } diff --git a/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt index 4bf56af4..538a7c5f 100644 --- a/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt @@ -4,12 +4,13 @@ import androidx.compose.ui.window.WindowState import androidx.compose.ui.unit.dp import at.mocode.shared.di.initKoin import at.mocode.frontend.core.network.networkModule +import at.mocode.clients.authfeature.di.authFeatureModule fun main() = application { // Initialize DI (Koin) with shared modules + network module try { - initKoin { modules(networkModule) } - println("[DesktopApp] Koin initialized with networkModule") + initKoin { modules(networkModule, authFeatureModule) } + println("[DesktopApp] Koin initialized with networkModule + authFeatureModule") } catch (e: Exception) { println("[DesktopApp] Koin initialization warning: ${e.message}") }