import Foundation import Security import Combine final class AuthService: ObservableObject { static let shared = AuthService() @Published private(set) var isAuthenticated: Bool = false private let service = "com.aamos.admin" private let account = "jwt-token" private init() { isAuthenticated = load() != nil } // MARK: - Public API func save(token: String) throws { guard let data = token.data(using: .utf8) else { throw KeychainError.encodingFailed } delete() let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecValueData: data, kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } DispatchQueue.main.async { self.isAuthenticated = true } } func load() -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data, let token = String(data: data, encoding: .utf8) else { return nil } return token } @discardableResult func delete() -> Bool { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account ] let status = SecItemDelete(query as CFDictionary) let deleted = status == errSecSuccess || status == errSecItemNotFound if deleted { DispatchQueue.main.async { self.isAuthenticated = false } } return deleted } func logout() { delete() } } // MARK: - Errors enum KeychainError: LocalizedError { case encodingFailed case saveFailed(OSStatus) var errorDescription: String? { switch self { case .encodingFailed: return "Failed to encode token as UTF-8 data." case .saveFailed(let status): return "Keychain save failed with OSStatus \(status)." } } }