Feature: Self-Hosted Server URL configuration & Authentik OIDC support
- DynamicBaseUrlInterceptor: dynamically rewrites Retrofit HTTP requests to custom server URL - SessionManager: persist custom server_url setting (e.g. https://mitbringsl.mydomain.com) - AuthScreen & ViewModel: add Server URL chip & edit dialog + Authentik / OIDC login mode - NetworkModule: inject SessionManager into DynamicBaseUrlInterceptor & AuthInterceptor - ADB reinstall & launch verified ✅
This commit is contained in:
parent
04e5c55c2a
commit
47b24dfcdf
6 changed files with 284 additions and 95 deletions
|
|
@ -2,20 +2,16 @@ package com.example.mitbringsl.data.auth
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import com.example.mitbringsl.BuildConfig
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
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
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the user's session state and local device identification.
|
* Manages the user's session state, token storage, and self-hosted server URL.
|
||||||
*
|
|
||||||
* Local-First design: Users do NOT need an account to use the app locally.
|
|
||||||
* When not logged in, [getUserId] returns a stable local ID ("local_user").
|
|
||||||
* Logging in is 100% optional and only required to sync across devices.
|
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class SessionManager @Inject constructor(
|
class SessionManager @Inject constructor(
|
||||||
|
|
@ -30,11 +26,26 @@ class SessionManager @Inject constructor(
|
||||||
private val _isLoggedIn = MutableStateFlow(!getToken().isNullOrBlank())
|
private val _isLoggedIn = MutableStateFlow(!getToken().isNullOrBlank())
|
||||||
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
||||||
|
|
||||||
|
private val _serverUrl = MutableStateFlow(getServerUrl())
|
||||||
|
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
||||||
|
|
||||||
|
fun getServerUrl(): String {
|
||||||
|
return prefs.getString("server_url", null) ?: BuildConfig.BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveServerUrl(url: String) {
|
||||||
|
val cleanUrl = if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
"https://$url"
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
}.trim().trimEnd('/')
|
||||||
|
|
||||||
|
prefs.edit { putString("server_url", cleanUrl) }
|
||||||
|
_serverUrl.value = cleanUrl
|
||||||
|
}
|
||||||
|
|
||||||
fun getToken(): String? = prefs.getString("token", null)
|
fun getToken(): String? = prefs.getString("token", null)
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the authenticated user's ID if logged in, or [LOCAL_USER_ID] for local-only mode.
|
|
||||||
*/
|
|
||||||
fun getUserId(): String {
|
fun getUserId(): String {
|
||||||
val remoteId = prefs.getString("user_id", null)
|
val remoteId = prefs.getString("user_id", null)
|
||||||
if (!remoteId.isNullOrBlank()) return remoteId
|
if (!remoteId.isNullOrBlank()) return remoteId
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
package com.example.mitbringsl.data.remote.api
|
package com.example.mitbringsl.data.remote.api
|
||||||
|
|
||||||
|
import com.example.mitbringsl.data.auth.SessionManager
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OkHttp interceptor that injects the session Bearer token into every request.
|
* OkHttp interceptor that injects the session Bearer token into every request.
|
||||||
*
|
|
||||||
* [tokenProvider] is a lambda so the interceptor always reads the most recent
|
|
||||||
* token without needing to be recreated after login.
|
|
||||||
*/
|
*/
|
||||||
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
|
class AuthInterceptor(private val sessionManager: SessionManager) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val token = tokenProvider()
|
val token = sessionManager.getToken()
|
||||||
val request = if (token.isNullOrBlank()) {
|
val request = if (token.isNullOrBlank()) {
|
||||||
chain.request()
|
chain.request()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.example.mitbringsl.data.remote.api
|
||||||
|
|
||||||
|
import com.example.mitbringsl.data.auth.SessionManager
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor that dynamically redirects requests to the user's configured self-hosted server URL.
|
||||||
|
*/
|
||||||
|
class DynamicBaseUrlInterceptor(
|
||||||
|
private val sessionManager: SessionManager
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val original = chain.request()
|
||||||
|
val customServerUrl = sessionManager.getServerUrl().trim().trimEnd('/')
|
||||||
|
|
||||||
|
if (customServerUrl.isBlank()) {
|
||||||
|
return chain.proceed(original)
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetUrl = customServerUrl.toHttpUrlOrNull() ?: return chain.proceed(original)
|
||||||
|
|
||||||
|
val newUrl = original.url.newBuilder()
|
||||||
|
.scheme(targetUrl.scheme)
|
||||||
|
.host(targetUrl.host)
|
||||||
|
.port(targetUrl.port)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val newRequest = original.newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(newRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.example.mitbringsl.di
|
package com.example.mitbringsl.di
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.example.mitbringsl.BuildConfig
|
import com.example.mitbringsl.BuildConfig
|
||||||
|
import com.example.mitbringsl.data.auth.SessionManager
|
||||||
import com.example.mitbringsl.data.remote.api.AuthInterceptor
|
import com.example.mitbringsl.data.remote.api.AuthInterceptor
|
||||||
|
import com.example.mitbringsl.data.remote.api.DynamicBaseUrlInterceptor
|
||||||
import com.example.mitbringsl.data.remote.api.MitbringslApi
|
import com.example.mitbringsl.data.remote.api.MitbringslApi
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
|
@ -21,10 +21,6 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object NetworkModule {
|
object NetworkModule {
|
||||||
|
|
||||||
/**
|
|
||||||
* Lax JSON decoder: ignores unknown keys so the app doesn't crash when the
|
|
||||||
* backend adds new fields, and handles missing optional fields gracefully.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideJson(): Json = Json {
|
fun provideJson(): Json = Json {
|
||||||
|
|
@ -35,15 +31,7 @@ object NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideTokenProvider(@ApplicationContext context: Context): () -> String? = {
|
fun provideOkHttpClient(sessionManager: SessionManager): OkHttpClient {
|
||||||
// Read token from SharedPreferences (written after login).
|
|
||||||
context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
|
||||||
.getString("token", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideOkHttpClient(tokenProvider: () -> String?): OkHttpClient {
|
|
||||||
val logging = HttpLoggingInterceptor().apply {
|
val logging = HttpLoggingInterceptor().apply {
|
||||||
level = if (BuildConfig.DEBUG) {
|
level = if (BuildConfig.DEBUG) {
|
||||||
HttpLoggingInterceptor.Level.BODY
|
HttpLoggingInterceptor.Level.BODY
|
||||||
|
|
@ -52,7 +40,8 @@ object NetworkModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
.addInterceptor(AuthInterceptor(tokenProvider))
|
.addInterceptor(DynamicBaseUrlInterceptor(sessionManager))
|
||||||
|
.addInterceptor(AuthInterceptor(sessionManager))
|
||||||
.addInterceptor(logging)
|
.addInterceptor(logging)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Dns
|
||||||
|
import androidx.compose.material.icons.filled.Key
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
|
@ -36,8 +38,11 @@ fun AuthScreen(
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val isLoggedIn by viewModel.sessionManager.isLoggedIn.collectAsStateWithLifecycle()
|
val isLoggedIn by viewModel.sessionManager.isLoggedIn.collectAsStateWithLifecycle()
|
||||||
|
val currentServerUrl by viewModel.sessionManager.serverUrl.collectAsStateWithLifecycle()
|
||||||
val userEmail = viewModel.sessionManager.getUserEmail()
|
val userEmail = viewModel.sessionManager.getUserEmail()
|
||||||
|
|
||||||
|
var showServerUrlDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val gradientBrush = Brush.verticalGradient(
|
val gradientBrush = Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
|
|
@ -59,7 +64,7 @@ fun AuthScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// App Logo / Title Header
|
// App Logo / Title Header
|
||||||
Surface(
|
Surface(
|
||||||
|
|
@ -83,13 +88,35 @@ fun AuthScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Melde dich an, um deine Listen geräteübergreifend zu synchronisieren.",
|
text = "Verbinde dich mit deinem Server oder Authentik für geräteübergreifenden Sync.",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Server URL Chip
|
||||||
|
AssistChip(
|
||||||
|
onClick = { showServerUrlDialog = true },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = "Server: $currentServerUrl",
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Dns,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// Auth Card
|
// Auth Card
|
||||||
Card(
|
Card(
|
||||||
|
|
@ -120,9 +147,7 @@ fun AuthScreen(
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = { viewModel.sessionManager.clearSession() },
|
||||||
viewModel.sessionManager.clearSession()
|
|
||||||
},
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.error
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
),
|
),
|
||||||
|
|
@ -133,7 +158,11 @@ fun AuthScreen(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = if (uiState.isRegisteringMode) "Konto erstellen" else "Anmelden",
|
text = when {
|
||||||
|
uiState.isOidcMode -> "Authentik / OIDC Login"
|
||||||
|
uiState.isRegisteringMode -> "Konto erstellen"
|
||||||
|
else -> "Anmelden"
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
@ -166,6 +195,24 @@ fun AuthScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState.isOidcMode) {
|
||||||
|
// OIDC / Authentik Token Field
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.oidcTokenInput,
|
||||||
|
onValueChange = viewModel::onOidcTokenInputChanged,
|
||||||
|
label = { Text("OIDC ID-Token (JWT)") },
|
||||||
|
placeholder = { Text("eyJhbGci...") },
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 4,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
viewModel.submit()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
// Display Name (only in register mode)
|
// Display Name (only in register mode)
|
||||||
if (uiState.isRegisteringMode) {
|
if (uiState.isRegisteringMode) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|
@ -216,6 +263,7 @@ fun AuthScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
|
@ -239,7 +287,11 @@ fun AuthScreen(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = if (uiState.isRegisteringMode) "Registrieren" else "Anmelden",
|
text = when {
|
||||||
|
uiState.isOidcMode -> "Mit Authentik / OIDC verbinden"
|
||||||
|
uiState.isRegisteringMode -> "Registrieren"
|
||||||
|
else -> "Anmelden"
|
||||||
|
},
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
)
|
)
|
||||||
|
|
@ -248,18 +300,34 @@ fun AuthScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
// Toggle Login / Register mode
|
// Toggle Buttons (E-Mail vs OIDC / Authentik)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
TextButton(onClick = viewModel::toggleMode) {
|
TextButton(onClick = viewModel::toggleMode) {
|
||||||
Text(
|
Text(
|
||||||
text = if (uiState.isRegisteringMode) {
|
text = if (uiState.isRegisteringMode) "Zurück zu Login" else "Konto erstellen",
|
||||||
"Bereits ein Konto? Hier anmelden"
|
|
||||||
} else {
|
|
||||||
"Noch kein Konto? Hier registrieren"
|
|
||||||
},
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = viewModel::toggleOidcMode) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Key,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = if (uiState.isOidcMode) "Passwort Login" else "Authentik / OIDC",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
@ -276,4 +344,52 @@ fun AuthScreen(
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server URL Dialog
|
||||||
|
if (showServerUrlDialog) {
|
||||||
|
var tempUrl by remember { mutableStateOf(currentServerUrl) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showServerUrlDialog = false },
|
||||||
|
title = { Text("Self-Hosted Server URL") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Gib die URL deines Mitbringsl Backend-Servers ein:",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = tempUrl,
|
||||||
|
onValueChange = { tempUrl = it },
|
||||||
|
label = { Text("Server URL") },
|
||||||
|
placeholder = { Text("https://mitbringsl.example.com") },
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (tempUrl.isNotBlank()) {
|
||||||
|
viewModel.saveServerUrl(tempUrl)
|
||||||
|
showServerUrlDialog = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Text("Speichern")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showServerUrlDialog = false }) {
|
||||||
|
Text("Abbrechen")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@ data class AuthUiState(
|
||||||
val emailInput: String = "",
|
val emailInput: String = "",
|
||||||
val passwordInput: String = "",
|
val passwordInput: String = "",
|
||||||
val displayNameInput: String = "",
|
val displayNameInput: String = "",
|
||||||
|
val oidcTokenInput: String = "",
|
||||||
val isRegisteringMode: Boolean = false,
|
val isRegisteringMode: Boolean = false,
|
||||||
|
val isOidcMode: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
|
val serverUrlInput: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -28,9 +31,20 @@ class AuthViewModel @Inject constructor(
|
||||||
val sessionManager: SessionManager,
|
val sessionManager: SessionManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(AuthUiState())
|
private val _uiState = MutableStateFlow(
|
||||||
|
AuthUiState(serverUrlInput = sessionManager.getServerUrl())
|
||||||
|
)
|
||||||
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun onServerUrlChanged(value: String) {
|
||||||
|
_uiState.update { it.copy(serverUrlInput = value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveServerUrl(url: String) {
|
||||||
|
sessionManager.saveServerUrl(url)
|
||||||
|
_uiState.update { it.copy(serverUrlInput = sessionManager.getServerUrl(), errorMessage = null) }
|
||||||
|
}
|
||||||
|
|
||||||
fun onEmailChanged(value: String) {
|
fun onEmailChanged(value: String) {
|
||||||
_uiState.update { it.copy(emailInput = value, errorMessage = null) }
|
_uiState.update { it.copy(emailInput = value, errorMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
@ -43,10 +57,24 @@ class AuthViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(displayNameInput = value, errorMessage = null) }
|
_uiState.update { it.copy(displayNameInput = value, errorMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onOidcTokenInputChanged(value: String) {
|
||||||
|
_uiState.update { it.copy(oidcTokenInput = value, errorMessage = null) }
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleMode() {
|
fun toggleMode() {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRegisteringMode = !it.isRegisteringMode,
|
isRegisteringMode = !it.isRegisteringMode,
|
||||||
|
isOidcMode = false,
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleOidcMode() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isOidcMode = !it.isOidcMode,
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +82,16 @@ class AuthViewModel @Inject constructor(
|
||||||
|
|
||||||
fun submit() {
|
fun submit() {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
|
|
||||||
|
if (state.isOidcMode) {
|
||||||
|
if (state.oidcTokenInput.isBlank()) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Bitte ID-Token eingeben.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onOidcTokenReceived(provider = "generic", idToken = state.oidcTokenInput.trim())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (state.emailInput.isBlank() || state.passwordInput.isBlank()) {
|
if (state.emailInput.isBlank() || state.passwordInput.isBlank()) {
|
||||||
_uiState.update { it.copy(errorMessage = "Bitte E-Mail und Passwort ausfüllen.") }
|
_uiState.update { it.copy(errorMessage = "Bitte E-Mail und Passwort ausfüllen.") }
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue