Files
KisaniCal/KisaniCal/Managers/AuthManager.swift

123 lines
3.9 KiB
Swift
Raw Normal View History

import SwiftUI
import AuthenticationServices
import Security
// MARK: - User model
struct AppUser: Codable, Equatable {
let id: String
var email: String?
var displayName: String?
}
// MARK: - Auth Manager
@MainActor
final class AuthManager: NSObject, ObservableObject {
static let shared = AuthManager()
@Published var currentUser: AppUser?
@Published var isLoading = false
@Published var authError: String?
private let keychainUser = "kisani.auth.user.v1"
private let keychainEmail = "kisani.auth.apple.email."
override init() {
super.init()
currentUser = loadUser()
}
var isAuthenticated: Bool { currentUser != nil }
// MARK: - Sign in with Apple
func handleApple(_ result: Result<ASAuthorization, Error>) {
authError = nil
switch result {
case .success(let auth):
guard let cred = auth.credential as? ASAuthorizationAppleIDCredential else { return }
// Apple only provides email + name on the very first sign-in cache them
var email = cred.email ?? loadAppleEmail(for: cred.user)
if let e = cred.email { saveAppleEmail(e, for: cred.user) }
let fullName = [cred.fullName?.givenName, cred.fullName?.familyName]
.compactMap { $0 }.joined(separator: " ")
let user = AppUser(
id: cred.user,
email: email,
displayName: fullName.isEmpty ? nil : fullName
)
persist(user)
case .failure(let err):
if (err as NSError).code != ASAuthorizationError.canceled.rawValue {
authError = err.localizedDescription
}
}
}
// MARK: - Profile
func updateDisplayName(_ name: String) {
guard var user = currentUser, !name.trimmingCharacters(in: .whitespaces).isEmpty else { return }
user.displayName = name.trimmingCharacters(in: .whitespaces)
persist(user)
}
// MARK: - Sign Out
func signOut() {
currentUser = nil
deleteKeychain(key: keychainUser)
}
// MARK: - Keychain
private func persist(_ user: AppUser) {
currentUser = user
if let data = try? JSONEncoder().encode(user) {
saveKeychain(key: keychainUser, data: data)
}
}
private func loadUser() -> AppUser? {
guard let data = readKeychain(key: keychainUser) else { return nil }
return try? JSONDecoder().decode(AppUser.self, from: data)
}
private func saveKeychain(key: String, data: Data) {
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key, kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock]
SecItemDelete(q as CFDictionary)
SecItemAdd(q as CFDictionary, nil)
}
private func readKeychain(key: String) -> Data? {
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne]
var ref: AnyObject?
SecItemCopyMatching(q as CFDictionary, &ref)
return ref as? Data
}
private func deleteKeychain(key: String) {
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key]
SecItemDelete(q as CFDictionary)
}
// Apple only provides email on first sign-in store it ourselves
private func saveAppleEmail(_ email: String, for uid: String) {
saveKeychain(key: keychainEmail + uid, data: Data(email.utf8))
}
private func loadAppleEmail(for uid: String) -> String? {
guard let d = readKeychain(key: keychainEmail + uid) else { return nil }
return String(data: d, encoding: .utf8)
}
}