6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
100 lines
2.6 KiB
Swift
100 lines
2.6 KiB
Swift
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)."
|
|
}
|
|
}
|
|
}
|