refactor: update Dockerfile paths, network module auth flow, and Keycloak config

Updated Dockerfiles to fix frontend path references after refactoring. Refactored `networkModule` to replace the `Auth` plugin with manual auth header injection for enhanced logout support. Adjusted Keycloak realm configuration to set default credentials as non-temporary. Improved error handling in `AuthApiClient` with detailed response messages.
This commit is contained in:
2026-01-23 15:42:07 +01:00
parent a3ac6a52be
commit 48ee074dbd
7 changed files with 44 additions and 35 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
# =================================================================== # ===================================================================
# Multi-stage Dockerfile for Meldestelle API Gateway # Multi-stage Dockerfile for Meldestelle API Gateway
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts # 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 === # === CENTRALIZED BUILD ARGUMENTS ===
@@ -55,13 +55,13 @@ COPY contracts/ contracts/
# Create dummy frontend directories to satisfy settings.gradle.kts include paths # Create dummy frontend directories to satisfy settings.gradle.kts include paths
# This prevents Gradle from failing configuration phase without copying actual frontend code # This prevents Gradle from failing configuration phase without copying actual frontend code
RUN mkdir -p \ RUN mkdir -p \
frontend/core/auth \
frontend/core/domain \ frontend/core/domain \
frontend/core/design-system \ frontend/core/design-system \
frontend/core/navigation \ frontend/core/navigation \
frontend/core/network \ frontend/core/network \
frontend/core/local-db \ frontend/core/local-db \
frontend/core/sync \ frontend/core/sync \
frontend/features/auth-feature \
frontend/features/ping-feature \ frontend/features/ping-feature \
frontend/shared \ frontend/shared \
frontend/shells/meldestelle-portal \ frontend/shells/meldestelle-portal \
+2 -2
View File
@@ -3,7 +3,7 @@
# =================================================================== # ===================================================================
# Multi-stage Dockerfile for Meldestelle Ping Service # Multi-stage Dockerfile for Meldestelle Ping Service
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts # 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 === # === CENTRALIZED BUILD ARGUMENTS ===
@@ -53,13 +53,13 @@ COPY contracts/ contracts/
# Create dummy frontend directories to satisfy settings.gradle.kts include paths # Create dummy frontend directories to satisfy settings.gradle.kts include paths
RUN mkdir -p \ RUN mkdir -p \
frontend/core/auth \
frontend/core/domain \ frontend/core/domain \
frontend/core/design-system \ frontend/core/design-system \
frontend/core/navigation \ frontend/core/navigation \
frontend/core/network \ frontend/core/network \
frontend/core/local-db \ frontend/core/local-db \
frontend/core/sync \ frontend/core/sync \
frontend/features/auth-feature \
frontend/features/ping-feature \ frontend/features/ping-feature \
frontend/shared \ frontend/shared \
frontend/shells/meldestelle-portal \ frontend/shells/meldestelle-portal \
@@ -238,7 +238,7 @@
{ {
"type": "password", "type": "password",
"value": "Change_Me_In_Production!", "value": "Change_Me_In_Production!",
"temporary": true "temporary": false
} }
], ],
"realmRoles": [ "realmRoles": [
+1 -1
View File
@@ -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. 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:** **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)
@@ -4,6 +4,7 @@ import at.mocode.shared.core.AppConstants
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -63,9 +64,10 @@ class AuthApiClient(
username = username username = username
) )
} else { } else {
val errorBody = response.bodyAsText()
LoginResponse( LoginResponse(
success = false, success = false,
message = "Login fehlgeschlagen: HTTP ${response.status.value}" message = "Login fehlgeschlagen: HTTP ${response.status.value} - $errorBody"
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -104,9 +106,10 @@ class AuthApiClient(
message = null message = null
) )
} else { } else {
val errorBody = response.bodyAsText()
LoginResponse( LoginResponse(
success = false, success = false,
message = "Token refresh fehlgeschlagen: HTTP ${response.status.value}" message = "Token refresh fehlgeschlagen: HTTP ${response.status.value} - $errorBody"
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -15,10 +15,10 @@ val authModule = module {
// Single in-memory token manager // Single in-memory token manager
single { AuthTokenManager() } single { AuthTokenManager() }
// AuthApiClient with injected apiClient and DEV client secret // AuthApiClient with injected baseHttpClient (NOT apiClient)
single { single {
AuthApiClient( AuthApiClient(
httpClient = get(named("apiClient")), httpClient = get(named("baseHttpClient")),
clientSecret = AppConstants.KEYCLOAK_CLIENT_SECRET clientSecret = AppConstants.KEYCLOAK_CLIENT_SECRET
) )
} }
@@ -30,8 +30,7 @@ val authModule = module {
single<TokenProvider> { single<TokenProvider> {
object : TokenProvider { object : TokenProvider {
override fun getAccessToken(): String? { override fun getAccessToken(): String? {
val token = get<AuthTokenManager>().getToken() return get<AuthTokenManager>().getToken()
return token
} }
} }
} }
@@ -2,10 +2,9 @@ package at.mocode.frontend.core.network
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.plugins.* 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.contentnegotiation.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.qualifier.named 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". * Koin module providing HttpClients.
* The client uses the environment-aware base URL from NetworkConfig.
*/ */
val networkModule = module { 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")) { single(named("apiClient")) {
val tokenProvider: TokenProvider? = try { get<TokenProvider>() } catch (_: Throwable) { null } val tokenProvider: TokenProvider? = try { get<TokenProvider>() } catch (_: Throwable) { null }
HttpClient { HttpClient {
@@ -54,19 +71,15 @@ val networkModule = module {
exponentialDelay() exponentialDelay()
} }
// Authentication plugin (Bearer) // Manual Auth Header Injection (instead of Auth plugin) to support immediate logout
install(Auth) { install(DefaultRequest) {
bearer { val base = NetworkConfig.baseUrl.trimEnd('/')
loadTokens { url(base) // Set base URL
val token = tokenProvider?.getAccessToken()
token?.let { BearerTokens(it, refreshToken = "") } // Inject Authorization header if token is present
} val token = tokenProvider?.getAccessToken()
// Only send token to our API base URL if (token != null) {
sendWithoutRequest { request -> header("Authorization", "Bearer $token")
val base = NetworkConfig.baseUrl.trimEnd('/')
val url = request.url.toString()
url.startsWith(base)
}
} }
} }
@@ -79,12 +92,6 @@ val networkModule = module {
} }
level = LogLevel.INFO level = LogLevel.INFO
} }
// Set base URL
defaultRequest {
// Set only the base URL; endpoints will append paths
url(NetworkConfig.baseUrl)
}
} }
} }
} }