Closer/iphone/Closer/Core/Auth/AuthService.swift

177 lines
6.1 KiB
Swift

import Foundation
import FirebaseAuth
import FirebaseCore
import GoogleSignIn
// MARK: - Auth Service
final class AuthService: NSObject, @unchecked Sendable {
static let shared = AuthService()
private let auth = Auth.auth()
private let limiter = AuthRateLimiter.shared
private override init() {
super.init()
}
// MARK: - Auth State
var currentUserId: String? { auth.currentUser?.uid }
var currentUserEmail: String? { auth.currentUser?.email }
var isSignedIn: Bool { auth.currentUser != nil }
var isAnonymous: Bool { auth.currentUser?.isAnonymous ?? false }
var isGoogleAccount: Bool {
auth.currentUser?.providerData.contains { $0.providerID == GoogleAuthProviderID } ?? false
}
/// Observe auth state changes as an async sequence
func authStateStream() -> AsyncStream<AuthState> {
AsyncStream { continuation in
let listener = Auth.auth().addStateDidChangeListener { _, user in
if let user = user {
continuation.yield(.authenticated(userId: user.uid, isAnonymous: user.isAnonymous))
} else {
continuation.yield(.unauthenticated)
}
}
continuation.onTermination = { _ in
Auth.auth().removeStateDidChangeListener(listener)
}
}
}
// MARK: - Auth Operations
func signInAnonymously() async throws -> String {
_ = limiter.recordFailure(.anonymous)
guard limiter.timeUntilNextAttempt(.anonymous) == 0 else {
throw AuthError.throttled(limiter.throttleMessage(.anonymous) ?? "Too many attempts. Please wait.")
}
let result = try await auth.signInAnonymously()
limiter.recordSuccess(.anonymous)
return result.user.uid
}
func signInWithEmail(_ email: String, password: String) async throws -> String {
guard limiter.timeUntilNextAttempt(.login) == 0 else {
throw AuthError.throttled(limiter.throttleMessage(.login) ?? "Too many attempts. Please wait.")
}
do {
let result = try await auth.signIn(withEmail: email, password: password)
limiter.recordSuccess(.login)
return result.user.uid
} catch {
limiter.recordFailure(.login)
throw AuthError.map(error)
}
}
func signUpWithEmail(_ email: String, password: String) async throws -> String {
guard limiter.timeUntilNextAttempt(.signUp) == 0 else {
throw AuthError.throttled(limiter.throttleMessage(.signUp) ?? "Too many attempts. Please wait.")
}
do {
let result = try await auth.createUser(withEmail: email, password: password)
limiter.recordSuccess(.signUp)
return result.user.uid
} catch {
limiter.recordFailure(.signUp)
throw AuthError.map(error)
}
}
func sendPasswordResetEmail(_ email: String) async throws {
guard limiter.timeUntilNextAttempt(.passwordReset) == 0 else {
throw AuthError.throttled(limiter.throttleMessage(.passwordReset) ?? "Too many attempts. Please wait.")
}
do {
try await auth.sendPasswordResetEmail(withEmail: email)
limiter.recordSuccess(.passwordReset)
} catch {
limiter.recordFailure(.passwordReset)
throw AuthError.map(error)
}
}
func signInWithGoogle(idToken: String, accessToken: String) async throws -> GoogleSignInResult {
let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: accessToken)
let result = try await auth.signIn(with: credential)
let user = result.user
return GoogleSignInResult(
uid: user.uid,
displayName: user.displayName ?? "",
photoUrl: user.photoURL?.absoluteString ?? "",
email: user.email ?? "",
isAnonymous: user.isAnonymous
)
}
func signOut() throws {
try auth.signOut()
}
func reauthenticateWithEmail(_ email: String, password: String) async throws {
guard let currentUser = auth.currentUser else {
throw AuthError.notSignedIn
}
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
try await currentUser.reauthenticate(with: credential)
}
func deleteAccount() async throws {
guard let currentUser = auth.currentUser else {
throw AuthError.notSignedIn
}
try await currentUser.delete()
}
}
// MARK: - Auth Errors
enum AuthError: LocalizedError {
case throttled(String)
case notSignedIn
case networkError
case invalidCredential
case emailAlreadyInUse
case weakPassword
case userNotFound
case unknown(Error)
static func map(_ error: Error) -> AuthError {
let nsError = error as NSError
switch nsError.code {
case AuthErrorCode.networkError.rawValue:
return .networkError
case AuthErrorCode.invalidCredential.rawValue:
return .invalidCredential
case AuthErrorCode.emailAlreadyInUse.rawValue:
return .emailAlreadyInUse
case AuthErrorCode.weakPassword.rawValue:
return .weakPassword
case AuthErrorCode.userNotFound.rawValue:
return .userNotFound
default:
return .unknown(error)
}
}
var errorDescription: String? {
switch self {
case .throttled(let msg): return msg
case .notSignedIn: return "No user is signed in."
case .networkError: return "Network error. Please check your connection."
case .invalidCredential: return "Invalid email or password."
case .emailAlreadyInUse: return "This email is already registered."
case .weakPassword: return "Password must be at least 6 characters."
case .userNotFound: return "No account found with this email."
case .unknown(let error): return error.localizedDescription
}
}
}