Files
pos-system/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift

344 lines
10 KiB
Swift

//
// AuthManager.swift
// AppClientBaseSwift
//
// Authentication state management
// Qun lý trng thái xác thc
//
import Foundation
import Security
import Combine
// MARK: - Auth State
// Trng thái xác thc
/// Authentication state enumeration
/// Enum trng thái xác thc
enum AuthState: Equatable {
case unknown
case unauthenticated
case authenticated(User)
var isAuthenticated: Bool {
if case .authenticated = self {
return true
}
return false
}
var user: User? {
if case .authenticated(let user) = self {
return user
}
return nil
}
}
// MARK: - Auth Manager
// Qun lý xác thc
/// Main authentication manager
/// Qun lý xác thc chính
final class AuthManager: ObservableObject {
// MARK: - Properties
/// Shared singleton instance
/// Instance singleton dùng chung
@MainActor static let shared = AuthManager()
/// Current authentication state
/// Trng thái xác thc hin ti
@MainActor @Published private(set) var authState: AuthState = .unknown
/// Access token from Keychain
/// Access token t Keychain
@MainActor var accessToken: String? {
KeychainHelper.read(key: StorageKeys.accessToken)
}
/// Refresh token from Keychain
/// Refresh token t Keychain
@MainActor var refreshToken: String? {
KeychainHelper.read(key: StorageKeys.refreshToken)
}
/// Whether user is currently authenticated
/// Ngưi dùng có đang xác thc không
@MainActor var isAuthenticated: Bool {
authState.isAuthenticated
}
/// Current authenticated user
/// Ngưi dùng đã xác thc hin ti
@MainActor var currentUser: User? {
authState.user
}
// MARK: - Init
private init() {}
// MARK: - Public Methods
/// Set authenticated state with user (for mock login)
/// Đt trng thái authenticated vi user (cho mock login)
@MainActor func setAuthenticated(user: User) {
authState = .authenticated(user)
}
// MARK: - Public Methods
/// Initialize auth state on app launch
/// Khi to trng thái xác thc khi app khi đng
@MainActor func initialize() async {
guard accessToken != nil else {
authState = .unauthenticated
return
}
// Try to load cached user
// Th ti user đã cache
if let userData = UserDefaults.standard.data(forKey: StorageKeys.userData),
let user = try? JSONDecoder().decode(User.self, from: userData)
{
authState = .authenticated(user)
} else {
// Fetch user from API
// Ly user t API
await refreshCurrentUser()
}
}
/// Login with email and password using OAuth2 Password Grant
/// Đăng nhp vi email và mt khu s dng OAuth2 Password Grant
/// - Parameters:
/// - email: User email / Email ngưi dùng
/// - password: User password / Mt khu ngưi dùng
@MainActor func login(email: String, password: String) async throws {
// OAuth2 Password Grant
// OAuth2 Password Grant
let formData: [String: String] = [
"grant_type": "password",
"client_id": APIConfig.oauthClientId,
"client_secret": APIConfig.oauthClientSecret,
"username": email,
"password": password,
"scope": APIConfig.oauthScope
]
let tokenResponse: OAuthTokenResponse = try await APIService.shared.postForm(
endpoint: APIConfig.tokenEndpoint,
formData: formData
)
// Save tokens to Keychain
// Lưu tokens vào Keychain
KeychainHelper.save(key: StorageKeys.accessToken, value: tokenResponse.accessToken)
if let refreshToken = tokenResponse.refreshToken {
KeychainHelper.save(key: StorageKeys.refreshToken, value: refreshToken)
}
// Fetch user info from API
// Ly thông tin user t API
await fetchCurrentUser()
}
/// Register new user
/// Đăng ký ngưi dùng mi
/// - Parameters:
/// - firstName: User first name / Tên ngưi dùng
/// - lastName: User last name / H ngưi dùng
/// - email: User email / Email ngưi dùng
/// - password: User password / Mt khu ngưi dùng
@MainActor func register(firstName: String, lastName: String, email: String, password: String) async throws {
struct RegisterRequest: Encodable {
let firstName: String
let lastName: String
let email: String
let password: String
}
struct RegisterResponse: Decodable {
let success: Bool
let data: RegisterData?
}
struct RegisterData: Decodable {
let userId: String
let email: String
}
let request = RegisterRequest(
firstName: firstName,
lastName: lastName,
email: email,
password: password
)
let _: RegisterResponse = try await APIService.shared.post(
endpoint: "/auth/register",
body: request
)
// Auto login after successful registration
// T đng đăng nhp sau khi đăng ký thành công
try await login(email: email, password: password)
}
/// Logout current user
/// Đăng xut ngưi dùng hin ti
@MainActor func logout() {
// Clear tokens from Keychain
// Xóa tokens khi Keychain
KeychainHelper.delete(key: StorageKeys.accessToken)
KeychainHelper.delete(key: StorageKeys.refreshToken)
// Clear cached user data
// Xóa d liu user đã cache
UserDefaults.standard.removeObject(forKey: StorageKeys.userData)
authState = .unauthenticated
}
/// Handle unauthorized response (401)
/// X lý response unauthorized (401)
@MainActor func handleUnauthorized() {
// Try refresh token first, then logout
// Th refresh token trưc, sau đó logout
Task {
let success = await refreshTokens()
if !success {
logout()
}
}
}
/// Fetch current user from API
/// Ly thông tin user hin ti t API
@MainActor func fetchCurrentUser() async {
do {
let user: User = try await APIService.shared.get(endpoint: "/users/me")
// Cache user data
// Cache d liu user
if let userData = try? JSONEncoder().encode(user) {
UserDefaults.standard.set(userData, forKey: StorageKeys.userData)
}
authState = .authenticated(user)
} catch {
print("Failed to fetch user: \(error)")
authState = .unauthenticated
}
}
/// Refresh current user from cache or API
/// Làm mi thông tin user t cache hoc API
@MainActor func refreshCurrentUser() async {
await fetchCurrentUser()
}
// MARK: - Private Methods
/// Refresh access token using OAuth2 refresh_token grant
/// Làm mi access token s dng OAuth2 refresh_token grant
/// - Returns: Whether refresh was successful / Refresh có thành công không
@MainActor private func refreshTokens() async -> Bool {
guard let refreshToken = refreshToken else {
return false
}
// OAuth2 Refresh Token Grant
// OAuth2 Refresh Token Grant
let formData: [String: String] = [
"grant_type": "refresh_token",
"refresh_token": refreshToken
]
do {
let response: OAuthTokenResponse = try await APIService.shared.postForm(
endpoint: APIConfig.tokenEndpoint,
formData: formData
)
// Save new tokens
// Lưu tokens mi
KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken)
if let newRefreshToken = response.refreshToken {
KeychainHelper.save(key: StorageKeys.refreshToken, value: newRefreshToken)
}
return true
} catch {
print("Token refresh failed: \(error)")
return false
}
}
}
// MARK: - Keychain Helper
// Helper Keychain
/// Helper for Keychain operations
/// Helper cho các thao tác Keychain
enum KeychainHelper {
/// Save value to Keychain
/// Lưu giá tr vào Keychain
static func save(key: String, value: String) {
guard let data = value.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key,
kSecValueData as String: data,
]
// Delete existing
// Xóa existing
SecItemDelete(query as CFDictionary)
// Add new
// Thêm mi
SecItemAdd(query as CFDictionary, nil)
}
/// Read value from Keychain
/// Đc giá tr t Keychain
static func read(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
guard status == errSecSuccess,
let data = dataTypeRef as? Data,
let value = String(data: data, encoding: .utf8)
else {
return nil
}
return value
}
/// Delete value from Keychain
/// Xóa giá tr khi Keychain
static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}