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:
parent
b3927ed97c
commit
7d9d729d7d
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user