refactor(desktop, core): Onboarding zu DeviceInitialization umbenannt, Navigation und Screens angepasst

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-18 11:10:01 +02:00
parent 315517f03f
commit 7bbb991e69
24 changed files with 742 additions and 222 deletions
@@ -3,8 +3,8 @@ package at.mocode.frontend.core.auth.di
import at.mocode.frontend.core.auth.data.AuthApiClient
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.auth.presentation.LoginViewModel
import at.mocode.frontend.core.network.TokenProvider
import at.mocode.frontend.core.domain.AppConstants
import at.mocode.frontend.core.network.TokenProvider
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -24,7 +24,7 @@ val authModule = module {
}
// LoginViewModel
factory { LoginViewModel(get(), get(), get(named("apiClient"))) }
factory { LoginViewModel(get(), get()) }
// Brücke zum TokenProvider des Kernnetzwerks, ohne dort eine harte Abhängigkeit hinzuzufügen
single<TokenProvider> {
@@ -0,0 +1,99 @@
package at.mocode.frontend.core.auth.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Login
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.MsCard
/**
* Eine Plug-and-Play Komponente zur Anzeige des aktuellen Authentifizierungs-Status.
* Kann überall (Sidebar, Header, Screens) eingesetzt werden.
*/
@Composable
fun AuthStatusCard(
viewModel: LoginViewModel,
modifier: Modifier = Modifier,
onLoginClick: () -> Unit = {}
) {
val authState by viewModel.authState.collectAsState()
val uiState by viewModel.uiState.collectAsState()
MsCard(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null,
tint = if (authState.isAuthenticated) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(32.dp)
)
Spacer(Modifier.width(12.dp))
Column {
Text(
text = if (authState.isAuthenticated) "Angemeldet als" else "Nicht angemeldet",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = if (authState.isAuthenticated) (authState.username ?: "Unbekannt") else "Gast",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold
)
}
}
if (authState.isAuthenticated) {
Button(
onClick = { viewModel.logout() },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Abmelden")
}
} else {
Button(
onClick = onLoginClick,
enabled = !uiState.isOidcLoading
) {
if (uiState.isOidcLoading) {
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
} else {
Icon(Icons.AutoMirrored.Filled.Login, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Anmelden")
}
}
}
}
if (authState.isAuthenticated && authState.roles.isNotEmpty()) {
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
authState.roles.forEach { role ->
SuggestionChip(
onClick = {},
label = { Text(role, style = MaterialTheme.typography.labelSmall) }
)
}
}
}
}
}
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.auth.data.*
import at.mocode.frontend.core.domain.AppConstants
import io.ktor.client.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -34,13 +33,15 @@ data class LoginUiState(
*/
class LoginViewModel(
private val authTokenManager: AuthTokenManager,
private val authApiClient: AuthApiClient,
private val apiClient: HttpClient
private val authApiClient: AuthApiClient
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
private val _authState = MutableStateFlow(AuthState())
val authState: StateFlow<AuthState> = _authState.asStateFlow()
// PKCE-State für den laufenden OIDC-Flow (in-memory)
private var pendingCodeVerifier: String? = null
private var pendingState: String? = null
@@ -48,9 +49,10 @@ class LoginViewModel(
init {
// AuthTokenManager-State beobachten → UI synchron halten
viewModelScope.launch {
authTokenManager.authState.collect { authState ->
_uiState.value = _uiState.value.copy(isAuthenticated = authState.isAuthenticated)
if (!authState.isAuthenticated) {
authTokenManager.authState.collect { auth ->
_authState.value = auth
_uiState.value = _uiState.value.copy(isAuthenticated = auth.isAuthenticated)
if (!auth.isAuthenticated) {
_uiState.value = LoginUiState()
}
}
@@ -223,4 +225,11 @@ class LoginViewModel(
}
}
}
/** Abmelden. */
fun logout() {
viewModelScope.launch {
authTokenManager.clearToken()
}
}
}