LocalAuthRepository.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import Foundation
  2. /// Internal struct to persist user credentials locally.
  3. private struct LocalUserRecord: Codable {
  4. var user: UserAccount
  5. var passwordHash: String
  6. }
  7. /// Concrete local implementation of `AuthServiceProtocol`.
  8. /// Handles registration, authentication, profile updates, and password changes purely in local storage (UserDefaults / sandbox).
  9. final class LocalAuthRepository: AuthServiceProtocol {
  10. private let usersKey = "com.celestia.trace.registered_users_db"
  11. private let currentUserKey = "com.celestia.trace.current_user_session"
  12. init() {}
  13. // MARK: - AuthServiceProtocol
  14. func login(request: LoginRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  15. let identifier = request.identifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
  16. let password = request.password
  17. guard !identifier.isEmpty, password.count >= 6 else {
  18. completion(.failure(.invalidInput("请输入有效账号与不少于 6 位的密码")))
  19. return
  20. }
  21. var records = loadAllRecords()
  22. // Match identifier by email, phone, or matching username/id
  23. guard let matchingIndex = records.firstIndex(where: { rec in
  24. rec.user.email?.lowercased() == identifier ||
  25. rec.user.phoneNumber == identifier ||
  26. rec.user.username.lowercased() == identifier ||
  27. rec.user.id == identifier
  28. }) else {
  29. // Auto-fallback for demo convenience if no users registered yet, or error out:
  30. completion(.failure(.invalidCredentials))
  31. return
  32. }
  33. let record = records[matchingIndex]
  34. if record.passwordHash == hashPassword(password) {
  35. var updatedUser = record.user
  36. updatedUser.token = "jwt_token_" + UUID().uuidString.prefix(12)
  37. records[matchingIndex].user = updatedUser
  38. saveAllRecords(records)
  39. saveCurrentSession(updatedUser)
  40. completion(.success(updatedUser))
  41. } else {
  42. completion(.failure(.invalidCredentials))
  43. }
  44. }
  45. func register(request: RegisterRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  46. let username = request.username.trimmingCharacters(in: .whitespacesAndNewlines)
  47. let identifier = request.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
  48. let password = request.password
  49. guard !username.isEmpty else {
  50. completion(.failure(.invalidInput("请输入用户名/昵称")))
  51. return
  52. }
  53. guard !identifier.isEmpty else {
  54. completion(.failure(.invalidInput("请输入手机号或邮箱")))
  55. return
  56. }
  57. guard password.count >= 6 else {
  58. completion(.failure(.invalidInput("密码长度不能小于 6 位")))
  59. return
  60. }
  61. var records = loadAllRecords()
  62. let identifierLower = identifier.lowercased()
  63. let alreadyExists = records.contains { rec in
  64. rec.user.email?.lowercased() == identifierLower ||
  65. rec.user.phoneNumber == identifierLower ||
  66. rec.user.username.lowercased() == username.lowercased()
  67. }
  68. if alreadyExists {
  69. completion(.failure(.userAlreadyExists))
  70. return
  71. }
  72. let isEmail = identifier.contains("@")
  73. let newUser = UserAccount(
  74. id: "usr_" + UUID().uuidString.prefix(8),
  75. username: username,
  76. email: isEmail ? identifier : nil,
  77. phoneNumber: isEmail ? nil : identifier,
  78. avatarURL: "preset_avatar_\((records.count % 5) + 1)",
  79. token: "jwt_token_" + UUID().uuidString.prefix(12),
  80. registeredAt: Date()
  81. )
  82. let record = LocalUserRecord(user: newUser, passwordHash: hashPassword(password))
  83. records.append(record)
  84. saveAllRecords(records)
  85. saveCurrentSession(newUser)
  86. completion(.success(newUser))
  87. }
  88. func updateProfile(request: UpdateProfileRequest, completion: @escaping (AuthResult<UserAccount>) -> Void) {
  89. guard var currentUser = getCurrentUser() else {
  90. completion(.failure(.userNotFound))
  91. return
  92. }
  93. if let username = request.username, !username.trimmingCharacters(in: .whitespaces).isEmpty {
  94. currentUser.username = username
  95. }
  96. if let email = request.email {
  97. currentUser.email = email.isEmpty ? nil : email
  98. }
  99. if let phone = request.phoneNumber {
  100. currentUser.phoneNumber = phone.isEmpty ? nil : phone
  101. }
  102. if let avatar = request.avatarURL {
  103. currentUser.avatarURL = avatar
  104. }
  105. var records = loadAllRecords()
  106. if let idx = records.firstIndex(where: { $0.user.id == currentUser.id }) {
  107. records[idx].user = currentUser
  108. saveAllRecords(records)
  109. }
  110. saveCurrentSession(currentUser)
  111. completion(.success(currentUser))
  112. }
  113. func changePassword(request: ChangePasswordRequest, completion: @escaping (AuthResult<Bool>) -> Void) {
  114. guard let currentUser = getCurrentUser() else {
  115. completion(.failure(.userNotFound))
  116. return
  117. }
  118. guard request.newPassword.count >= 6 else {
  119. completion(.failure(.invalidInput("新密码长度不能少于 6 位")))
  120. return
  121. }
  122. var records = loadAllRecords()
  123. guard let idx = records.firstIndex(where: { $0.user.id == currentUser.id }) else {
  124. completion(.failure(.userNotFound))
  125. return
  126. }
  127. if records[idx].passwordHash != hashPassword(request.oldPassword) {
  128. completion(.failure(.incorrectPassword))
  129. return
  130. }
  131. records[idx].passwordHash = hashPassword(request.newPassword)
  132. saveAllRecords(records)
  133. completion(.success(true))
  134. }
  135. func logout(completion: @escaping () -> Void) {
  136. UserDefaults.standard.removeObject(forKey: currentUserKey)
  137. completion()
  138. }
  139. func getCurrentUser() -> UserAccount? {
  140. guard let data = UserDefaults.standard.data(forKey: currentUserKey),
  141. let user = try? JSONDecoder().decode(UserAccount.self, from: data) else {
  142. return nil
  143. }
  144. return user
  145. }
  146. // MARK: - Private Helpers
  147. private func loadAllRecords() -> [LocalUserRecord] {
  148. guard let data = UserDefaults.standard.data(forKey: usersKey),
  149. let records = try? JSONDecoder().decode([LocalUserRecord].self, from: data) else {
  150. return []
  151. }
  152. return records
  153. }
  154. private func saveAllRecords(_ records: [LocalUserRecord]) {
  155. if let data = try? JSONEncoder().encode(records) {
  156. UserDefaults.standard.set(data, forKey: usersKey)
  157. }
  158. }
  159. private func saveCurrentSession(_ user: UserAccount) {
  160. if let data = try? JSONEncoder().encode(user) {
  161. UserDefaults.standard.set(data, forKey: currentUserKey)
  162. }
  163. }
  164. private func hashPassword(_ raw: String) -> String {
  165. return "hashed_" + String(raw.reversed()) + "_salt"
  166. }
  167. }