import Foundation /// DTO helper structures matching Go backend API schema private struct BackendResponse: Decodable { let code: Int let message: String let data: T? } private struct BackendAuthData: Decodable { let user: BackendUser let token: String } private struct BackendUser: Decodable { let id: String let username: String let email: String? let phoneNumber: String? let avatarURL: String? let registeredAt: Date func toUserAccount(token: String) -> UserAccount { return UserAccount( id: id, username: username, email: email, phoneNumber: phoneNumber, avatarURL: avatarURL, token: token, registeredAt: registeredAt ) } } /// Remote server adapter implementing `AuthServiceProtocol`. /// Connects to the CelestiaTrace Go backend server at https://celestia-trace.ccdw.life/v1 final class RemoteAuthServiceStub: AuthServiceProtocol { private let baseURL: URL private let session: URLSession private var cachedUser: UserAccount? init(baseURL: URL = URL(string: "https://celestia-trace.ccdw.life/v1")!, session: URLSession = .shared) { self.baseURL = baseURL self.session = session } private var decoder: JSONDecoder { let dec = JSONDecoder() dec.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let dateStr = try container.decode(String.self) let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = formatter.date(from: dateStr) { return date } formatter.formatOptions = [.withInternetDateTime] if let date = formatter.date(from: dateStr) { return date } throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateStr)") } return dec } func login(request: LoginRequest, completion: @escaping (AuthResult) -> Void) { let endpoint = baseURL.appendingPathComponent("auth/login") var urlReq = URLRequest(url: endpoint) urlReq.httpMethod = "POST" urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: Any] = [ "identifier": request.identifier, "password": request.password ] urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body) performRequest(urlReq, completion: completion) } func register(request: RegisterRequest, completion: @escaping (AuthResult) -> Void) { let endpoint = baseURL.appendingPathComponent("auth/register") var urlReq = URLRequest(url: endpoint) urlReq.httpMethod = "POST" urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: Any] = [ "username": request.username, "identifier": request.identifier, "password": request.password ] urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body) performRequest(urlReq, completion: completion) } func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult) -> Void) { guard let token = cachedUser?.token else { completion(.failure(.userNotFound)) return } let endpoint = baseURL.appendingPathComponent("user/profile") var urlReq = URLRequest(url: endpoint) urlReq.httpMethod = "PUT" urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type") urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") var body: [String: Any] = [:] if let username = request.username { body["username"] = username } if let email = request.email { body["email"] = email } if let phone = request.phoneNumber { body["phoneNumber"] = phone } if let avatar = request.avatarURL { body["avatarURL"] = avatar } urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body) let task = session.dataTask(with: urlReq) { [weak self] data, response, error in guard let self = self else { return } if let error = error { completion(.failure(.networkError(error.localizedDescription))) return } guard let data = data else { completion(.failure(.networkError("未收到服务器响应数据"))) return } do { let resp = try self.decoder.decode(BackendResponse.self, from: data) if resp.code == 0, let bUser = resp.data { let userAccount = bUser.toUserAccount(token: token) self.cachedUser = userAccount completion(.success(userAccount)) } else { completion(.failure(.networkError(resp.message))) } } catch { completion(.failure(.networkError("响应解析失败: \(error.localizedDescription)"))) } } task.resume() } func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult) -> Void) { guard let token = cachedUser?.token else { completion(.failure(.userNotFound)) return } let endpoint = baseURL.appendingPathComponent("user/change-password") var urlReq = URLRequest(url: endpoint) urlReq.httpMethod = "POST" urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type") urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let body: [String: Any] = [ "oldPassword": request.oldPassword, "newPassword": request.newPassword ] urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body) let task = session.dataTask(with: urlReq) { data, response, error in if let error = error { completion(.failure(.networkError(error.localizedDescription))) return } guard let data = data else { completion(.failure(.networkError("未收到服务器响应数据"))) return } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let code = json["code"] as? Int { if code == 0 { completion(.success(true)) } else { let msg = json["message"] as? String ?? "密码修改失败" completion(.failure(.incorrectPassword)) } } else { completion(.failure(.networkError("非法服务器响应格式"))) } } catch { completion(.failure(.networkError(error.localizedDescription))) } } task.resume() } func logout(completion: @escaping () -> Void) { if let token = cachedUser?.token { let endpoint = baseURL.appendingPathComponent("auth/logout") var urlReq = URLRequest(url: endpoint) urlReq.httpMethod = "POST" urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: urlReq) { _, _, _ in } task.resume() } cachedUser = nil completion() } func getCurrentUser() -> UserAccount? { return cachedUser } private func performRequest(_ urlReq: URLRequest, completion: @escaping (AuthResult) -> Void) { let task = session.dataTask(with: urlReq) { [weak self] data, response, error in guard let self = self else { return } if let error = error { completion(.failure(.networkError(error.localizedDescription))) return } guard let data = data else { completion(.failure(.networkError("未收到服务器响应数据"))) return } do { let resp = try self.decoder.decode(BackendResponse.self, from: data) if resp.code == 0, let authData = resp.data { let userAccount = authData.user.toUserAccount(token: authData.token) self.cachedUser = userAccount completion(.success(userAccount)) } else { if resp.code == 409 { completion(.failure(.userAlreadyExists)) } else if resp.code == 401 { completion(.failure(.invalidCredentials)) } else { completion(.failure(.networkError(resp.message))) } } } catch { completion(.failure(.networkError("响应解析失败: \(error.localizedDescription)"))) } } task.resume() } }