108 lines
3.9 KiB
Swift
108 lines
3.9 KiB
Swift
import SwiftUI
|
|
|
|
/// `POST /api/auth/change-password` (see `Networking/AccountAPI.swift`)
|
|
/// rotates EVERY session for the account on success, including this
|
|
/// device's — and the new session is only delivered as a Set-Cookie, which
|
|
/// this Bearer-token client never sees. There is no fresh token to recover
|
|
/// from that response, so trying to "stay logged in" here would leave the
|
|
/// app holding a dead token. Instead: show a brief confirmation, then log
|
|
/// out locally and let the user sign back in with the new password. This is
|
|
/// a deliberate consequence of how the backend's session rotation works,
|
|
/// not an arbitrary UX choice — don't "fix" this into a silent success.
|
|
struct ChangePasswordView: View {
|
|
@EnvironmentObject private var appState: AppState
|
|
@State private var currentPassword = ""
|
|
@State private var newPassword = ""
|
|
@State private var confirmPassword = ""
|
|
@State private var isSubmitting = false
|
|
@State private var errorMessage: String?
|
|
@State private var showSuccessAlert = false
|
|
|
|
private var passwordsMatch: Bool {
|
|
!newPassword.isEmpty && newPassword == confirmPassword
|
|
}
|
|
|
|
private var canSubmit: Bool {
|
|
!currentPassword.isEmpty && passwordsMatch && !isSubmitting
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: ZenithSpacing.md) {
|
|
ZenithTextField(
|
|
label: "Aktuelles Passwort",
|
|
text: $currentPassword,
|
|
placeholder: "Aktuelles Passwort",
|
|
isSecure: true,
|
|
textContentType: .password
|
|
)
|
|
|
|
ZenithDivider()
|
|
|
|
ZenithTextField(
|
|
label: "Neues Passwort",
|
|
text: $newPassword,
|
|
placeholder: "Neues Passwort",
|
|
isSecure: true,
|
|
textContentType: .newPassword
|
|
)
|
|
|
|
ZenithTextField(
|
|
label: "Neues Passwort bestätigen",
|
|
text: $confirmPassword,
|
|
placeholder: "Neues Passwort bestätigen",
|
|
isSecure: true,
|
|
textContentType: .newPassword
|
|
)
|
|
|
|
if !confirmPassword.isEmpty && !passwordsMatch {
|
|
Text("Die Passwörter stimmen nicht überein.")
|
|
.zenithBodySmStyle(color: .zenithError)
|
|
}
|
|
|
|
if let errorMessage {
|
|
Text(errorMessage)
|
|
.zenithBodySmStyle(color: .zenithError)
|
|
}
|
|
|
|
Button {
|
|
Task { await submit() }
|
|
} label: {
|
|
if isSubmitting {
|
|
ProgressView()
|
|
.tint(.white)
|
|
} else {
|
|
Text("Passwort ändern")
|
|
}
|
|
}
|
|
.buttonStyle(.zenithPrimary)
|
|
.disabled(!canSubmit)
|
|
.padding(.top, ZenithSpacing.sm)
|
|
}
|
|
.padding(ZenithSpacing.sm)
|
|
}
|
|
.background(Color.zenithBg)
|
|
.navigationTitle("Passwort ändern")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.alert("Passwort geändert", isPresented: $showSuccessAlert) {
|
|
Button("OK") {
|
|
Task { await appState.logout() }
|
|
}
|
|
} message: {
|
|
Text("Passwort geändert. Bitte melde dich mit dem neuen Passwort erneut an.")
|
|
}
|
|
}
|
|
|
|
private func submit() async {
|
|
errorMessage = nil
|
|
isSubmitting = true
|
|
defer { isSubmitting = false }
|
|
do {
|
|
try await AccountAPI.changePassword(currentPassword: currentPassword, newPassword: newPassword)
|
|
showSuccessAlert = true
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|