From 920168e7d112651ea07b0a4d4c2c78183bf1ed3f Mon Sep 17 00:00:00 2001 From: Professional Date: Mon, 26 May 2025 16:56:49 +0700 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/adminController.js | 89 +++++++++---------- src/auth.js | 87 +++++++++++------- src/services/api.js | 71 +++++++++++++-- src/services/socketService.js | 118 +++++++++++++------------ 4 files changed, 227 insertions(+), 138 deletions(-) diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index ae16cc9..5daecb7 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -135,64 +135,63 @@ const toggleUserActive = async (req, res) => { // Если пользователь был активным и теперь блокируется if (wasActive && !user.isActive) { - // Находим активное соединение пользователя - const activeUsers = Array.from(io.sockets.sockets).map(socket => socket[1]); - const userSockets = activeUsers.filter(socket => { - const userData = socket.handshake.query; - return userData && userData.userId === userId; + console.log(`[AdminController] Блокировка пользователя ${userId}. Причина: ${reason || 'Не указана'}`); + + // Находим все активные сокеты пользователя - новый подход с более надежным поиском + const activeSockets = Array.from(io.sockets.sockets.values()).filter(socket => { + return socket && + socket.handshake && + socket.handshake.query && + socket.handshake.query.userId === userId; }); - // Получаем информацию о пользователе из массива activeUsers - // (этот массив хранится в server.js) - const activeUsersArray = Array.from(io.sockets.adapter.rooms).filter(room => { - return room[1].has(room[0]); // Фильтруем комнаты, где id комнаты совпадает с id сокета - }).map(room => { - const socket = io.sockets.sockets.get(room[0]); - return socket && socket.handshake.query.userId - ? { userId: socket.handshake.query.userId, socketId: room[0] } - : null; - }).filter(Boolean); + console.log(`[AdminController] Найдено активных сокетов пользователя: ${activeSockets.length}`); - // Находим сокет пользователя - const userSocketInfo = activeUsersArray.find(u => u.userId === userId); - - // Если пользователь онлайн, отправляем уведомление о блокировке - if (userSocketInfo && userSocketInfo.socketId) { - io.to(userSocketInfo.socketId).emit("account_blocked", { - message: 'Ваш аккаунт был заблокирован администратором.', - reason: reason || 'Нарушение правил сервиса', - timestamp: new Date().toISOString() + if (activeSockets.length > 0) { + // Отправляем уведомление по всем активным сокетам пользователя + activeSockets.forEach(socket => { + console.log(`[AdminController] Отправка уведомления о блокировке на сокет: ${socket.id}`); + socket.emit("account_blocked", { + message: 'Ваш аккаунт был заблокирован администратором.', + reason: reason || 'Нарушение правил сервиса', + timestamp: new Date().toISOString(), + blocked: true + }); }); - console.log(`[AdminController] Отправлено уведомление о блокировке пользователю ${userId}`); + + // Принудительно отключаем все сокеты пользователя + setTimeout(() => { + activeSockets.forEach(socket => { + console.log(`[AdminController] Принудительное отключение сокета: ${socket.id}`); + socket.disconnect(true); + }); + }, 1000); // Небольшая задержка, чтобы сообщение о блокировке успело доставиться } else { - // Если пользователь не в сети, просто оставляем лог - console.log(`[AdminController] Пользователь ${userId} заблокирован, но не находится в сети для отправки уведомления`); + console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях`); } } // Если пользователь был неактивным и теперь разблокируется else if (!wasActive && user.isActive) { - // Используем тот же подход для поиска сокетов пользователя - const activeUsersArray = Array.from(io.sockets.adapter.rooms).filter(room => { - return room[1].has(room[0]); - }).map(room => { - const socket = io.sockets.sockets.get(room[0]); - return socket && socket.handshake.query.userId - ? { userId: socket.handshake.query.userId, socketId: room[0] } - : null; - }).filter(Boolean); + console.log(`[AdminController] Разблокировка пользователя ${userId}`); - // Находим сокет пользователя (если он вдруг залогинился с другим статусом) - const userSocketInfo = activeUsersArray.find(u => u.userId === userId); + // То же самое для поиска сокетов пользователя + const activeSockets = Array.from(io.sockets.sockets.values()).filter(socket => { + return socket && + socket.handshake && + socket.handshake.query && + socket.handshake.query.userId === userId; + }); - // Если пользователь онлайн, отправляем уведомление о разблокировке - if (userSocketInfo && userSocketInfo.socketId) { - io.to(userSocketInfo.socketId).emit("account_unblocked", { - message: 'Ваш аккаунт был разблокирован администратором.', - timestamp: new Date().toISOString() + if (activeSockets.length > 0) { + activeSockets.forEach(socket => { + console.log(`[AdminController] Отправка уведомления о разблокировке на сокет: ${socket.id}`); + socket.emit("account_unblocked", { + message: 'Ваш аккаунт был разблокирован администратором.', + timestamp: new Date().toISOString() + }); }); - console.log(`[AdminController] Отправлено уведомление о разблокировке пользователю ${userId}`); } else { - console.log(`[AdminController] Пользователь ${userId} разблокирован, но не находится в сети для отправки уведомления`); + console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях для отправки уведомления о разблокировке`); } } diff --git a/src/auth.js b/src/auth.js index cad8b1e..3c853ae 100644 --- a/src/auth.js +++ b/src/auth.js @@ -129,37 +129,64 @@ async function logout() { // Функция обработки события блокировки аккаунта function handleAccountBlocked(data) { - console.log('Обработка блокировки аккаунта:', data); + console.log('[Auth] Экстренная обработка блокировки аккаунта:', data); - // Создаем объект с информацией о блокировке - const blockInfo = { - blocked: true, - message: data?.message || 'Ваш аккаунт был заблокирован администратором.', - reason: data?.reason || 'Нарушение правил сервиса', - timestamp: new Date().toISOString() - }; - - // Сохраняем информацию о блокировке в localStorage для отображения на странице логина - localStorage.setItem('accountBlockedInfo', JSON.stringify(blockInfo)); - - // Немедленно выполняем выход пользователя - сначала очищаем данные - user.value = null; - token.value = null; - localStorage.removeItem('userToken'); - - // Удаляем токен из заголовков для будущих запросов API - api.setAuthToken(null); - - // Принудительно прерываем все активные запросы API, если они есть - api.cancelActiveRequests && api.cancelActiveRequests(); - - // Перенаправляем на страницу входа с уведомлением о блокировке - router.push({ - name: 'Login', - query: { blocked: 'true' } - }); - - console.log('Пользователь был разлогинен из-за блокировки аккаунта.'); + try { + // Создаем объект с информацией о блокировке + const blockInfo = { + blocked: true, + message: data?.message || 'Ваш аккаунт был заблокирован администратором.', + reason: data?.reason || 'Нарушение правил сервиса', + timestamp: new Date().toISOString() + }; + + // Сохраняем информацию о блокировке в localStorage для отображения на странице логина + localStorage.setItem('accountBlockedInfo', JSON.stringify(blockInfo)); + + // Немедленно очищаем все данные пользователя + console.log('[Auth] Очистка данных пользователя из-за блокировки'); + user.value = null; + token.value = null; + + // Очистка localStorage - удаляем все ключи, которые могут содержать пользовательские данные + localStorage.removeItem('userToken'); + localStorage.removeItem('userSettings'); + localStorage.removeItem('userPreferences'); + + // Удаляем токены доступа для API запросов + if (typeof api.setAuthToken === 'function') { + api.setAuthToken(null); + console.log('[Auth] Токен авторизации API удален'); + } + + // Пробуем отменить активные запросы + if (typeof api.cancelActiveRequests === 'function') { + api.cancelActiveRequests(); + console.log('[Auth] Активные запросы API отменены'); + } + + // Устанавливаем флаг в SessionStorage для индикации блокировки при перезагрузке страницы + sessionStorage.setItem('forcedLogout', 'blocked'); + + // Принудительный перенаправление на страницу логина с флагом блокировки + console.log('[Auth] Перенаправление на страницу входа с параметром blocked=true'); + + // Используем прямое изменение URL для полной перезагрузки страницы + // Это гарантирует, что все состояния будут сброшены + window.location.href = '/login?blocked=true&reason=' + encodeURIComponent(blockInfo.reason); + + // Не используем router.push, т.к. он не всегда работает корректно при критических ошибках + // router.push({ name: 'Login', query: { blocked: 'true', reason: blockInfo.reason } }); + + console.log('[Auth] Пользователь был разлогинен из-за блокировки аккаунта.'); + return true; // Возвращаем true, чтобы вызывающий код знал, что выход выполнен + } catch (error) { + console.error('[Auth] Критическая ошибка при обработке блокировки аккаунта:', error); + + // В случае любой ошибки, выполняем аварийный выход + window.location.href = '/login?blocked=true&emergency=true'; + return false; + } } // Функция обработки события разблокировки аккаунта diff --git a/src/services/api.js b/src/services/api.js index 930e654..3eebc99 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,5 +1,6 @@ import axios from 'axios'; import { useAuth } from '../auth'; // Импортируем функцию авторизации +import { isAccountBlocked } from './socketService'; // Импортируем проверку блокировки из socketService // Создаем экземпляр Axios с базовым URL нашего API // Настраиваем базовый URL в зависимости от окружения @@ -7,6 +8,31 @@ import { useAuth } from '../auth'; // Импортируем функцию ав // Храним токены отмены запросов для возможности отмены при блокировке const cancelTokenSources = new Map(); +// Проверка блокировки аккаунта из localStorage +const checkAccountBlockStatus = () => { + try { + const blockedInfo = localStorage.getItem('accountBlockedInfo'); + if (blockedInfo) { + const blockedData = JSON.parse(blockedInfo); + if (blockedData?.blocked === true) { + console.warn('[API] Обнаружен заблокированный аккаунт, запросы невозможны'); + return true; + } + } + + // Проверяем также флаг из socketService + if (isAccountBlocked && isAccountBlocked()) { + console.warn('[API] Аккаунт заблокирован согласно socketService, запросы невозможны'); + return true; + } + + return false; + } catch (e) { + console.error('[API] Ошибка при проверке статуса блокировки:', e); + return false; + } +}; + // Определяем базовый URL в зависимости от среды выполнения const getBaseUrl = () => { // Всегда используем относительный путь /api. @@ -26,6 +52,24 @@ const apiClient = axios.create({ // (Опционально, но очень полезно) Перехватчик для добавления JWT токена к запросам apiClient.interceptors.request.use( (config) => { + // Проверяем, заблокирован ли аккаунт, ПЕРЕД выполнением запроса + if (checkAccountBlockStatus()) { + // Если аккаунт заблокирован, отменяем запрос + const source = axios.CancelToken.source(); + config.cancelToken = source.token; + source.cancel('Запрос отменен из-за блокировки аккаунта'); + console.error('[API] Запрос отменен из-за блокировки аккаунта:', config.url); + + // Перенаправляем на страницу логина, если пользователь пытается выполнять запросы + setTimeout(() => { + if (window.location.pathname !== '/login') { + window.location.href = '/login?blocked=true'; + } + }, 0); + + return Promise.reject(new Error('Аккаунт заблокирован')); + } + const token = localStorage.getItem('userToken'); // Предполагаем, что токен хранится в localStorage if (token) { console.log('[API] Добавление токена к запросу:', config.url); @@ -42,11 +86,6 @@ apiClient.interceptors.request.use( const requestKey = `${config.method}-${config.url}`; cancelTokenSources.set(requestKey, source); - // Добавляем обработчик, который удаляет источник токена отмены после завершения запроса - config.onFinally = () => { - cancelTokenSources.delete(requestKey); - }; - return config; }, (error) => { @@ -70,6 +109,27 @@ apiClient.interceptors.response.use( cancelTokenSources.delete(requestKey); } + // Проверяем на блокировку аккаунта + if (error.response && error.response.status === 403) { + const errorData = error.response.data; + // Проверка на признаки блокировки аккаунта в ответе + if (errorData && ( + errorData.blocked === true || + errorData.message?.includes('заблокирован') || + errorData.message?.includes('blocked') + )) { + console.error('[API] Получено уведомление о блокировке аккаунта:', errorData); + + // Вызываем обработчик блокировки аккаунта + setTimeout(() => { + const { handleAccountBlocked } = useAuth(); + if (typeof handleAccountBlocked === 'function') { + handleAccountBlocked(errorData); + } + }, 0); + } + } + if (error.response && error.response.status === 401) { console.error('[API] Неавторизованный запрос (401):', error.config.url); @@ -108,6 +168,7 @@ export default { // Добавляем новые утилитарные методы cancelActiveRequests, setAuthToken, + checkAccountBlockStatus, register(userData) { return apiClient.post('/auth/register', userData); diff --git a/src/services/socketService.js b/src/services/socketService.js index f3a29e1..6f57dff 100644 --- a/src/services/socketService.js +++ b/src/services/socketService.js @@ -1,33 +1,28 @@ import { io } from 'socket.io-client'; -import { useAuth } from '@/auth'; // Чтобы получить ID текущего пользователя +import { useAuth } from '@/auth'; let socket; -// const SOCKET_URL = 'http://localhost:5000'; // Закомментируем, так как Vite будет проксировать +// Для блокировки аккаунта, добавляем состояние +let wasAccountBlocked = false; + +// const SOCKET_URL = 'http://localhost:5000'; // Для разработки через Vite proxy, URL должен быть относительным к домену фронтенда -const SOCKET_URL = '/'; // Vite будет перехватывать /socket.io/ путь +const SOCKET_URL = '/'; export const connectSocket = () => { const { user, isAuthenticated, handleAccountBlocked, handleAccountUnblocked } = useAuth(); + if (wasAccountBlocked) { + console.log('[SocketService] Попытка подключения отклонена - аккаунт был заблокирован'); + return null; + } + if (isAuthenticated.value && user.value && user.value._id) { // Подключаемся только если пользователь аутентифицирован и есть ID - // Передаем userId в query для идентификации на сервере при подключении (альтернатива событию addUser) - // Однако, мы используем событие addUser, так что query здесь может быть избыточен, если сервер его не использует при connect. - // Оставим его, если вдруг понадобится. - // socket = io(SOCKET_URL, { - // query: { userId: user.value._id } - // }); - - // При использовании Vite proxy, путь для Socket.IO должен быть относительным, - // а Vite добавит префикс /socket.io/ к target в proxy. - // Однако, клиентская библиотека socket.io обычно сама добавляет /socket.io/ - // поэтому мы можем просто указать базовый URL (который будет проксироваться) - // или указать полный путь, который будет проксирован. - // Для ясности и чтобы соответствовать настройке proxy, можно использовать path. socket = io(SOCKET_URL, { - path: '/socket.io/', // Явно указываем путь, который будет проксироваться - transports: ['websocket', 'polling'] // Явно указываем транспорты для лучшей совместимости + path: '/socket.io/', + transports: ['websocket', 'polling'] }); console.log('[SocketService] Попытка подключения к Socket.IO серверу через Vite proxy...'); @@ -40,32 +35,60 @@ export const connectSocket = () => { socket.on('disconnect', (reason) => { console.log('[SocketService] Отключен от Socket.IO. Причина:', reason); + + // Если отключение произошло из-за блокировки аккаунта, + // не пытаемся автоматически переподключиться + if (wasAccountBlocked && (reason === 'io server disconnect' || reason === 'transport close')) { + console.log('[SocketService] Предотвращение переподключения из-за блокировки аккаунта'); + socket.disconnect(); + socket = null; + } }); socket.on('connect_error', (error) => { console.error('[SocketService] Ошибка подключения к Socket.IO:', error.message, error.data); }); - // Добавляем обработчик для события блокировки аккаунта + // ПЕРЕРАБОТАННЫЙ обработчик блокировки аккаунта socket.on('account_blocked', (data) => { console.log('[SocketService] Получено уведомление о блокировке аккаунта:', data); - // Немедленно отключаем сокет - socket.disconnect(); - console.log('[SocketService] Сокет принудительно отключен из-за блокировки аккаунта'); + // Устанавливаем флаг блокировки аккаунта для предотвращения повторных подключений + wasAccountBlocked = true; - // Сразу вызываем функцию обработки блокировки из auth.js, без таймаута - handleAccountBlocked(data); + // Принудительно прерываем соединение + try { + console.log('[SocketService] Принудительное отключение сокета из-за блокировки аккаунта'); + socket.disconnect(); + } catch (error) { + console.error('[SocketService] Ошибка при отключении сокета:', error); + } - // Разрываем все существующие соединения и очищаем состояние сокета + // Вызываем обработчик блокировки для немедленного выхода + if (typeof handleAccountBlocked === 'function') { + // Оборачиваем в try-catch для надежности + try { + handleAccountBlocked(data); + } catch (error) { + console.error('[SocketService] Ошибка при вызове handleAccountBlocked:', error); + // Аварийный выход в случае ошибки в обработчике + window.location.href = '/login?blocked=true'; + } + } else { + console.error('[SocketService] handleAccountBlocked не определен!'); + // Принудительная перезагрузка страницы + window.location.reload(); + } + + // Дополнительно очищаем все соединения и состояние socket = null; }); - // Добавляем обработчик для события разблокировки аккаунта + // Обработчик разблокировки аккаунта socket.on('account_unblocked', (data) => { console.log('[SocketService] Получено уведомление о разблокировке аккаунта:', data); + wasAccountBlocked = false; - // Сразу вызываем обработчик разблокировки, без таймаута if (typeof handleAccountUnblocked === 'function') { handleAccountUnblocked(data); } else { @@ -73,12 +96,6 @@ export const connectSocket = () => { } }); - // Можно здесь же слушать глобальные события, если нужно - // socket.on('getUsers', (users) => { - // console.log('[SocketService] Получен список активных пользователей:', users); - // // Тут можно обновить какой-нибудь стор активных пользователей - // }); - return socket; } else { console.warn('[SocketService] Не удалось подключиться: пользователь не аутентифицирован или нет ID.'); @@ -95,34 +112,19 @@ export const disconnectSocket = () => { }; export const getSocket = () => { + // Если аккаунт был заблокирован, возвращаем null + if (wasAccountBlocked) { + console.warn('[SocketService] getSocket: Аккаунт заблокирован, сокет недоступен'); + return null; + } + if (!socket) { console.warn('[SocketService] getSocket вызван, но сокет не инициализирован. Попытка подключения...'); - // Можно попробовать подключиться здесь, если это желаемое поведение, - // но лучше управлять подключением более явно. - // return connectSocket(); // Будь осторожен с рекурсией или множественными подключениями } return socket; }; -// Функции для отправки и прослушивания специфичных событий можно добавить здесь -// или использовать getSocket() в компонентах и вызывать socket.emit / socket.on там. - -// Пример: -// export const sendMessageOnSocket = (messageData) => { -// const currentSocket = getSocket(); -// if (currentSocket) { -// currentSocket.emit('sendMessage', messageData); -// } else { -// console.error('[SocketService] Не могу отправить сообщение: сокет не подключен.'); -// } -// }; - -// export const listenForMessage = (callback) => { -// const currentSocket = getSocket(); -// if (currentSocket) { -// currentSocket.on('getMessage', callback); -// // Возвращаем функцию для отписки -// return () => currentSocket.off('getMessage', callback); -// } -// return () => {}; // Пустая функция отписки, если сокета нет -// }; \ No newline at end of file +// Новая функция для проверки статуса блокировки +export const isAccountBlocked = () => { + return wasAccountBlocked; +}; \ No newline at end of file