import Foundation private struct AuthPayload: Decodable { let user: UserAccount let token: String let refreshToken: String let expiresAt: Date } private struct LoginBody: Encodable { let identifier: String let password: String } private struct RegisterBody: Encodable { let username: String let identifier: String let password: String } private struct UpdateProfileBody: Encodable { let username: String? let email: String? let phoneNumber: String? let avatarURL: String? } private struct ChangePasswordBody: Encodable { let oldPassword: String let newPassword: String } final class RemoteAuthService: AuthServiceProtocol { private let client: APIClient private let profileKey = "com.celestia.trace.cached_server_profile" private var cachedUser: UserAccount? init(client: APIClient = .shared) { self.client = client // Remove credentials created by the retired local-only auth repository. // These keys never contained a valid server session. UserDefaults.standard.removeObject(forKey: "com.celestia.trace.registered_users_db") UserDefaults.standard.removeObject(forKey: "com.celestia.trace.current_user_session") if let data = UserDefaults.standard.data(forKey: profileKey) { cachedUser = try? JSONDecoder().decode(UserAccount.self, from: data) } } func restoreSession(completion: @escaping (AuthResult) -> Void) { Task { guard await client.hasStoredSession() else { clearCachedProfile() completion(.success(nil)) return } do { let user: UserAccount = try await client.request("user/profile", authenticated: true) cache(user) completion(.success(user)) } catch APIError.transport { completion(.success(cachedUser)) } catch { await client.clearSession() clearCachedProfile() completion(.failure(map(error))) } } } func login(request: LoginRequest, completion: @escaping (AuthResult) -> Void) { performAuthentication(path: "auth/login", body: LoginBody(identifier: request.identifier, password: request.password), completion: completion) } func register(request: RegisterRequest, completion: @escaping (AuthResult) -> Void) { performAuthentication( path: "auth/register", body: RegisterBody(username: request.username, identifier: request.identifier, password: request.password), completion: completion ) } func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult) -> Void) { Task { do { let body = UpdateProfileBody(username: request.username, email: request.email, phoneNumber: request.phoneNumber, avatarURL: request.avatarURL) let user: UserAccount = try await client.request("user/profile", method: .put, body: try JSONEncoder().encode(body), authenticated: true) cache(user) completion(.success(user)) } catch { completion(.failure(map(error))) } } } func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult) -> Void) { Task { do { let body = ChangePasswordBody(oldPassword: request.oldPassword, newPassword: request.newPassword) try await client.requestVoid("user/change-password", method: .post, body: try JSONEncoder().encode(body), authenticated: true) completion(.success(true)) } catch { completion(.failure(map(error))) } } } func logout(completion: @escaping () -> Void) { Task { try? await client.requestVoid("auth/logout", method: .post, authenticated: true) await client.clearSession() clearCachedProfile() completion() } } func getCurrentUser() -> UserAccount? { cachedUser } private func performAuthentication( path: String, body: Body, completion: @escaping (AuthResult) -> Void ) { Task { do { let payload: AuthPayload = try await client.request(path, method: .post, body: try JSONEncoder().encode(body)) try await client.storeTokens(accessToken: payload.token, refreshToken: payload.refreshToken, expiresAt: payload.expiresAt) cache(payload.user) completion(.success(payload.user)) } catch { completion(.failure(map(error))) } } } private func cache(_ user: UserAccount) { cachedUser = user if let data = try? JSONEncoder().encode(user) { UserDefaults.standard.set(data, forKey: profileKey) } } private func clearCachedProfile() { cachedUser = nil UserDefaults.standard.removeObject(forKey: profileKey) } private func map(_ error: Error) -> AuthError { if case APIError.unauthorized = error { return .invalidCredentials } if case APIError.server(let code, _) = error { if code == 409 { return .userAlreadyExists } if code == 401 { return .invalidCredentials } } return .networkError(error.localizedDescription) } }