diff --git a/yobble/Network/AuthService.swift b/yobble/Network/AuthService.swift index 338cf90..37c02b8 100644 --- a/yobble/Network/AuthService.swift +++ b/yobble/Network/AuthService.swift @@ -46,6 +46,7 @@ final class AuthService { NetworkClient.shared.request( path: "/v1/auth/login/password", method: .post, + headers: ["X-Client-Type": "ios"], body: body, requiresAuth: false ) { result in @@ -84,22 +85,87 @@ final class AuthService { } func requestLoginCode(identifier: String, completion: @escaping (Bool, String?) -> Void) { - if AppConfig.DEBUG { - print("[AuthService] requestLoginCode placeholder for \(identifier)") + let payload = LoginCodeRequestPayload(login: identifier) + + guard let body = try? JSONEncoder().encode(payload) else { + completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) + return } - DispatchQueue.global().asyncAfter(deadline: .now() + 0.8) { - completion(true, nil) + NetworkClient.shared.request( + path: "/v1/auth/login/code", + method: .post, + headers: ["X-Client-Type": "ios"], + body: body, + requiresAuth: false + ) { [weak self] result in + guard let self else { return } + + 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 ?? apiResponse.data.message + completion(false, message) + return + } + completion(true, nil) + } catch { + completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) + } + case .failure(let error): + completion(false, self.passwordlessRequestErrorMessage(for: error)) + } } } func loginWithCode(identifier: String, code: String, completion: @escaping (Bool, String?) -> Void) { - if AppConfig.DEBUG { - print("[AuthService] loginWithCode placeholder for \(identifier) using code \(code)") + let payload = VerifyCodeRequestPayload(login: identifier, otp: code) + + guard let body = try? JSONEncoder().encode(payload) else { + completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: "")) + return } - DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { - completion(false, NSLocalizedString("Вход по коду пока недоступен. Заглушка.", comment: "")) + NetworkClient.shared.request( + path: "/v1/auth/login/verify_code", + method: .post, + headers: ["X-Client-Type": "ios"], + body: body, + requiresAuth: false + ) { [weak self] result in + guard let self else { return } + + 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: identifier) + KeychainService.shared.save(tokens.refresh_token, forKey: "refresh_token", service: identifier) + if let userId = tokens.user_id { + KeychainService.shared.save(userId, forKey: "userId", service: identifier) + } + UserDefaults.standard.set(identifier, forKey: "currentUser") + + NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) + + completion(true, nil) + } catch { + completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) + } + case .failure(let error): + completion(false, self.passwordlessVerifyErrorMessage(for: error)) + } } } @@ -281,6 +347,70 @@ final class AuthService { } } + private func passwordlessRequestErrorMessage(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, 404: + return message ?? NSLocalizedString("Аккаунт не найден.", comment: "") + case 403: + return message ?? NSLocalizedString("Этому аккаунту недоступен вход по коду.", comment: "") + case 422: + return message ?? NSLocalizedString("Неверный логин. Проверьте и попробуйте снова.", comment: "") + case 429: + return NSLocalizedString("Слишком много попыток. Попробуйте позже.", comment: "") + case 502: + 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 passwordlessVerifyErrorMessage(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 message ?? NSLocalizedString("Неверный или просроченный код.", comment: "") + case 403: + return message ?? NSLocalizedString("Этот аккаунт недоступен.", comment: "") + case 404: + return message ?? NSLocalizedString("Аккаунт не найден.", comment: "") + case 422: + return message ?? NSLocalizedString("Некорректные данные. Проверьте код и логин.", comment: "") + case 429: + return NSLocalizedString("Слишком много попыток. Попробуйте позже.", comment: "") + case 502: + 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 mappedRegistrationMessage(for message: String, statusCode: Int) -> String { if statusCode == 400 { if message.contains("Invalid invitation code") { @@ -420,6 +550,15 @@ private struct LoginRequest: Encodable { let password: String } +private struct LoginCodeRequestPayload: Encodable { + let login: String +} + +private struct VerifyCodeRequestPayload: Encodable { + let login: String + let otp: String +} + private struct RegisterRequest: Encodable { let login: String let password: String diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 90d81d4..701f282 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -208,6 +208,9 @@ } } } + }, + "Аккаунт не найден." : { + }, "Активные сессии" : { "comment" : "Заголовок экрана активных сессий", @@ -394,9 +397,6 @@ }, "Вход и защита аккаунта (заглушка)" : { "comment" : "Раздел настроек безопасности для аутентификации" - }, - "Вход по коду пока недоступен. Заглушка." : { - }, "Вход по паролю" : { @@ -1272,6 +1272,9 @@ } } } + }, + "Неверный или просроченный код." : { + }, "Неверный код" : { "comment" : "Заголовок ошибки неправильного кода 2FA" @@ -1306,6 +1309,9 @@ } } } + }, + "Неверный логин. Проверьте и попробуйте снова." : { + }, "Неверный пароль" : { "comment" : "Неверный пароль", @@ -1378,6 +1384,9 @@ } } } + }, + "Некорректные данные. Проверьте код и логин." : { + }, "Некорректный ответ от сервера." : { @@ -2123,6 +2132,9 @@ } } } + }, + "Проверьте код и попробуйте снова." : { + }, "Проверьте цифры и попробуйте снова." : { "comment" : "Описание ошибки неверного кода 2FA" @@ -2376,6 +2388,9 @@ } } } + }, + "Сессия недействительна. Авторизуйтесь заново." : { + }, "Системная" : { "localizations" : { @@ -2783,6 +2798,12 @@ }, "Это устройство" : { "comment" : "Заголовок секции текущего устройства" + }, + "Этому аккаунту недоступен вход по коду." : { + + }, + "Этот аккаунт недоступен." : { + }, "Я ознакомился и принимаю правила сервиса" : {