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:
Stefan Mogeritsch 2026-01-23 15:42:07 +01:00
parent a3ac6a52be
commit 48ee074dbd
7 changed files with 44 additions and 35 deletions

View File

@ -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 \

View File

@ -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 \

View File

@ -238,7 +238,7 @@
{
"type": "password",
"value": "Change_Me_In_Production!",
"temporary": true
"temporary": false
}
],
"realmRoles": [

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.
**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)

View File

@ -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) {

View File

@ -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<TokenProvider> {
object : TokenProvider {
override fun getAccessToken(): String? {
val token = get<AuthTokenManager>().getToken()
return token
return get<AuthTokenManager>().getToken()
}
}
}

View File

@ -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<TokenProvider>() } 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)
}
}
}
}