v0.4.0 — ManaTwoFactorEnrollView + ManaTwoFactorDisableView
3-Phasen-Wizard für 2FA-Enrollment + Single-Step-Sheet für Disable. Setzt mana-swift-core ≥ 1.4.0 voraus. ManaTwoFactorEnrollView: 1. Passwort-Re-Auth → server liefert otpauth-URI + Backup-Codes 2. QR-Code-Display (CoreImage.CIFilter.qrCodeGenerator) + 6-stellige Test-Code-Eingabe 3. Backup-Codes-Liste mit Copy-to-Clipboard ManaTwoFactorDisableView: - Re-Auth via Passwort, destructive-Button, .done-Konfirmation 5 neue Tests, 44/44 grün. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1555565b6
commit
dc8e5a4e9b
4 changed files with 595 additions and 0 deletions
107
Sources/ManaAuthUI/TwoFactor/TwoFactorEnrollmentViewModel.swift
Normal file
107
Sources/ManaAuthUI/TwoFactor/TwoFactorEnrollmentViewModel.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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 []
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue