RemoteAuthServiceStub.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import Foundation
  2. /// DTO helper structures matching Go backend API schema
  3. private struct BackendResponse<T: Decodable>: Decodable {
  4. let code: Int
  5. let message: String
  6. let data: T?
  7. }
  8. private struct BackendAuthData: Decodable {
  9. let user: BackendUser
  10. let token: String
  11. }
  12. private struct BackendUser: Decodable {
  13. let id: String
  14. let username: String
  15. let email: String?
  16. let phoneNumber: String?
  17. let avatarURL: String?
  18. let registeredAt: Date
  19. func toUserAccount(token: String) -> UserAccount {
  20. return UserAccount(
  21. id: id,
  22. username: username,
  23. email: email,
  24. phoneNumber: phoneNumber,
  25. avatarURL: avatarURL,
  26. token: token,
  27. registeredAt: registeredAt
  28. )
  29. }
  30. }
  31. /// Remote server adapter implementing `AuthServiceProtocol`.
  32. /// Connects to the CelestiaTrace Go backend server at https://celestia-trace.ccdw.life/v1
  33. final class RemoteAuthServiceStub: AuthServiceProtocol {
  34. private let baseURL: URL
  35. private let session: URLSession
  36. private var cachedUser: UserAccount?
  37. init(baseURL: URL = URL(string: "https://celestia-trace.ccdw.life/v1")!, session: URLSession = .shared) {
  38. self.baseURL = baseURL
  39. self.session = session
  40. }
  41. private var decoder: JSONDecoder {
  42. let dec = JSONDecoder()
  43. dec.dateDecodingStrategy = .custom { decoder in
  44. let container = try decoder.singleValueContainer()
  45. let dateStr = try container.decode(String.self)
  46. let formatter = ISO8601DateFormatter()
  47. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  48. if let date = formatter.date(from: dateStr) {
  49. return date
  50. }
  51. formatter.formatOptions = [.withInternetDateTime]
  52. if let date = formatter.date(from: dateStr) {
  53. return date
  54. }
  55. throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateStr)")
  56. }
  57. return dec
  58. }
  59. func login(request: LoginRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  60. let endpoint = baseURL.appendingPathComponent("auth/login")
  61. var urlReq = URLRequest(url: endpoint)
  62. urlReq.httpMethod = "POST"
  63. urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
  64. let body: [String: Any] = [
  65. "identifier": request.identifier,
  66. "password": request.password
  67. ]
  68. urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body)
  69. performRequest(urlReq, completion: completion)
  70. }
  71. func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  72. let endpoint = baseURL.appendingPathComponent("auth/register")
  73. var urlReq = URLRequest(url: endpoint)
  74. urlReq.httpMethod = "POST"
  75. urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
  76. let body: [String: Any] = [
  77. "username": request.username,
  78. "identifier": request.identifier,
  79. "password": request.password
  80. ]
  81. urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body)
  82. performRequest(urlReq, completion: completion)
  83. }
  84. func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  85. guard let token = cachedUser?.token else {
  86. completion(.failure(.userNotFound))
  87. return
  88. }
  89. let endpoint = baseURL.appendingPathComponent("user/profile")
  90. var urlReq = URLRequest(url: endpoint)
  91. urlReq.httpMethod = "PUT"
  92. urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
  93. urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  94. var body: [String: Any] = [:]
  95. if let username = request.username { body["username"] = username }
  96. if let email = request.email { body["email"] = email }
  97. if let phone = request.phoneNumber { body["phoneNumber"] = phone }
  98. if let avatar = request.avatarURL { body["avatarURL"] = avatar }
  99. urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body)
  100. let task = session.dataTask(with: urlReq) { [weak self] data, response, error in
  101. guard let self = self else { return }
  102. if let error = error {
  103. completion(.failure(.networkError(error.localizedDescription)))
  104. return
  105. }
  106. guard let data = data else {
  107. completion(.failure(.networkError("未收到服务器响应数据")))
  108. return
  109. }
  110. do {
  111. let resp = try self.decoder.decode(BackendResponse<BackendUser>.self, from: data)
  112. if resp.code == 0, let bUser = resp.data {
  113. let userAccount = bUser.toUserAccount(token: token)
  114. self.cachedUser = userAccount
  115. completion(.success(userAccount))
  116. } else {
  117. completion(.failure(.networkError(resp.message)))
  118. }
  119. } catch {
  120. completion(.failure(.networkError("响应解析失败: \(error.localizedDescription)")))
  121. }
  122. }
  123. task.resume()
  124. }
  125. func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult<Bool>) -> Void) {
  126. guard let token = cachedUser?.token else {
  127. completion(.failure(.userNotFound))
  128. return
  129. }
  130. let endpoint = baseURL.appendingPathComponent("user/change-password")
  131. var urlReq = URLRequest(url: endpoint)
  132. urlReq.httpMethod = "POST"
  133. urlReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
  134. urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  135. let body: [String: Any] = [
  136. "oldPassword": request.oldPassword,
  137. "newPassword": request.newPassword
  138. ]
  139. urlReq.httpBody = try? JSONSerialization.data(withJSONObject: body)
  140. let task = session.dataTask(with: urlReq) { data, response, error in
  141. if let error = error {
  142. completion(.failure(.networkError(error.localizedDescription)))
  143. return
  144. }
  145. guard let data = data else {
  146. completion(.failure(.networkError("未收到服务器响应数据")))
  147. return
  148. }
  149. do {
  150. if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
  151. let code = json["code"] as? Int {
  152. if code == 0 {
  153. completion(.success(true))
  154. } else {
  155. let msg = json["message"] as? String ?? "密码修改失败"
  156. completion(.failure(.incorrectPassword))
  157. }
  158. } else {
  159. completion(.failure(.networkError("非法服务器响应格式")))
  160. }
  161. } catch {
  162. completion(.failure(.networkError(error.localizedDescription)))
  163. }
  164. }
  165. task.resume()
  166. }
  167. func logout(completion: @escaping () -> Void) {
  168. if let token = cachedUser?.token {
  169. let endpoint = baseURL.appendingPathComponent("auth/logout")
  170. var urlReq = URLRequest(url: endpoint)
  171. urlReq.httpMethod = "POST"
  172. urlReq.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  173. let task = session.dataTask(with: urlReq) { _, _, _ in }
  174. task.resume()
  175. }
  176. cachedUser = nil
  177. completion()
  178. }
  179. func getCurrentUser() -> UserAccount? {
  180. return cachedUser
  181. }
  182. private func performRequest(_ urlReq: URLRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  183. let task = session.dataTask(with: urlReq) { [weak self] data, response, error in
  184. guard let self = self else { return }
  185. if let error = error {
  186. completion(.failure(.networkError(error.localizedDescription)))
  187. return
  188. }
  189. guard let data = data else {
  190. completion(.failure(.networkError("未收到服务器响应数据")))
  191. return
  192. }
  193. do {
  194. let resp = try self.decoder.decode(BackendResponse<BackendAuthData>.self, from: data)
  195. if resp.code == 0, let authData = resp.data {
  196. let userAccount = authData.user.toUserAccount(token: authData.token)
  197. self.cachedUser = userAccount
  198. completion(.success(userAccount))
  199. } else {
  200. if resp.code == 409 {
  201. completion(.failure(.userAlreadyExists))
  202. } else if resp.code == 401 {
  203. completion(.failure(.invalidCredentials))
  204. } else {
  205. completion(.failure(.networkError(resp.message)))
  206. }
  207. }
  208. } catch {
  209. completion(.failure(.networkError("响应解析失败: \(error.localizedDescription)")))
  210. }
  211. }
  212. task.resume()
  213. }
  214. }