MP-24 Epic 4: Fertigstellung von MP-24: Authentication DI Refactoring & Cleanup

Das Refactoring der Authentifizierungs-Komponenten auf Dependency Injection (Koin) wurde verifiziert und abgeschlossen. Alle manuellen Instanziierungen wurden entfernt und die korrekte Initialisierung in allen Entry-Points sichergestellt.
This commit is contained in:
2025-12-06 22:32:12 +01:00
parent 7d9d729d7d
commit afd109efcc
9 changed files with 66 additions and 114 deletions
@@ -6,7 +6,6 @@ import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
import io.ktor.client.HttpClient
import org.koin.core.context.GlobalContext
import org.koin.core.qualifier.named
/**
@@ -31,6 +30,7 @@ data class LoginResponse(
* HTTP client for authentication API calls
*/
class AuthApiClient(
private val httpClient: HttpClient,
// Keycloak Basis-URL (z. B. http://localhost:8180)
private val keycloakBaseUrl: String = AppConstants.KEYCLOAK_URL,
// Realm-Name in Keycloak
@@ -40,7 +40,6 @@ class AuthApiClient(
// Optional: Client-Secret (nur bei vertraulichen Clients erforderlich)
private val clientSecret: String? = null
) {
private val client: HttpClient by lazy { GlobalContext.get().koin.get<HttpClient>(named("apiClient")) }
/**
* Authenticate user with username and password
@@ -48,7 +47,7 @@ class AuthApiClient(
suspend fun login(username: String, password: String): LoginResponse {
val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token"
return try {
val response = client.submitForm(
val response = httpClient.submitForm(
url = tokenEndpoint,
formParameters = Parameters.build {
append("grant_type", "password")
@@ -93,7 +92,7 @@ class AuthApiClient(
suspend fun exchangeAuthorizationCode(code: String, codeVerifier: String, redirectUri: String): LoginResponse {
val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token"
return try {
val response = client.submitForm(
val response = httpClient.submitForm(
url = tokenEndpoint,
formParameters = Parameters.build {
append("grant_type", "authorization_code")
@@ -136,7 +135,7 @@ class AuthApiClient(
suspend fun refreshToken(refreshToken: String): LoginResponse {
val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token"
return try {
val response = client.submitForm(
val response = httpClient.submitForm(
url = tokenEndpoint,
formParameters = Parameters.build {
append("grant_type", "refresh_token")
@@ -1,62 +0,0 @@
package at.mocode.clients.authfeature
import at.mocode.shared.core.AppConstants
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
/**
* Singleton object for managing authenticated HTTP client configuration.
* Provides methods to create HTTP clients and add authentication headers manually.
*/
object AuthenticatedHttpClient {
private val authTokenManager = AuthTokenManager()
/**
* Create a basic HTTP client with JSON support
*/
fun create(baseUrl: String = AppConstants.GATEWAY_URL): HttpClient {
return HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
}
/**
* Add an authentication header to an HTTP request builder if a token is available
*/
fun HttpRequestBuilder.addAuthHeader() {
authTokenManager.getBearerToken()?.let { bearerToken ->
header(HttpHeaders.Authorization, bearerToken)
}
}
/**
* Get the shared AuthTokenManager instance
*/
fun getAuthTokenManager(): AuthTokenManager = authTokenManager
/**
* Create an HTTP client without authentication (for login/public endpoints)
*/
fun createUnauthenticated(): HttpClient {
return HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
}
}
@@ -14,13 +14,11 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
authTokenManager: AuthTokenManager,
viewModel: LoginViewModel = viewModel { LoginViewModel(authTokenManager) },
viewModel: LoginViewModel,
onLoginSuccess: () -> Unit = {}
) {
val uiState by viewModel.uiState.collectAsState()
@@ -3,8 +3,6 @@ package at.mocode.clients.authfeature
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.ktor.client.request.post
import org.koin.core.context.GlobalContext
import org.koin.core.qualifier.named
import io.ktor.client.HttpClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -31,15 +29,14 @@ data class LoginUiState(
* ViewModel for handling login authentication logic
*/
class LoginViewModel(
private val authTokenManager: AuthTokenManager
private val authTokenManager: AuthTokenManager,
private val authApiClient: AuthApiClient,
private val apiClient: HttpClient
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
private val authApiClient = AuthApiClient()
private val apiClient: HttpClient by lazy { GlobalContext.get().koin.get<HttpClient>(named("apiClient")) }
fun updateUsername(username: String) {
_uiState.value = _uiState.value.copy(
username = username,
@@ -1,7 +1,10 @@
package at.mocode.clients.authfeature.di
import at.mocode.clients.authfeature.AuthApiClient
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.clients.authfeature.LoginViewModel
import at.mocode.frontend.core.network.TokenProvider
import org.koin.core.qualifier.named
import org.koin.dsl.module
/**
@@ -11,6 +14,12 @@ val authFeatureModule = module {
// Single in-memory token manager
single { AuthTokenManager() }
// AuthApiClient with injected apiClient
single { AuthApiClient(get(named("apiClient"))) }
// LoginViewModel
factory { LoginViewModel(get(), get(), get(named("apiClient"))) }
// Bridge to core network TokenProvider without adding a hard dependency there
single<TokenProvider> {
object : TokenProvider {