| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- 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<UserAccount?>) -> 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<UserAccount>) -> Void) {
- performAuthentication(path: "auth/login", body: LoginBody(identifier: request.identifier, password: request.password), completion: completion)
- }
- func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> 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<UserAccount>) -> 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<Bool>) -> 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<Body: Encodable>(
- path: String,
- body: Body,
- completion: @escaping (AuthResult<UserAccount>) -> 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)
- }
- }
|