303 lines
15 KiB
Swift
303 lines
15 KiB
Swift
//
|
||
// AuthService.swift
|
||
// VolnahubApp
|
||
//
|
||
// Created by cheykrym on 09/06/2025.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
class AuthService {
|
||
|
||
func autoLogin(completion: @escaping (Bool, String?) -> Void) {
|
||
// 1️⃣ Проверяем наличие текущего пользователя
|
||
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
|
||
}
|
||
|
||
// 2️⃣ Текущий пользователь не найден или токены отсутствуют
|
||
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 {
|
||
// Нашли пользователя с токенами — назначаем как currentUser
|
||
UserDefaults.standard.set(user, forKey: "currentUser")
|
||
if AppConfig.DEBUG{ print("AutoLogin: переключились на пользователя \(user)")}
|
||
completion(true, nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 3️⃣ Если никто не найден
|
||
// completion(false, "Не найден авторизованный пользователь. Пожалуйста, войдите снова.")
|
||
completion(false, nil)
|
||
}
|
||
|
||
|
||
func login(username: String, password: String, completion: @escaping (Bool, String?) -> Void) {
|
||
let url = URL(string: "\(AppConfig.API_SERVER)/v1/auth/login")!
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
request.setValue("\(AppConfig.USER_AGENT)", forHTTPHeaderField: "User-Agent")
|
||
|
||
let payload: [String: String] = [
|
||
"login": username,
|
||
"password": password
|
||
]
|
||
|
||
do {
|
||
let jsonData = try JSONEncoder().encode(payload)
|
||
request.httpBody = jsonData
|
||
} catch {
|
||
DispatchQueue.main.async {
|
||
completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: ""))
|
||
}
|
||
return
|
||
}
|
||
|
||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||
DispatchQueue.main.async {
|
||
if let error = error {
|
||
let errorMessage = String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), error.localizedDescription)
|
||
completion(false, errorMessage)
|
||
return
|
||
}
|
||
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
completion(false, NSLocalizedString("Некорректный ответ от сервера.", comment: ""))
|
||
return
|
||
}
|
||
|
||
guard (200...299).contains(httpResponse.statusCode) else {
|
||
if httpResponse.statusCode == 401{
|
||
completion(false, NSLocalizedString("Неверный логин или пароль.", comment: ""))
|
||
} else if httpResponse.statusCode == 502{
|
||
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
|
||
} else if httpResponse.statusCode == 429 {
|
||
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
|
||
} else {
|
||
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)")
|
||
completion(false, errorMessage)
|
||
}
|
||
return
|
||
}
|
||
|
||
guard let data = data else {
|
||
completion(false, NSLocalizedString("Пустой ответ от сервера.", comment: ""))
|
||
return
|
||
}
|
||
|
||
do {
|
||
let decoder = JSONDecoder()
|
||
let loginResponse = try decoder.decode(APIResponse<LoginPayload>.self, from: data).data
|
||
|
||
// Сохраняем токены в Keychain
|
||
KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username)
|
||
KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username)
|
||
KeychainService.shared.save(loginResponse.user_id, forKey: "userId", service: username)
|
||
UserDefaults.standard.set(username, forKey: "currentUser")
|
||
|
||
completion(true, nil)
|
||
} catch {
|
||
completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: ""))
|
||
}
|
||
}
|
||
}
|
||
task.resume()
|
||
}
|
||
|
||
func register(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) {
|
||
let url = URL(string: "\(AppConfig.API_SERVER)/v1/auth/register")!
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
request.setValue(AppConfig.USER_AGENT, forHTTPHeaderField: "User-Agent")
|
||
|
||
let payload: [String: Any] = [
|
||
"login": username,
|
||
"password": password,
|
||
"invite": invite ?? NSNull()
|
||
]
|
||
|
||
do {
|
||
let jsonData = try JSONSerialization.data(withJSONObject: payload)
|
||
request.httpBody = jsonData
|
||
} catch {
|
||
DispatchQueue.main.async {
|
||
completion(false, NSLocalizedString("Не удалось сериализовать данные запроса.", comment: ""))
|
||
}
|
||
return
|
||
}
|
||
|
||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||
DispatchQueue.main.async {
|
||
if let error = error {
|
||
let errorMessage = String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), error.localizedDescription)
|
||
completion(false, errorMessage)
|
||
return
|
||
}
|
||
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
completion(false, NSLocalizedString("Некорректный ответ от сервера.", comment: ""))
|
||
return
|
||
}
|
||
|
||
guard let data = data else {
|
||
completion(false, NSLocalizedString("Пустой ответ от сервера.", comment: ""))
|
||
return
|
||
}
|
||
|
||
let decoder = JSONDecoder()
|
||
|
||
if (200...299).contains(httpResponse.statusCode) {
|
||
do {
|
||
let _ = try decoder.decode(APIResponse<RegisterPayload>.self, from: data)
|
||
if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")}
|
||
|
||
// Сразу логинимся
|
||
self.login(username: username, password: password) { loginSuccess, loginMessage in
|
||
if loginSuccess {
|
||
completion(true, "Регистрация и вход выполнены успешно.")
|
||
} else {
|
||
// Регистрация успешна, но логин не удался — покажем сообщение
|
||
completion(false, loginMessage ?? NSLocalizedString("Регистрация выполнена, но вход не удался.", comment: ""))
|
||
}
|
||
}
|
||
} catch {
|
||
completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: ""))
|
||
}
|
||
} else {
|
||
// Ошибка сервера — пробуем распарсить message
|
||
if let errorResponseMessage = try? decoder.decode(ErrorResponse.self, from: data),
|
||
let message = errorResponseMessage.data?.message {
|
||
|
||
if let jsonString = String(data: data, encoding: .utf8) {
|
||
if AppConfig.DEBUG{ print("Raw JSON:", jsonString)}
|
||
}
|
||
if AppConfig.DEBUG{ print("message:", message)}
|
||
|
||
if httpResponse.statusCode == 400 {
|
||
if message.contains("Invalid invitation code") {
|
||
completion(false, NSLocalizedString("Неверный код приглашения.", comment: ""))
|
||
} else if message.contains("This invitation is not active") {
|
||
completion(false, NSLocalizedString("Приглашение не активно.", comment: ""))
|
||
} else if message.contains("This invitation has reached its usage limit") {
|
||
completion(false, NSLocalizedString("Приглашение достигло лимита использования.", comment: ""))
|
||
} else if message.contains("This invitation has expired") {
|
||
completion(false, NSLocalizedString("Приглашение истекло.", comment: ""))
|
||
} else if message.contains("Login already registered") {
|
||
completion(false, NSLocalizedString("Логин уже занят.", comment: ""))
|
||
} else {
|
||
completion(false, message)
|
||
}
|
||
} else if httpResponse.statusCode == 403 {
|
||
if message.contains("Registration is currently disabled") {
|
||
completion(false, NSLocalizedString("Регистрация временно недоступна.", comment: ""))
|
||
} else {
|
||
completion(false, message)
|
||
}
|
||
} else if httpResponse.statusCode == 429 {
|
||
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
|
||
} else if httpResponse.statusCode == 502{
|
||
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
|
||
} else {
|
||
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)")
|
||
completion(false, errorMessage)
|
||
}
|
||
} else {
|
||
// Не удалось распарсить JSON — fallback
|
||
if httpResponse.statusCode == 400 {
|
||
completion(false, NSLocalizedString("Неверный запрос (400).", comment: ""))
|
||
} else if httpResponse.statusCode == 403 {
|
||
completion(false, NSLocalizedString("Регистрация запрещена.", comment: ""))
|
||
} else if httpResponse.statusCode == 429 {
|
||
completion(false, NSLocalizedString("Слишком много запросов.", comment: ""))
|
||
} else if httpResponse.statusCode == 502{
|
||
completion(false, NSLocalizedString("Сервер не отвечает. Попробуйте позже.", comment: ""))
|
||
} else {
|
||
let errorMessage = String(format: NSLocalizedString("Ошибка сервера: %@", comment: ""), "\(httpResponse.statusCode)")
|
||
completion(false, errorMessage)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
task.resume()
|
||
}
|
||
|
||
|
||
|
||
|
||
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, "Нет доступных пользователей. Пожалуйста, войдите снова.")
|
||
completion(false, nil)
|
||
}
|
||
}
|
||
|
||
struct APIResponse<T: Decodable>: Decodable {
|
||
let status: String
|
||
let data: T
|
||
}
|
||
|
||
struct LoginPayload: Decodable {
|
||
let access_token: String
|
||
let refresh_token: String
|
||
let user_id: String
|
||
}
|
||
|
||
struct TokenRefreshPayload: Decodable {
|
||
let access_token: String
|
||
let token_type: String
|
||
}
|
||
|
||
struct RegisterPayload: Decodable {
|
||
let message: String
|
||
}
|
||
|
||
struct ErrorPayload: Decodable {
|
||
let message: String?
|
||
}
|
||
|
||
struct ErrorResponse: Decodable {
|
||
let status: String?
|
||
let data: ErrorPayload?
|
||
}
|