Closer/iphone/Closer/Models/AuthState.swift

108 lines
3.1 KiB
Swift

import Foundation
// MARK: - Auth State
enum AuthState: Equatable {
case loading
case authenticated(userId: String, isAnonymous: Bool)
case unauthenticated
}
// MARK: - Auth Rate Limiter
/// Per-session client-side rate limiter for Firebase Auth operations.
/// Mirrors the Android AuthRateLimiter.kt implementation.
final class AuthRateLimiter: @unchecked Sendable {
static let shared = AuthRateLimiter()
enum Flow: String, CaseIterable {
case login, signUp, passwordReset, anonymous
}
private struct AttemptState {
var failures: Int = 0
var lockoutEnd: Date?
}
private let lock = NSLock()
private var states: [Flow: AttemptState] = [:]
// Constants matching Android
let softLimitFailures = 3
let maxFailuresBeforeLockout = 5
let lockoutDuration: TimeInterval = 30
let maxBackoff: TimeInterval = 8
let baseBackoff: TimeInterval = 1
let backoffExponentBase: Double = 2.0
private init() {}
// How long to wait before next attempt, in seconds
func timeUntilNextAttempt(_ flow: Flow = .login) -> TimeInterval {
lock.lock()
defer { lock.unlock() }
guard let state = states[flow] else { return 0 }
// Check hard lockout
if let lockoutEnd = state.lockoutEnd, Date() < lockoutEnd {
return lockoutEnd.timeIntervalSinceNow
}
return computeBackoffDelay(state.failures)
}
@discardableResult
func recordFailure(_ flow: Flow = .login) -> TimeInterval {
lock.lock()
defer { lock.unlock() }
var state = states[flow] ?? AttemptState()
state.failures += 1
if state.failures >= maxFailuresBeforeLockout {
state.lockoutEnd = Date().addingTimeInterval(lockoutDuration)
}
states[flow] = state
if let lockoutEnd = state.lockoutEnd, Date() < lockoutEnd {
return lockoutEnd.timeIntervalSinceNow
}
return computeBackoffDelay(state.failures)
}
func recordSuccess(_ flow: Flow = .login) {
lock.lock()
defer { lock.unlock() }
states.removeValue(forKey: flow)
}
var isThrottled: Bool {
timeUntilNextAttempt() > 0
}
func throttleMessage(_ flow: Flow = .login) -> String? {
let wait = timeUntilNextAttempt(flow)
guard wait > 0 else { return nil }
let seconds = Int(ceil(wait))
return "Too many attempts. Try again in \(seconds) second\(seconds == 1 ? "" : "s")."
}
private func computeBackoffDelay(_ failures: Int) -> TimeInterval {
guard failures >= softLimitFailures else { return 0 }
let exponent = failures - softLimitFailures
let raw = baseBackoff * pow(backoffExponentBase, Double(exponent))
return min(raw, maxBackoff)
}
}
// MARK: - Google Sign-In Result
struct GoogleSignInResult {
let uid: String
let displayName: String
let photoUrl: String
let email: String
let isAnonymous: Bool
}