refactor(auth): migrate Auth module from feature to core package

- Reorganized `auth-feature` into `core/auth` to improve architecture and modularity.
- Removed unused PKCE and OAuth callback utilities (`AuthCallbackParams`, `OAuthPkceService`).
- Updated imports and adjusted build scripts to reflect new module structure.
- Refactored `LoginScreen` and `PingScreen` to include `onBack` functionality in top bars for improved navigation.
- Corrected sync endpoint in `PingSyncService`.
This commit is contained in:
2026-01-23 01:16:40 +01:00
parent c692a2395c
commit d7cf8200e1
34 changed files with 470 additions and 773 deletions
@@ -80,8 +80,8 @@ kotlin {
implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network)
implementation(projects.frontend.core.sync)
implementation(project(":frontend:core:local-db"))
implementation(projects.frontend.features.authFeature)
implementation(projects.frontend.core.localDb)
implementation(projects.frontend.core.auth)
implementation(projects.frontend.features.pingFeature)
// DI (Koin) needed to call initKoin { modules(...) }
@@ -1,24 +1,23 @@
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.auth.data.AuthApiClient
import at.mocode.frontend.core.auth.presentation.LoginScreen
import at.mocode.frontend.core.auth.presentation.LoginViewModel
import at.mocode.ping.feature.presentation.PingScreen
import at.mocode.ping.feature.presentation.PingViewModel
import at.mocode.shared.core.AppConstants
import androidx.compose.material3.OutlinedTextField
import androidx.compose.ui.text.input.PasswordVisualTransformation
import at.mocode.frontend.core.designsystem.components.AppFooter
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
import at.mocode.clients.authfeature.AuthApiClient
import at.mocode.clients.authfeature.LoginViewModel
import at.mocode.clients.authfeature.oauth.OAuthPkceService
import at.mocode.clients.authfeature.oauth.AuthCallbackParams
import at.mocode.clients.authfeature.oauth.CallbackParams
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -33,29 +32,10 @@ fun MainApp() {
// Resolve AuthTokenManager from Koin
val authTokenManager = koinInject<AuthTokenManager>()
val authApiClient = koinInject<AuthApiClient>()
// Delta-Sync blueprint: resolve the Ping feature view model via Koin.
val pingViewModel: PingViewModel = koinViewModel()
// scope removed (unused)
// Handle PKCE callback on an app load (web)
LaunchedEffect(Unit) {
val callback: CallbackParams? = AuthCallbackParams.parse()
if (callback != null) {
val code = callback.code
val state = callback.state
val pkce = OAuthPkceService.current()
if (pkce != null && pkce.state == state) {
val res = authApiClient.exchangeAuthorizationCode(code, pkce.codeVerifier, AppConstants.webRedirectUri())
val token = res.token
if (res.success && token != null) {
authTokenManager.setToken(token)
OAuthPkceService.clear()
currentScreen = AppScreen.Profile
}
}
}
}
val loginViewModel: LoginViewModel = koinViewModel()
when (currentScreen) {
is AppScreen.Landing -> LandingScreen(
@@ -65,18 +45,21 @@ fun MainApp() {
is AppScreen.Home -> WelcomeScreen(
authTokenManager = authTokenManager,
onOpenPing = { currentScreen = AppScreen.Ping },
onOpenLogin = {
// Fallback to the local LoginScreen (Password Grant) if PKCE cannot be started
currentScreen = AppScreen.Login
},
onOpenLogin = { currentScreen = AppScreen.Login },
onOpenProfile = { currentScreen = AppScreen.Profile }
)
is AppScreen.Login -> LoginScreen(
onLoginSuccess = { currentScreen = AppScreen.Profile }
viewModel = loginViewModel,
onLoginSuccess = { currentScreen = AppScreen.Profile },
onBack = { currentScreen = AppScreen.Home }
)
is AppScreen.Ping -> PingScreen(
viewModel = pingViewModel,
onBack = { currentScreen = AppScreen.Home } // Navigate back to Home
)
is AppScreen.Ping -> PingScreen(viewModel = pingViewModel)
is AppScreen.Profile -> AuthStatusScreen(
authTokenManager = authTokenManager,
onBackToHome = { currentScreen = AppScreen.Home }
@@ -93,8 +76,13 @@ private fun LandingScreen(
onPrimaryCta: () -> Unit,
onSecondary: () -> Unit
) {
// Minimal Landing-Layout: Hero → Manifest → Features → Footer
Column(modifier = Modifier.fillMaxSize()) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
// Hero
Column(
modifier = Modifier
@@ -102,7 +90,6 @@ private fun LandingScreen(
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Wortmarke (Platzhalter)
Text(
text = "EquestEvents",
style = MaterialTheme.typography.titleMedium,
@@ -179,7 +166,7 @@ private fun LandingScreen(
}
// Footer
at.mocode.clients.shared.commonui.components.AppFooter()
AppFooter()
}
}
@@ -207,12 +194,12 @@ private fun WelcomeScreen(
onOpenProfile: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -239,41 +226,11 @@ private fun WelcomeScreen(
Button(onClick = onOpenPing, modifier = Modifier.weight(1f)) { Text("Ping-Service") }
if (!authState.isAuthenticated) {
Button(
onClick = {
// Try PKCE login (Authorization Code Flow w/ PKCE)
scope.launch {
try {
val pkce = OAuthPkceService.startAuth()
val url = OAuthPkceService.buildAuthorizeUrl(pkce, AppConstants.webRedirectUri())
uriHandler.openUri(url)
} catch (_: Throwable) {
// Fallback: open the local Login screen (Password Grant)
onOpenLogin()
}
}
},
onClick = onOpenLogin,
modifier = Modifier.weight(1f)
) { Text("Login") }
}
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.registerUrl()) },
modifier = Modifier.weight(1f)
) { Text("Registrieren (Keycloak)") }
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.loginUrl()) },
modifier = Modifier.weight(1f)
) { Text("Keycloak Login-Seite") }
}
// Desktop Download Link
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.desktopDownloadUrl()) },
modifier = Modifier.fillMaxWidth()
) { Text("Desktop-App herunterladen") }
}
}
@@ -299,6 +256,9 @@ private fun AuthStatusScreen(
authTokenManager.clearToken()
onBackToHome()
}) { Text("Abmelden") }
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = onBackToHome) { Text("Zurück zur Startseite") }
} else {
Text("Nicht angemeldet.")
Spacer(Modifier.height(8.dp))
@@ -308,64 +268,3 @@ private fun AuthStatusScreen(
}
}
}
@Composable
private fun LoginScreen(
onLoginSuccess: () -> Unit
) {
val viewModel = koinViewModel<LoginViewModel>()
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(uiState.isAuthenticated) {
if (uiState.isAuthenticated) {
onLoginSuccess()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("Anmeldung", style = MaterialTheme.typography.headlineMedium)
OutlinedTextField(
value = uiState.username,
onValueChange = { viewModel.updateUsername(it) },
label = { Text("Benutzername") },
singleLine = true,
enabled = !uiState.isLoading,
isError = uiState.usernameError != null,
modifier = Modifier.fillMaxWidth()
)
if (uiState.usernameError != null) {
Text(uiState.usernameError!!, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
}
OutlinedTextField(
value = uiState.password,
onValueChange = { viewModel.updatePassword(it) },
label = { Text("Passwort") },
singleLine = true,
enabled = !uiState.isLoading,
visualTransformation = PasswordVisualTransformation(),
isError = uiState.passwordError != null,
modifier = Modifier.fillMaxWidth()
)
if (uiState.passwordError != null) {
Text(uiState.passwordError!!, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
}
if (uiState.errorMessage != null) {
Text(uiState.errorMessage!!, color = MaterialTheme.colorScheme.error)
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = { viewModel.login() },
enabled = uiState.canLogin
) { Text(if (uiState.isLoading) "Bitte warten…" else "Login") }
}
}
}
@@ -1,6 +1,6 @@
package navigation
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.domain.models.User
import at.mocode.frontend.core.navigation.CurrentUserProvider
import at.mocode.frontend.core.navigation.DeepLinkHandler
@@ -1,6 +1,6 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import at.mocode.clients.authfeature.di.authFeatureModule
import at.mocode.frontend.core.auth.di.authModule
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.localdb.DatabaseProvider
import at.mocode.frontend.core.localdb.localDbModule
@@ -27,8 +27,8 @@ fun main() {
// Initialize DI (Koin) with shared modules + network + local DB modules
try {
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule + pingFeatureModule")
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authModule, navigationModule) }
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authModule + navigationModule + pingFeatureModule")
} catch (e: dynamic) {
console.warn("[WebApp] Koin initialization warning:", e)
}
@@ -4,7 +4,7 @@ import androidx.compose.ui.window.WindowState
import androidx.compose.ui.unit.dp
import at.mocode.shared.di.initKoin
import at.mocode.frontend.core.network.networkModule
import at.mocode.clients.authfeature.di.authFeatureModule
import at.mocode.frontend.core.auth.di.authModule
import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.ping.feature.di.pingFeatureModule
import at.mocode.frontend.core.localdb.AppDatabase
@@ -19,8 +19,8 @@ fun main() = application {
// Initialize DI (Koin) with shared modules + network module
try {
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
initKoin { modules(networkModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule, localDbModule) }
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule + pingFeatureModule + localDbModule")
initKoin { modules(networkModule, syncModule, pingFeatureModule, authModule, navigationModule, localDbModule) }
println("[DesktopApp] Koin initialized with networkModule + authModule + navigationModule + pingFeatureModule + localDbModule")
} catch (e: Exception) {
println("[DesktopApp] Koin initialization warning: ${e.message}")
}