diff --git a/backend/infrastructure/gateway/Dockerfile b/backend/infrastructure/gateway/Dockerfile index 64386183..3992c250 100644 --- a/backend/infrastructure/gateway/Dockerfile +++ b/backend/infrastructure/gateway/Dockerfile @@ -3,7 +3,7 @@ # =================================================================== # Multi-stage Dockerfile for Meldestelle API Gateway # Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts -# Version: 2.2.1 - Optimized for Monorepo (Fixed missing frontend dirs) +# Version: 2.2.2 - Optimized for Monorepo (Fixed frontend paths after refactoring) # =================================================================== # === CENTRALIZED BUILD ARGUMENTS === @@ -55,13 +55,13 @@ COPY contracts/ contracts/ # Create dummy frontend directories to satisfy settings.gradle.kts include paths # This prevents Gradle from failing configuration phase without copying actual frontend code RUN mkdir -p \ + frontend/core/auth \ frontend/core/domain \ frontend/core/design-system \ frontend/core/navigation \ frontend/core/network \ frontend/core/local-db \ frontend/core/sync \ - frontend/features/auth-feature \ frontend/features/ping-feature \ frontend/shared \ frontend/shells/meldestelle-portal \ diff --git a/backend/services/ping/Dockerfile b/backend/services/ping/Dockerfile index af0ecc47..4c06f99e 100644 --- a/backend/services/ping/Dockerfile +++ b/backend/services/ping/Dockerfile @@ -3,7 +3,7 @@ # =================================================================== # Multi-stage Dockerfile for Meldestelle Ping Service # Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts -# Version: 2.2.0 - Optimized for Monorepo (Fixed missing frontend dirs) +# Version: 2.2.1 - Optimized for Monorepo (Fixed frontend paths after refactoring) # =================================================================== # === CENTRALIZED BUILD ARGUMENTS === @@ -53,13 +53,13 @@ COPY contracts/ contracts/ # Create dummy frontend directories to satisfy settings.gradle.kts include paths RUN mkdir -p \ + frontend/core/auth \ frontend/core/domain \ frontend/core/design-system \ frontend/core/navigation \ frontend/core/network \ frontend/core/local-db \ frontend/core/sync \ - frontend/features/auth-feature \ frontend/features/ping-feature \ frontend/shared \ frontend/shells/meldestelle-portal \ diff --git a/config/docker/keycloak/meldestelle-realm.json b/config/docker/keycloak/meldestelle-realm.json index 1d8ad6ce..4642df65 100644 --- a/config/docker/keycloak/meldestelle-realm.json +++ b/config/docker/keycloak/meldestelle-realm.json @@ -238,7 +238,7 @@ { "type": "password", "value": "Change_Me_In_Production!", - "temporary": true + "temporary": false } ], "realmRoles": [ diff --git a/core/README.md b/core/README.md index 55253a24..4a210a5e 100644 --- a/core/README.md +++ b/core/README.md @@ -3,4 +3,4 @@ Dieses Modul enthält projektübergreifende Kern-Logik und Utility-Klassen, die sowohl vom Backend als auch vom Frontend genutzt werden können. **Die vollständige Dokumentation befindet sich hier:** -[**-> docs/03_Domain/01_Core_Model/README.md**](../docs/03_Domain/01_Core_Model/README.md) +[**→ docs/03_Domain/01_Core_Model/README.md**](../docs/03_Domain/01_Core_Model/README.md) diff --git a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/data/AuthApiClient.kt b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/data/AuthApiClient.kt index 9011821b..35aff3ab 100644 --- a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/data/AuthApiClient.kt +++ b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/data/AuthApiClient.kt @@ -4,6 +4,7 @@ import at.mocode.shared.core.AppConstants import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.forms.* +import io.ktor.client.statement.* import io.ktor.http.* import kotlinx.serialization.Serializable @@ -63,9 +64,10 @@ class AuthApiClient( username = username ) } else { + val errorBody = response.bodyAsText() LoginResponse( success = false, - message = "Login fehlgeschlagen: HTTP ${response.status.value}" + message = "Login fehlgeschlagen: HTTP ${response.status.value} - $errorBody" ) } } catch (e: Exception) { @@ -104,9 +106,10 @@ class AuthApiClient( message = null ) } else { + val errorBody = response.bodyAsText() LoginResponse( success = false, - message = "Token refresh fehlgeschlagen: HTTP ${response.status.value}" + message = "Token refresh fehlgeschlagen: HTTP ${response.status.value} - $errorBody" ) } } catch (e: Exception) { diff --git a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/di/AuthModule.kt b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/di/AuthModule.kt index ac175970..447745db 100644 --- a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/di/AuthModule.kt +++ b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/di/AuthModule.kt @@ -15,10 +15,10 @@ val authModule = module { // Single in-memory token manager single { AuthTokenManager() } - // AuthApiClient with injected apiClient and DEV client secret + // AuthApiClient with injected baseHttpClient (NOT apiClient) single { AuthApiClient( - httpClient = get(named("apiClient")), + httpClient = get(named("baseHttpClient")), clientSecret = AppConstants.KEYCLOAK_CLIENT_SECRET ) } @@ -30,8 +30,7 @@ val authModule = module { single { object : TokenProvider { override fun getAccessToken(): String? { - val token = get().getToken() - return token + return get().getToken() } } } 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 68880308..e82b03b9 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 @@ -2,10 +2,9 @@ package at.mocode.frontend.core.network import io.ktor.client.* import io.ktor.client.plugins.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json import org.koin.core.qualifier.named @@ -19,10 +18,28 @@ interface TokenProvider { } /** - * Koin module that provides a preconfigured Ktor HttpClient under the named qualifier "apiClient". - * The client uses the environment-aware base URL from NetworkConfig. + * Koin module providing HttpClients. */ val networkModule = module { + + // 1. Base Client (Raw, for Auth/Keycloak) + single(named("baseHttpClient")) { + HttpClient { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true; isLenient = true; encodeDefaults = true }) + } + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + println("[baseClient] $message") + } + } + level = LogLevel.INFO + } + } + } + + // 2. API Client (Configured for Gateway & Auth Header) single(named("apiClient")) { val tokenProvider: TokenProvider? = try { get() } catch (_: Throwable) { null } HttpClient { @@ -54,19 +71,15 @@ val networkModule = module { exponentialDelay() } - // Authentication plugin (Bearer) - install(Auth) { - 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) - } + // Manual Auth Header Injection (instead of Auth plugin) to support immediate logout + install(DefaultRequest) { + val base = NetworkConfig.baseUrl.trimEnd('/') + url(base) // Set base URL + + // Inject Authorization header if token is present + val token = tokenProvider?.getAccessToken() + if (token != null) { + header("Authorization", "Bearer $token") } } @@ -79,12 +92,6 @@ val networkModule = module { } level = LogLevel.INFO } - - // Set base URL - defaultRequest { - // Set only the base URL; endpoints will append paths - url(NetworkConfig.baseUrl) - } } } }