|
|
@@ -1,47 +1,243 @@
|
|
|
import Foundation
|
|
|
|
|
|
-/// Remote server adapter placeholder implementing `AuthServiceProtocol`.
|
|
|
-/// This stub demonstrates how `CelestiaTrace` can switch to a real remote backend server in the future.
|
|
|
+/// 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://api.celestiatrace.com/v1")!, session: URLSession = .shared) {
|
|
|
+ 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) {
|
|
|
- // TODO: Replace stub with URLSession POST request to `/auth/login`
|
|
|
- // let endpoint = baseURL.appendingPathComponent("auth/login")
|
|
|
- // var urlReq = URLRequest(url: endpoint)
|
|
|
- // urlReq.httpMethod = "POST"
|
|
|
- // ...
|
|
|
+ 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)
|
|
|
|
|
|
- completion(.failure(.networkError("未配置实际远程服务器地址")))
|
|
|
+ performRequest(urlReq, completion: completion)
|
|
|
}
|
|
|
|
|
|
func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
|
|
|
- // TODO: Replace stub with URLSession POST request to `/auth/register`
|
|
|
- completion(.failure(.networkError("未配置实际远程服务器地址")))
|
|
|
+ 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) {
|
|
|
- // TODO: Replace stub with URLSession PUT request to `/user/profile`
|
|
|
- completion(.failure(.networkError("未配置实际远程服务器地址")))
|
|
|
+ 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) {
|
|
|
- // TODO: Replace stub with URLSession POST request to `/user/change-password`
|
|
|
- completion(.failure(.networkError("未配置实际远程服务器地址")))
|
|
|
+ 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) {
|
|
|
- // TODO: Call remote `/auth/logout` endpoint if needed
|
|
|
+ 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 nil
|
|
|
+ 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()
|
|
|
}
|
|
|
}
|