Files
boc/aamos-admin-upgrade/ios/AamosAdmin/Services/AuthService.swift
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- 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
2026-07-08 19:56:03 +00:00

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)."
}
}
}