feat: Erste Stufe der iOS-App - Anmeldung und Kontenliste
Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage, Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste Fachansicht die Kontenliste. Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen. Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a7545bf5b
commit
6b86a6bf91
@@ -0,0 +1,93 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Pflicht-Passwortwechsel. Solange der offen ist, sperrt das Backend jeden
|
||||
/// fachlichen Endpunkt mit 403 – auch das Anlegen der Gerätemarke. Deshalb
|
||||
/// steht dieser Bildschirm vor allem anderen.
|
||||
///
|
||||
/// Die Richtlinie des Servers (Mindestlänge, Zeichenarten) prüfen wir hier
|
||||
/// nicht nach. Das Backend kennt sie, und sie doppelt zu pflegen hiesse, sie
|
||||
/// irgendwann unterschiedlich zu haben. Es meldet den Grund im Klartext.
|
||||
struct PasswortWechselAnsicht: View {
|
||||
@Environment(AppZustand.self) private var zustand
|
||||
@State private var alt = ""
|
||||
@State private var neu = ""
|
||||
@State private var wiederholung = ""
|
||||
@FocusState private var feld: Feld?
|
||||
|
||||
private enum Feld { case alt, neu, wiederholung }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
SecureField(String(localized: "passwort.bisher"), text: $alt)
|
||||
.textContentType(.password)
|
||||
.focused($feld, equals: .alt)
|
||||
.submitLabel(.next)
|
||||
.onSubmit { feld = .neu }
|
||||
} footer: {
|
||||
Text(String(localized: "passwort.erklaerung"))
|
||||
}
|
||||
|
||||
Section {
|
||||
SecureField(String(localized: "passwort.neu"), text: $neu)
|
||||
.textContentType(.newPassword)
|
||||
.focused($feld, equals: .neu)
|
||||
.submitLabel(.next)
|
||||
.onSubmit { feld = .wiederholung }
|
||||
|
||||
SecureField(String(localized: "passwort.wiederholen"), text: $wiederholung)
|
||||
.textContentType(.newPassword)
|
||||
.focused($feld, equals: .wiederholung)
|
||||
.submitLabel(.go)
|
||||
.onSubmit { wechsle() }
|
||||
} footer: {
|
||||
if !wiederholung.isEmpty, neu != wiederholung {
|
||||
Text(String(localized: "passwort.stimmtNichtUeberein"))
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
||||
if let meldung = zustand.meldung {
|
||||
Section {
|
||||
Label(meldung, systemImage: "exclamationmark.triangle")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: wechsle) {
|
||||
if zustand.beschaeftigt {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text(String(localized: "passwort.speichern"))
|
||||
}
|
||||
}
|
||||
.disabled(bereit == false || zustand.beschaeftigt)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(String(localized: "allgemein.abmelden"), role: .destructive) {
|
||||
Task { await zustand.meldeAb() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "passwort.titel"))
|
||||
.onAppear { feld = .alt }
|
||||
}
|
||||
}
|
||||
|
||||
private var bereit: Bool {
|
||||
!alt.isEmpty && !neu.isEmpty && neu == wiederholung
|
||||
}
|
||||
|
||||
private func wechsle() {
|
||||
guard bereit else { return }
|
||||
let bisher = alt
|
||||
let frisch = neu
|
||||
Task {
|
||||
await zustand.wechslePasswort(alt: bisher, neu: frisch)
|
||||
alt = ""; neu = ""; wiederholung = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user