108 lines
3.1 KiB
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
|
|
} |