feat: Implement account deletion feature with UI and API integration
Some checks failed
Deploy tt-tagebuch / deploy (push) Has been cancelled

This commit is contained in:
Torsten Schulz (local)
2026-05-18 14:12:14 +02:00
parent b9bbd45ae9
commit ecfd3bf851
13 changed files with 265 additions and 9 deletions

View File

@@ -5972,8 +5972,10 @@ private fun SettingsScreen(
}
val authState by dependencies.authManager.state.collectAsState()
val clubState by dependencies.clubManager.state.collectAsState()
val androidContext = LocalContext.current
var sessionStatus by rememberSaveable { mutableStateOf<String?>(null) }
var isLoading by rememberSaveable { mutableStateOf(false) }
var showDeleteAccountDialog by rememberSaveable { mutableStateOf(false) }
val sessionValidMessage = tr("mobile.sessionValid", "Session gültig")
val sessionInvalidMessage = tr("mobile.sessionInvalid", "Session ungültig")
val sessionCheckFailedMessage = tr("mobile.sessionCheckFailed", "Session check fehlgeschlagen")
@@ -6084,11 +6086,22 @@ private fun SettingsScreen(
) { Text(tr("billing.title", "Abrechnung")) }
}
SectionTitle(tr("mobile.legal", "Rechtliches"))
Text(
tr("mobile.legalInAppTodo", "Impressum/Datenschutz: in-App-Ansicht folgt (kein Browser-Aufruf)."),
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(bottom = 4.dp),
)
TextButton(
onClick = {
androidContext.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("${dependencies.apiConfig.baseUrl}/datenschutz")),
)
},
modifier = Modifier.fillMaxWidth(),
) { Text("Datenschutzerklärung") }
TextButton(
onClick = {
androidContext.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("${dependencies.apiConfig.baseUrl}/impressum")),
)
},
modifier = Modifier.fillMaxWidth(),
) { Text("Impressum") }
Button(
onClick = {
isLoading = true
@@ -6134,6 +6147,17 @@ private fun SettingsScreen(
Text(tr("auth.logout", "Abmelden"))
}
SectionTitle("Konto und Daten")
OutlinedButton(
onClick = { showDeleteAccountDialog = true },
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.heightIn(min = TouchMinHeight),
) {
Text("Eigenes Konto löschen", color = MaterialTheme.colors.error)
}
OutlinedButton(
onClick = {
dependencies.applicationScope.launch {
@@ -6149,6 +6173,73 @@ private fun SettingsScreen(
Text(tr("club.change", "Verein wechseln"))
}
}
if (showDeleteAccountDialog) {
DeleteAccountDialog(
onDismiss = { showDeleteAccountDialog = false },
onConfirm = { password, onError ->
dependencies.applicationScope.launch {
runCatching {
dependencies.authManager.deleteAccount(password)
dependencies.clubManager.clearSelection()
dependencies.diaryManager.clear()
dependencies.membersManager.clear()
dependencies.trainingStatsManager.clear()
dependencies.scheduleManager.clear()
dependencies.pendingApprovalsManager.clear()
dependencies.permissionsAdminManager.clear()
dependencies.apiLogsManager.clear()
dependencies.clubInternalTournamentsManager.clear()
dependencies.officialTournamentsReadManager.clear()
}.onSuccess {
showDeleteAccountDialog = false
}.onFailure {
onError(it.message ?: "Konto konnte nicht gelöscht werden.")
}
}
},
)
}
}
@Composable
private fun DeleteAccountDialog(
onDismiss: () -> Unit,
onConfirm: (password: String, onError: (String) -> Unit) -> Unit,
) {
var password by rememberSaveable { mutableStateOf("") }
var error by rememberSaveable { mutableStateOf<String?>(null) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Eigenes Konto löschen") },
text = {
Column {
Text("Diese Aktion löscht dein Benutzerkonto, deine Login-Sessions, Vereinszuordnungen und personenbezogene Integrationsdaten. Geteilte Vereinsdaten bleiben erhalten.")
error?.let { Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(top = 8.dp)) }
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Passwort zur Bestätigung") },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
singleLine = true,
)
}
},
confirmButton = {
TextButton(
onClick = {
if (password.isBlank()) {
error = "Bitte Passwort eingeben."
} else {
onConfirm(password) { error = it }
}
},
) { Text("Endgültig löschen", color = MaterialTheme.colors.error) }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Abbrechen") }
},
)
}
@Composable

View File

@@ -1,9 +1,11 @@
package de.tsschulz.tt_tagebuch.shared.api
import de.tsschulz.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tsschulz.tt_tagebuch.shared.api.models.DeleteAccountRequest
import de.tsschulz.tt_tagebuch.shared.api.models.LoginRequest
import de.tsschulz.tt_tagebuch.shared.api.models.LoginResponse
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.post
import io.ktor.client.request.setBody
@@ -19,4 +21,10 @@ class AuthApi(
suspend fun logout() {
client.http.post("/api/auth/logout")
}
suspend fun deleteAccount(password: String) {
client.http.delete("/api/auth/account") {
setBody(DeleteAccountRequest(password = password))
}
}
}

View File

@@ -8,3 +8,8 @@ data class LoginRequest(
val password: String,
val rememberMe: Boolean = false,
)
@Serializable
data class DeleteAccountRequest(
val password: String,
)

View File

@@ -54,6 +54,11 @@ class AuthManager(
clearLocal()
}
suspend fun deleteAccount(password: String) {
authApi.deleteAccount(password)
clearLocal()
}
suspend fun clearLocal() {
tokenProvider.token = null
tokenProvider.username = null