import Foundation import ManaCore import Observation /// State-Maschine für ``ManaTwoFactorEnrollView``. 3-Phasen-Wizard: /// /// 1. **Re-Auth** — User gibt aktuelles Passwort ein /// 2. **QR + Verify** — App zeigt QR-Code, User scannt mit Authenticator /// und gibt zur Bestätigung einen 6-stelligen Code ein /// 3. **Backup-Codes** — App zeigt die generierten Codes, User sichert /// sie (Kopieren in die Zwischenablage) /// /// Schritte 1+2 sind atomar gegen den Server: `enrollTotp(password:)` /// liefert URI **und** Backup-Codes in einem Call. Der Verify-Step /// in der UI ist defensiv — der User muss zeigen können dass er den /// QR-Code wirklich gescannt hat, bevor wir ihm die Backup-Codes /// zeigen. Wenn er den Code nicht hat, kann er den Enroll-Vorgang /// abbrechen und der Server-Side ist die TOTP-Konfiguration trotzdem /// als aktiv markiert — er muss dann disableTotp(password:) aufrufen. @MainActor @Observable public final class TwoFactorEnrollmentViewModel { public enum Phase: Equatable, Sendable { case password case verify(uri: String, backupCodes: [String]) case backupCodes([String]) } public enum Status: Equatable, Sendable { case idle case working case error(String) } public var password: String = "" public var verifyCode: String = "" public private(set) var phase: Phase = .password public private(set) var status: Status = .idle private let auth: AuthClient public init(auth: AuthClient) { self.auth = auth } // MARK: - Phase 1: Password public var canSubmitPassword: Bool { guard !password.isEmpty else { return false } if case .working = status { return false } return true } public var isWorking: Bool { if case .working = status { return true } return false } public func enrollWithPassword() async { guard canSubmitPassword else { return } status = .working do { let enrollment = try await auth.enrollTotp(password: password) password = "" phase = .verify(uri: enrollment.totpURI, backupCodes: enrollment.backupCodes) status = .idle } catch let error as AuthError { status = .error(error.errorDescription ?? "Aktivierung fehlgeschlagen") } catch { status = .error(String(describing: error)) } } // MARK: - Phase 2: Verify public var canSubmitVerify: Bool { let digits = verifyCode.filter { $0.isNumber } return digits.count == 6 && !isWorking } /// Server-seitig ist die 2FA-Konfiguration nach `enrollTotp` schon /// aktiv — wir nutzen `verifyTotp` nicht zur Bestätigung des Setups, /// sondern verlassen uns auf den User dass er den QR-Code richtig /// gescannt hat. Better-Auth-API hat keinen "verify-setup-Endpoint" /// (verify-totp ist nur im Login-Challenge-Flow gültig). Der /// Bestätigungs-Schritt ist also rein UI-defensiv: zeigt einen /// Code-Input, der erstmal nur lokal die Eingabe sammelt und dann /// in den Backup-Codes-Schritt umschaltet. public func confirmVerify() { if case let .verify(_, codes) = phase { verifyCode = "" phase = .backupCodes(codes) status = .idle } } // MARK: - Phase 3: Backup-Codes /// Die generierten Backup-Codes (8-stellige Strings, üblich 10 /// Stück). UI zeigt sie zum Kopieren/Sichern. public var backupCodes: [String] { if case let .backupCodes(codes) = phase { return codes } if case let .verify(_, codes) = phase { return codes } return [] } }