ios_app_v2/yobble/Network/AuthService.swift
2025-10-07 01:51:15 +03:00

335 lines
15 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")
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, apiResponse.data.message)
} 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")
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)") }
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)
}
switch statusCode {
case 400:
return NSLocalizedString("Неверный запрос (400).", comment: "")
case 403:
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):
if let message = extractMessage(from: data) {
return message
}
switch statusCode {
case 401:
return NSLocalizedString("Необходимо авторизоваться заново.", comment: "")
case 403:
return NSLocalizedString("Старый пароль указан неверно или совпадает с новым.", comment: "")
case 422:
return NSLocalizedString("Проверьте данные и повторите попытку.", comment: "")
case 429:
return NSLocalizedString("Слишком много попыток. Попробуйте позже.", comment: "")
default:
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 }
if let response = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
if let message = response.data?.message, !message.isEmpty {
return message
}
if let detail = response.detail, !detail.isEmpty {
return detail
}
}
return nil
}
}
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
}