| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import Foundation
- /// DTO helper structures matching Go backend API schema
- private struct BackendResponse<T: Decodable>: 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<UserAccount>) -> 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<UserAccount>) -> 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<UserAccount>) -> 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<BackendUser>.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<Bool>) -> 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<UserAccount>) -> 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<BackendAuthData>.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()
- }
- }
|