// // AuthService.swift // VolnahubApp // // Created by cheykrym on 09/06/2025. // import Foundation final class AuthService { func autoLogin(completion: @escaping (Bool, String?) -> Void) { if let currentUser = UserDefaults.standard.string(forKey: "currentUser"), let _ = KeychainService.shared.get(forKey: "access_token", service: currentUser), let _ = KeychainService.shared.get(forKey: "refresh_token", service: currentUser) { if AppConfig.DEBUG { print("AutoLogin: найден текущий пользователь — \(currentUser)") } completion(true, nil) return } if AppConfig.DEBUG { print("AutoLogin: текущий пользователь не найден или токены отсутствуют. Пробуем найти другого пользователя...") } let allUsers = KeychainService.shared.getAllServices() for user in allUsers { let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil if hasAccessToken && hasRefreshToken { UserDefaults.standard.set(user, forKey: "currentUser") if AppConfig.DEBUG { print("AutoLogin: переключились на пользователя \(user)") } completion(true, nil) return } } completion(false, nil) } func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) { let payload = LoginRequest(login: username, password: password) guard let body = try? JSONEncoder().encode(payload) else { completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) return } NetworkClient.shared.request( path: "/v1/auth/login", method: .post, body: body, requiresAuth: false ) { result in switch result { case .success(let response): do { let decoder = JSONDecoder() let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Неизвестная ошибка", comment: "") completion(false, message) return } let tokens = apiResponse.data KeychainService.shared.save(tokens.access_token, forKey: "access_token", service: username) KeychainService.shared.save(tokens.refresh_token, forKey: "refresh_token", service: username) if let userId = tokens.user_id { KeychainService.shared.save(userId, forKey: "userId", service: username) } UserDefaults.standard.set(username, forKey: "currentUser") NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) completion(true, nil) } catch { completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) } case .failure(let error): let message = self.loginErrorMessage(for: error) completion(false, message) } } } func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) { let payload = RegisterRequest(login: username, password: password, invite: invite) guard let body = try? JSONEncoder().encode(payload) else { completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) return } NetworkClient.shared.request( path: "/v1/auth/register", method: .post, body: body, requiresAuth: false ) { result in switch result { case .success(let response): do { let decoder = JSONDecoder() let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Неизвестная ошибка", comment: "") completion(false, message) return } if AppConfig.DEBUG { print("Регистрация успешна. Пытаемся сразу войти...") } self.login(username: username, password: password) { loginSuccess, loginMessage in if loginSuccess { completion(true, NSLocalizedString("Регистрация и вход выполнены успешно.", comment: "")) } else { completion(false, loginMessage ?? NSLocalizedString("Регистрация выполнена, но вход не удался.", comment: "")) } } } catch { completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) } case .failure(let error): let message = self.registerErrorMessage(for: error) completion(false, message) } } } func changePassword(oldPassword: String, newPassword: String, completion: @escaping (Bool, String?) -> Void) { let payload = ChangePasswordRequestPayload(old_password: oldPassword, new_password: newPassword) guard let body = try? JSONEncoder().encode(payload) else { completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) return } NetworkClient.shared.request( path: "/v1/auth/password/change", method: .post, body: body, requiresAuth: true ) { result in switch result { case .success(let response): do { let decoder = JSONDecoder() let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить пароль.", comment: "") completion(false, message) return } completion(true, NSLocalizedString(apiResponse.data.message, comment: "")) } catch { completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) } case .failure(let error): let message = self.changePasswordErrorMessage(for: error) completion(false, message) } } } func logoutCurrentUser(completion: @escaping (Bool, String?) -> Void) { guard let currentUser = UserDefaults.standard.string(forKey: "currentUser") else { completion(false, "Не найден текущий пользователь.") return } KeychainService.shared.delete(forKey: "access_token", service: currentUser) KeychainService.shared.delete(forKey: "refresh_token", service: currentUser) KeychainService.shared.delete(forKey: "userId", service: currentUser) UserDefaults.standard.removeObject(forKey: "currentUser") NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) let allUsers = KeychainService.shared.getAllServices() for user in allUsers { let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil let hasRefreshToken = KeychainService.shared.get(forKey: "refresh_token", service: user) != nil if hasAccessToken && hasRefreshToken { UserDefaults.standard.set(user, forKey: "currentUser") if AppConfig.DEBUG { print("Logout: переключились на пользователя \(user)") } NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) completion(true, nil) return } } completion(false, nil) } private func loginErrorMessage(for error: NetworkError) -> String { switch error { case .network(let err): return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), err.localizedDescription) case .server(let statusCode, _): switch statusCode { case 401: return NSLocalizedString("Неверный логин или пароль.", comment: "") case 429: return NSLocalizedString("Слишком много запросов.", comment: "") case 502: return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "") default: return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)") } case .unauthorized: return NSLocalizedString("Неверный логин или пароль.", comment: "") case .invalidURL, .noResponse: return NSLocalizedString("Некорректный ответ от сервера.", comment: "") } } private func registerErrorMessage(for error: NetworkError) -> String { switch error { case .network(let err): return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), err.localizedDescription) case .server(let statusCode, let data): if let data, let response = try? JSONDecoder().decode(ErrorResponse.self, from: data), let message = response.data?.message { return mappedRegistrationMessage(for: message, statusCode: statusCode) } let message = extractMessage(from: data) switch statusCode { case 400: return NSLocalizedString("Неверный запрос (400).", comment: "") case 403: return NSLocalizedString("Регистрация запрещена.", comment: "") case 409: return NSLocalizedString("Логин уже занят.", comment: "") case 422: if let message { if message == "Value error, Login must not end with 'bot' for non-bot accounts"{ return NSLocalizedString("Login must not end with 'bot' for non-bot accounts", comment: "") } return message } else { return NSLocalizedString("Ошибка в данных. Проверьте введённую информацию.", comment: "") } case 429: return NSLocalizedString("Слишком много запросов.", comment: "") case 502: return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "") default: return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)") } case .unauthorized: return NSLocalizedString("Регистрация запрещена.", comment: "") case .invalidURL, .noResponse: return NSLocalizedString("Некорректный ответ от сервера.", comment: "") } } private func mappedRegistrationMessage(for message: String, statusCode: Int) -> String { if statusCode == 400 { if message.contains("Invalid invitation code") { return NSLocalizedString("Неверный код приглашения.", comment: "") } else if message.contains("This invitation is not active") { return NSLocalizedString("Приглашение не активно.", comment: "") } else if message.contains("This invitation has reached its usage limit") { return NSLocalizedString("Приглашение достигло лимита использования.", comment: "") } else if message.contains("This invitation has expired") { return NSLocalizedString("Приглашение истекло.", comment: "") } else if message.contains("Login already registered") { return NSLocalizedString("Логин уже занят.", comment: "") } } if statusCode == 403 { if message.contains("Registration is currently disabled") { return NSLocalizedString("Регистрация временно недоступна.", comment: "") } } if statusCode == 429 { return NSLocalizedString("Слишком много запросов.", comment: "") } if statusCode == 502 { return NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: "") } return message } private func changePasswordErrorMessage(for error: NetworkError) -> String { switch error { case .network(let err): return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), err.localizedDescription) case .server(let statusCode, let data): let message = extractMessage(from: data) switch statusCode { case 401: return NSLocalizedString("Необходимо авторизоваться заново.", comment: "") case 403: if let message, Self.changePasswordForbiddenMessages.contains(message) { return NSLocalizedString(message, comment: "") } return NSLocalizedString("Старый пароль указан неверно или совпадает с новым.", comment: "") case 422: if let message { return message } return NSLocalizedString("Проверьте данные и повторите попытку.", comment: "") case 429: return NSLocalizedString("Слишком много попыток. Попробуйте позже.", comment: "") default: if let message { return message } return String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(statusCode)") } case .unauthorized: return NSLocalizedString("Необходимо авторизоваться заново.", comment: "") case .invalidURL, .noResponse: return NSLocalizedString("Некорректный ответ от сервера.", comment: "") } } private func extractMessage(from data: Data?) -> String? { guard let data else { return nil } let decoder = JSONDecoder() if let response = try? decoder.decode(ErrorResponse.self, from: data) { if let message = response.data?.message, !message.isEmpty { return message } if let detail = response.detail, !detail.isEmpty { return detail } } if let jsonObject = try? JSONSerialization.jsonObject(with: data) { if let dictionary = jsonObject as? [String: Any] { if let detail = Self.normalizedMessage(dictionary["detail"] as? String) { return detail } if let dataDict = dictionary["data"] as? [String: Any], let message = Self.normalizedMessage(dataDict["message"] as? String) { return message } if let errors = dictionary["errors"] as? [[String: Any]], let firstMessage = errors.compactMap({ Self.normalizedMessage($0["message"] as? String) }).first { return firstMessage } } else if let array = jsonObject as? [[String: Any]] { if let firstMessage = array.compactMap({ item -> String? in if let detail = Self.normalizedMessage(item["detail"] as? String) { return detail } if let message = Self.normalizedMessage(item["message"] as? String) { return message } if let msg = Self.normalizedMessage(item["msg"] as? String) { return msg } return nil }).first { return firstMessage } } } if let string = Self.normalizedMessage(String(data: data, encoding: .utf8)) { return string } return nil } } private extension AuthService { static let changePasswordForbiddenMessages: Set = [ "Неверный текущий пароль", "Пароль должен отличаться от старого", "Пароль не удовлетворяет требованиям" ] static func normalizedMessage(_ raw: String?) -> String? { guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil } return raw } } private struct LoginRequest: Encodable { let login: String let password: String } private struct RegisterRequest: Encodable { let login: String let password: String let invite: String? } private struct ChangePasswordRequestPayload: Encodable { let old_password: String let new_password: String }