53 lines
1.9 KiB
Swift
53 lines
1.9 KiB
Swift
|
|
import Foundation
|
||
|
|
import LocalAuthentication
|
||
|
|
|
||
|
|
enum AuthenticationError: LocalizedError {
|
||
|
|
case biometricsNotAvailable
|
||
|
|
case authenticationFailed(String)
|
||
|
|
case userCancelled
|
||
|
|
|
||
|
|
var errorDescription: String? {
|
||
|
|
switch self {
|
||
|
|
case .biometricsNotAvailable:
|
||
|
|
return "Touch ID is not available on this device."
|
||
|
|
case .authenticationFailed(let reason):
|
||
|
|
return "Authentication failed: \(reason)"
|
||
|
|
case .userCancelled:
|
||
|
|
return "Authentication was cancelled."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct AuthenticationService: Sendable {
|
||
|
|
|
||
|
|
/// Attempts biometric authentication (Touch ID) before proceeding.
|
||
|
|
/// Falls back gracefully if Touch ID is not available.
|
||
|
|
/// Returns true if authenticated, false if biometrics unavailable (will proceed to admin dialog).
|
||
|
|
func authenticateWithBiometrics() async throws -> Bool {
|
||
|
|
let context = LAContext()
|
||
|
|
var error: NSError?
|
||
|
|
|
||
|
|
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
||
|
|
// Touch ID not available — skip biometric step, will use admin dialog
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
do {
|
||
|
|
let success = try await context.evaluatePolicy(
|
||
|
|
.deviceOwnerAuthenticationWithBiometrics,
|
||
|
|
localizedReason: "Authenticate to modify system TTL value"
|
||
|
|
)
|
||
|
|
return success
|
||
|
|
} catch let authError as LAError {
|
||
|
|
switch authError.code {
|
||
|
|
case .userCancel, .appCancel:
|
||
|
|
throw AuthenticationError.userCancelled
|
||
|
|
case .userFallback:
|
||
|
|
// User chose to use password — allow proceeding to admin dialog
|
||
|
|
return true
|
||
|
|
default:
|
||
|
|
throw AuthenticationError.authenticationFailed(authError.localizedDescription)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|