import SwiftUI /// Der zweite Faktor. Entweder sechs Ziffern aus der Authenticator-App oder /// ein Wiederherstellungscode der Form `ABCDE-FGHIJ`. /// /// Fünf Fehlversuche, dann ist die Zwischensitzung verbraucht und es geht /// zurück zur Anmeldung – so hält es das Backend /// (`backend/app/services/sessions.py`, `MAX_2FA_ATTEMPTS`). struct ZweiFaktorAnsicht: View { @Environment(AppZustand.self) private var zustand @State private var code = "" @State private var mitWiederherstellung = false @FocusState private var imFeld: Bool var body: some View { NavigationStack { Form { Section { TextField(platzhalter, text: $code) .textContentType(mitWiederherstellung ? .none : .oneTimeCode) .keyboardType(mitWiederherstellung ? .asciiCapable : .numberPad) .textInputAutocapitalization(.characters) .autocorrectionDisabled() .monospaced() .focused($imFeld) .submitLabel(.go) .onSubmit { pruefe() } .onChange(of: code) { _, neu in // Sechs Ziffern aus der App: direkt absenden, das // spart den Griff zur Tastatur. if !mitWiederherstellung, neu.count == 6, neu.allSatisfy(\.isNumber) { pruefe() } } } header: { Text(String(localized: "zweifaktor.ueberschrift")) } footer: { Text(mitWiederherstellung ? String(localized: "zweifaktor.erklaerungWiederherstellung") : String(localized: "zweifaktor.erklaerungCode")) } if let meldung = zustand.meldung { Section { Label(meldung, systemImage: "exclamationmark.triangle") .foregroundStyle(.red) } } Section { Button(action: pruefe) { if zustand.beschaeftigt { ProgressView() } else { Text(String(localized: "zweifaktor.bestaetigen")) } } .disabled(code.isEmpty || zustand.beschaeftigt) Button(mitWiederherstellung ? String(localized: "zweifaktor.dochCode") : String(localized: "zweifaktor.dochWiederherstellung")) { mitWiederherstellung.toggle() code = "" imFeld = true } } Section { Button(String(localized: "zweifaktor.abbrechen"), role: .destructive) { Task { await zustand.meldeAb() } } } } .navigationTitle(String(localized: "zweifaktor.titel")) .onAppear { imFeld = true } } } private var platzhalter: String { mitWiederherstellung ? String(localized: "zweifaktor.platzhalterWiederherstellung") : String(localized: "zweifaktor.platzhalterCode") } private func pruefe() { let eingabe = code.trimmingCharacters(in: .whitespaces) guard !eingabe.isEmpty else { return } Task { await zustand.pruefeZweitenFaktor( code: eingabe, wiederherstellung: mitWiederherstellung) code = "" } } } /// Die Richtlinie verlangt einen zweiten Faktor, das Konto hat keinen. /// Einrichten geht in Stufe 1 noch nicht in der App – also sagen wir das /// klar, statt den Nutzer gegen eine Wand laufen zu lassen. struct ZweitenFaktorEinrichtenHinweis: View { @Environment(AppZustand.self) private var zustand var body: some View { NavigationStack { ContentUnavailableView { Label(String(localized: "zweifaktorEinrichten.titel"), systemImage: "lock.shield") } description: { Text(String(localized: "zweifaktorEinrichten.erklaerung")) } actions: { if let adresse = zustand.serverAdresse { Link(String(localized: "zweifaktorEinrichten.imBrowser"), destination: adresse) .buttonStyle(.borderedProminent) } Button(String(localized: "allgemein.abmelden"), role: .destructive) { Task { await zustand.meldeAb() } } } .navigationTitle(String(localized: "zweifaktorEinrichten.titel")) } } }