// Auth/AuthManager.swift import Foundation import SwiftUI import Combine struct EmptyDecodable: Decodable, Sendable { nonisolated init(from decoder: any Decoder) throws {} } @MainActor class AuthManager: ObservableObject { @Published var isAuthenticated = false @Published var isLoading = false @Published var errorMessage: String? @Published var currentUserId: Int = 0 @Published var currentUsername: String = "" @Published var currentUserRole: String = "" @Published var currentDisplayName: String = "" static let shared = AuthManager() private init() {} func restoreSession() async { guard KeychainHelper.get(Constants.Keychain.accessToken) != nil else { isAuthenticated = false return } isLoading = true defer { isLoading = false } do { let response: MeResponseData = try await APIClient.shared.request("/api/v1/auth/me") applyUser(response.user) isAuthenticated = true } catch APIError.notAuthenticated { KeychainHelper.clearAll() isAuthenticated = false } catch { restoreUserFromKeychain() isAuthenticated = true } } func login(username: String, password: String) async { isLoading = true errorMessage = nil defer { isLoading = false } do { let response: LoginResponseData = try await APIClient.shared.post( "/api/v1/auth/login", body: ["username": username, "password": password] ) KeychainHelper.set(response.accessToken, forKey: Constants.Keychain.accessToken) KeychainHelper.set(response.refreshToken, forKey: Constants.Keychain.refreshToken) applyUser(response.user) isAuthenticated = true // Register device immediately after login — the .task {} and // .onChange(scenePhase) paths both miss this case because they run // before login completes. Task { await APIClient.shared.registerDevice() } } catch APIError.serverError(let msg) { errorMessage = msg } catch APIError.networkError { errorMessage = "Cannot reach the server. Please check your connection." } catch { errorMessage = error.localizedDescription } } func logout() async { if let refreshToken = KeychainHelper.get(Constants.Keychain.refreshToken) { _ = try? await APIClient.shared.post( "/api/v1/auth/logout", body: ["refresh_token": refreshToken] ) as EmptyDecodable } KeychainHelper.clearAll() isAuthenticated = false currentUserId = 0 currentUsername = "" currentUserRole = "" currentDisplayName = "" } private func applyUser(_ user: APIUser) { currentUserId = user.id currentUsername = user.username currentUserRole = user.role currentDisplayName = user.displayName KeychainHelper.set(String(user.id), forKey: Constants.Keychain.userId) KeychainHelper.set(user.role, forKey: Constants.Keychain.userRole) KeychainHelper.set(user.username, forKey: Constants.Keychain.username) KeychainHelper.set(user.displayName, forKey: Constants.Keychain.displayName) } private func restoreUserFromKeychain() { currentUserId = Int(KeychainHelper.get(Constants.Keychain.userId) ?? "0") ?? 0 currentUserRole = KeychainHelper.get(Constants.Keychain.userRole) ?? "" currentUsername = KeychainHelper.get(Constants.Keychain.username) ?? "" currentDisplayName = KeychainHelper.get(Constants.Keychain.displayName) ?? "" } }