413 lines
18 KiB
Swift
413 lines
18 KiB
Swift
//
|
||
// 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
|
||
}
|