344 lines
10 KiB
Swift
344 lines
10 KiB
Swift
//
|
|
// AuthManager.swift
|
|
// AppClientBaseSwift
|
|
//
|
|
// Authentication state management
|
|
// Quản lý trạng thái xác thực
|
|
//
|
|
|
|
import Foundation
|
|
import Security
|
|
import Combine
|
|
|
|
// MARK: - Auth State
|
|
// Trạng thái xác thực
|
|
|
|
/// Authentication state enumeration
|
|
/// Enum trạng thái xác thực
|
|
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
|
|
// Quản lý xác thực
|
|
|
|
/// Main authentication manager
|
|
/// Quản lý xác thực chính
|
|
final class AuthManager: ObservableObject {
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Shared singleton instance
|
|
/// Instance singleton dùng chung
|
|
@MainActor static let shared = AuthManager()
|
|
|
|
/// Current authentication state
|
|
/// Trạng thái xác thực hiện tại
|
|
@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 thực không
|
|
@MainActor var isAuthenticated: Bool {
|
|
authState.isAuthenticated
|
|
}
|
|
|
|
/// Current authenticated user
|
|
/// Người dùng đã xác thực hiện tại
|
|
@MainActor var currentUser: User? {
|
|
authState.user
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Set authenticated state with user (for mock login)
|
|
/// Đặt trạng thái authenticated với user (cho mock login)
|
|
@MainActor func setAuthenticated(user: User) {
|
|
authState = .authenticated(user)
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Initialize auth state on app launch
|
|
/// Khởi tạo trạng thái xác thực khi app khởi động
|
|
@MainActor func initialize() async {
|
|
guard accessToken != nil else {
|
|
authState = .unauthenticated
|
|
return
|
|
}
|
|
|
|
// Try to load cached user
|
|
// Thử tải 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
|
|
// Lấy user từ API
|
|
await refreshCurrentUser()
|
|
}
|
|
}
|
|
|
|
/// Login with email and password using OAuth2 Password Grant
|
|
/// Đăng nhập với email và mật khẩu sử dụng OAuth2 Password Grant
|
|
/// - Parameters:
|
|
/// - email: User email / Email người dùng
|
|
/// - password: User password / Mật khẩu 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
|
|
// Lấy thông tin user từ API
|
|
await fetchCurrentUser()
|
|
}
|
|
|
|
/// Register new user
|
|
/// Đăng ký người dùng mới
|
|
/// - 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 / Mật khẩu 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 nhập sau khi đăng ký thành công
|
|
try await login(email: email, password: password)
|
|
}
|
|
|
|
/// Logout current user
|
|
/// Đăng xuất người dùng hiện tại
|
|
@MainActor func logout() {
|
|
// Clear tokens from Keychain
|
|
// Xóa tokens khỏi Keychain
|
|
KeychainHelper.delete(key: StorageKeys.accessToken)
|
|
KeychainHelper.delete(key: StorageKeys.refreshToken)
|
|
|
|
// Clear cached user data
|
|
// Xóa dữ liệu 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
|
|
/// Lấy thông tin user hiện tại từ API
|
|
@MainActor func fetchCurrentUser() async {
|
|
do {
|
|
let user: User = try await APIService.shared.get(endpoint: "/users/me")
|
|
|
|
// Cache user data
|
|
// Cache dữ liệu 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 mới thông tin user từ cache hoặc API
|
|
@MainActor func refreshCurrentUser() async {
|
|
await fetchCurrentUser()
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/// Refresh access token using OAuth2 refresh_token grant
|
|
/// Làm mới access token sử dụng 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 mới
|
|
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 mới
|
|
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ị khỏi 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)
|
|
}
|
|
}
|