chore(infra): Network/Auth – DoD schließen

- Entfernen/Deprecaten: `frontend/features/auth-feature/.../AuthenticatedHttpClient.kt` und alle manuellen `Authorization`‑Header‑Setzungen.
              - Stattdessen: DI‑`apiClient` via Koin injizieren (`single(named("apiClient"))`) und Token‑Anreicherung über Ktor `Auth` Plugin (Bearer) verdrahten.
              - Build‑Guard ergänzen: Auch Vorkommen von `HttpHeaders.Authorization` erkennen.
This commit is contained in:
Stefan Mogeritsch 2025-12-06 22:01:41 +01:00
parent b3927ed97c
commit 7d9d729d7d
8 changed files with 69 additions and 16 deletions

View File

@ -4,6 +4,8 @@ import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.auth.Auth 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.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel 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.core.qualifier.named
import org.koin.dsl.module 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". * Koin module that provides a preconfigured Ktor HttpClient under the named qualifier "apiClient".
* The client uses the environment-aware base URL from NetworkConfig. * The client uses the environment-aware base URL from NetworkConfig.
*/ */
val networkModule = module { val networkModule = module {
single(named("apiClient")) { single(named("apiClient")) {
val tokenProvider: TokenProvider? = try { get<TokenProvider>() } catch (_: Throwable) { null }
HttpClient { HttpClient {
// JSON (kotlinx) configuration // JSON (kotlinx) configuration
install(ContentNegotiation) { install(ContentNegotiation) {
@ -50,9 +60,20 @@ val networkModule = module {
exponentialDelay() exponentialDelay()
} }
// Authentication plugin (Bearer refresh can be wired later) // Authentication plugin (Bearer)
install(Auth) { 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 // Logging for development

View File

@ -61,6 +61,12 @@ kotlin {
implementation(libs.ktor.client.logging) implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.auth) implementation(libs.ktor.client.auth)
// DI
implementation(libs.koin.core)
// Network core (provides apiClient + TokenProvider)
implementation(projects.frontend.core.network)
// Coroutines and serialization // Coroutines and serialization
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)

View File

@ -5,6 +5,9 @@ import io.ktor.client.call.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.serialization.Serializable 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 * Data classes for authentication API communication
@ -37,7 +40,7 @@ class AuthApiClient(
// Optional: Client-Secret (nur bei vertraulichen Clients erforderlich) // Optional: Client-Secret (nur bei vertraulichen Clients erforderlich)
private val clientSecret: String? = null private val clientSecret: String? = null
) { ) {
private val client = AuthenticatedHttpClient.createUnauthenticated() private val client: HttpClient by lazy { GlobalContext.get().koin.get<HttpClient>(named("apiClient")) }
/** /**
* Authenticate user with username and password * Authenticate user with username and password

View File

@ -2,9 +2,10 @@ package at.mocode.clients.authfeature
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader import io.ktor.client.request.post
import at.mocode.shared.core.AppConstants import org.koin.core.context.GlobalContext
import io.ktor.client.request.* import org.koin.core.qualifier.named
import io.ktor.client.HttpClient
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -37,6 +38,7 @@ class LoginViewModel(
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow() val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
private val authApiClient = AuthApiClient() private val authApiClient = AuthApiClient()
private val apiClient: HttpClient by lazy { GlobalContext.get().koin.get<HttpClient>(named("apiClient")) }
fun updateUsername(username: String) { fun updateUsername(username: String) {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
@ -96,10 +98,8 @@ class LoginViewModel(
// Fire-and-forget: Trigger Backend Sync so the user exists in Members // Fire-and-forget: Trigger Backend Sync so the user exists in Members
viewModelScope.launch { viewModelScope.launch {
try { try {
val client = AuthenticatedHttpClient.create() // Fire-and-forget sync call; Bearer token added by Ktor Auth plugin
client.post("${AppConstants.GATEWAY_URL}/api/members/sync") { apiClient.post("/api/members/sync")
addAuthHeader()
}
} catch (_: Exception) { } catch (_: Exception) {
// Non-fatal: Wir zeigen Sync-Fehler im Login nicht an // Non-fatal: Wir zeigen Sync-Fehler im Login nicht an
} }

View File

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

View File

@ -6,8 +6,8 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import at.mocode.clients.shared.navigation.AppScreen import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.authfeature.AuthenticatedHttpClient
import at.mocode.clients.authfeature.AuthTokenManager import at.mocode.clients.authfeature.AuthTokenManager
import org.koin.core.context.GlobalContext
import at.mocode.clients.pingfeature.PingScreen import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.shared.core.AppConstants import at.mocode.shared.core.AppConstants
@ -29,7 +29,8 @@ fun MainApp() {
) { ) {
var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) } var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) }
val authTokenManager = remember { AuthenticatedHttpClient.getAuthTokenManager() } // Resolve AuthTokenManager from Koin
val authTokenManager = remember { GlobalContext.get().koin.get<AuthTokenManager>() }
val pingViewModel = remember { PingViewModel() } val pingViewModel = remember { PingViewModel() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@ -4,6 +4,7 @@ import kotlinx.browser.document
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import at.mocode.shared.di.initKoin import at.mocode.shared.di.initKoin
import at.mocode.frontend.core.network.networkModule 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.localDbModule
import at.mocode.frontend.core.localdb.DatabaseProvider import at.mocode.frontend.core.localdb.DatabaseProvider
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
@ -19,8 +20,8 @@ fun main() {
console.log("[WebApp] main() entered") console.log("[WebApp] main() entered")
// Initialize DI (Koin) with shared modules + network + local DB modules // Initialize DI (Koin) with shared modules + network + local DB modules
try { try {
initKoin { modules(networkModule, localDbModule) } initKoin { modules(networkModule, localDbModule, authFeatureModule) }
console.log("[WebApp] Koin initialized with networkModule + localDbModule") console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule")
} catch (e: dynamic) { } catch (e: dynamic) {
console.warn("[WebApp] Koin initialization warning:", e) console.warn("[WebApp] Koin initialization warning:", e)
} }

View File

@ -4,12 +4,13 @@ import androidx.compose.ui.window.WindowState
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.shared.di.initKoin import at.mocode.shared.di.initKoin
import at.mocode.frontend.core.network.networkModule import at.mocode.frontend.core.network.networkModule
import at.mocode.clients.authfeature.di.authFeatureModule
fun main() = application { fun main() = application {
// Initialize DI (Koin) with shared modules + network module // Initialize DI (Koin) with shared modules + network module
try { try {
initKoin { modules(networkModule) } initKoin { modules(networkModule, authFeatureModule) }
println("[DesktopApp] Koin initialized with networkModule") println("[DesktopApp] Koin initialized with networkModule + authFeatureModule")
} catch (e: Exception) { } catch (e: Exception) {
println("[DesktopApp] Koin initialization warning: ${e.message}") println("[DesktopApp] Koin initialization warning: ${e.message}")
} }