ios_app_v2/yobble/Network/AuthService.swift
2025-10-24 21:49:32 +03:00

413 lines
18 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<TokenPairPayload>.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<RegisterPayload>.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<MessagePayload>.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<String> = [
"Неверный текущий пароль",
"Пароль должен отличаться от старого",
"Пароль не удовлетворяет требованиям"
]
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
}