ManaPasswordStrength (reine Heuristik: Laenge + Zeichenklassen, keine externe Lib) + ManaPasswordStrengthView (segmentierte Anzeige, blendet bei leerem Feld aus, .weak via brand.error). Eingebunden unter den newPassword-Feldern in ManaSignUpView und ManaResetPasswordView. 7 Unit-Tests gegen die Bewertungslogik. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.6 KiB
Swift
98 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
/// Geschätzte Passwortstärke für Sign-Up/Reset-Felder. Heuristik aus
|
|
/// Länge und Zeichenklassen — bewusst simpel, ohne externe Bibliothek
|
|
/// (Invariante 1: pure SwiftUI/Foundation). Kein Ersatz für eine
|
|
/// serverseitige Policy, sondern visuelle Soforthilfe für den Menschen
|
|
/// beim Wählen eines guten Passworts.
|
|
public enum ManaPasswordStrength: Int, Sendable, CaseIterable {
|
|
case weak = 1
|
|
case fair = 2
|
|
case good = 3
|
|
case strong = 4
|
|
|
|
/// Deutsches Label für die Anzeige.
|
|
public var label: String {
|
|
switch self {
|
|
case .weak: "schwach"
|
|
case .fair: "mittel"
|
|
case .good: "gut"
|
|
case .strong: "stark"
|
|
}
|
|
}
|
|
|
|
/// Bewertet ein Passwort. `nil` für leere Eingabe (dann nichts
|
|
/// anzeigen). Unterhalb `minLength` (Default 8, die
|
|
/// mana-auth-Mindestlänge) immer `.weak`, egal wie komplex —
|
|
/// ein zu kurzes Passwort soll nie „gut" wirken.
|
|
///
|
|
/// Punkte: Mindestlänge (+1), ≥12 Zeichen (+1), Groß- **und**
|
|
/// Kleinbuchstaben (+1), Ziffer (+1), Sonderzeichen (+1).
|
|
public static func evaluate(
|
|
_ password: String,
|
|
minLength: Int = 8
|
|
) -> ManaPasswordStrength? {
|
|
guard !password.isEmpty else { return nil }
|
|
guard password.count >= minLength else { return .weak }
|
|
|
|
var score = 1 // Mindestlänge erreicht
|
|
if password.count >= 12 { score += 1 }
|
|
|
|
let hasLower = password.contains { $0.isLowercase }
|
|
let hasUpper = password.contains { $0.isUppercase }
|
|
let hasDigit = password.contains(where: \.isNumber)
|
|
let hasSymbol = password.contains { !$0.isLetter && !$0.isNumber && !$0.isWhitespace }
|
|
|
|
if hasLower, hasUpper { score += 1 }
|
|
if hasDigit { score += 1 }
|
|
if hasSymbol { score += 1 }
|
|
|
|
switch score {
|
|
case ...1: return .weak
|
|
case 2: return .fair
|
|
case 3, 4: return .good
|
|
default: return .strong
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Segmentierte Stärke-Anzeige für unter ein Passwortfeld. Blendet
|
|
/// sich aus, solange das Feld leer ist. Nutzt nur Brand-Semantik:
|
|
/// `.weak` wird per `brand.error` rot markiert, sonst `brand.primary`.
|
|
public struct ManaPasswordStrengthView: View {
|
|
@Environment(\.manaBrand) private var brand
|
|
private let password: String
|
|
private let minLength: Int
|
|
|
|
/// - Parameters:
|
|
/// - password: aktueller Feld-Inhalt.
|
|
/// - minLength: Mindestlänge unterhalb derer immer `.weak` gilt
|
|
/// (Default 8 — passend zu mana-auth).
|
|
public init(password: String, minLength: Int = 8) {
|
|
self.password = password
|
|
self.minLength = minLength
|
|
}
|
|
|
|
public var body: some View {
|
|
if let strength = ManaPasswordStrength.evaluate(password, minLength: minLength) {
|
|
let tint = strength == .weak ? brand.error : brand.primary
|
|
VStack(spacing: 6) {
|
|
HStack(spacing: 4) {
|
|
ForEach(ManaPasswordStrength.allCases, id: \.rawValue) { step in
|
|
Capsule()
|
|
.fill(step.rawValue <= strength.rawValue ? tint : brand.border)
|
|
.frame(height: 4)
|
|
}
|
|
}
|
|
|
|
Text("Passwortstärke: \(strength.label)")
|
|
.font(.caption)
|
|
.foregroundStyle(strength == .weak ? brand.error : brand.mutedForeground)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel("Passwortstärke: \(strength.label)")
|
|
.animation(.easeInOut(duration: 0.2), value: strength)
|
|
}
|
|
}
|
|
}
|