This commit is contained in:
Professional 2025-05-26 16:56:49 +07:00
parent f7d0d71858
commit 920168e7d1
4 changed files with 227 additions and 138 deletions

View File

@ -135,64 +135,63 @@ const toggleUserActive = async (req, res) => {
// Если пользователь был активным и теперь блокируется // Если пользователь был активным и теперь блокируется
if (wasActive && !user.isActive) { if (wasActive && !user.isActive) {
// Находим активное соединение пользователя console.log(`[AdminController] Блокировка пользователя ${userId}. Причина: ${reason || 'Не указана'}`);
const activeUsers = Array.from(io.sockets.sockets).map(socket => socket[1]);
const userSockets = activeUsers.filter(socket => { // Находим все активные сокеты пользователя - новый подход с более надежным поиском
const userData = socket.handshake.query; const activeSockets = Array.from(io.sockets.sockets.values()).filter(socket => {
return userData && userData.userId === userId; return socket &&
socket.handshake &&
socket.handshake.query &&
socket.handshake.query.userId === userId;
}); });
// Получаем информацию о пользователе из массива activeUsers console.log(`[AdminController] Найдено активных сокетов пользователя: ${activeSockets.length}`);
// (этот массив хранится в 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);
// Находим сокет пользователя if (activeSockets.length > 0) {
const userSocketInfo = activeUsersArray.find(u => u.userId === userId); // Отправляем уведомление по всем активным сокетам пользователя
activeSockets.forEach(socket => {
// Если пользователь онлайн, отправляем уведомление о блокировке console.log(`[AdminController] Отправка уведомления о блокировке на сокет: ${socket.id}`);
if (userSocketInfo && userSocketInfo.socketId) { socket.emit("account_blocked", {
io.to(userSocketInfo.socketId).emit("account_blocked", {
message: 'Ваш аккаунт был заблокирован администратором.', message: 'Ваш аккаунт был заблокирован администратором.',
reason: reason || 'Нарушение правил сервиса', reason: reason || 'Нарушение правил сервиса',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
blocked: true
}); });
console.log(`[AdminController] Отправлено уведомление о блокировке пользователю ${userId}`); });
// Принудительно отключаем все сокеты пользователя
setTimeout(() => {
activeSockets.forEach(socket => {
console.log(`[AdminController] Принудительное отключение сокета: ${socket.id}`);
socket.disconnect(true);
});
}, 1000); // Небольшая задержка, чтобы сообщение о блокировке успело доставиться
} else { } else {
// Если пользователь не в сети, просто оставляем лог console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях`);
console.log(`[AdminController] Пользователь ${userId} заблокирован, но не находится в сети для отправки уведомления`);
} }
} }
// Если пользователь был неактивным и теперь разблокируется // Если пользователь был неактивным и теперь разблокируется
else if (!wasActive && user.isActive) { else if (!wasActive && user.isActive) {
// Используем тот же подход для поиска сокетов пользователя console.log(`[AdminController] Разблокировка пользователя ${userId}`);
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);
// Находим сокет пользователя (если он вдруг залогинился с другим статусом) // То же самое для поиска сокетов пользователя
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 (activeSockets.length > 0) {
if (userSocketInfo && userSocketInfo.socketId) { activeSockets.forEach(socket => {
io.to(userSocketInfo.socketId).emit("account_unblocked", { console.log(`[AdminController] Отправка уведомления о разблокировке на сокет: ${socket.id}`);
socket.emit("account_unblocked", {
message: 'Ваш аккаунт был разблокирован администратором.', message: 'Ваш аккаунт был разблокирован администратором.',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
console.log(`[AdminController] Отправлено уведомление о разблокировке пользователю ${userId}`); });
} else { } else {
console.log(`[AdminController] Пользователь ${userId} разблокирован, но не находится в сети для отправки уведомления`); console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях для отправки уведомления о разблокировке`);
} }
} }

View File

@ -129,8 +129,9 @@ async function logout() {
// Функция обработки события блокировки аккаунта // Функция обработки события блокировки аккаунта
function handleAccountBlocked(data) { function handleAccountBlocked(data) {
console.log('Обработка блокировки аккаунта:', data); console.log('[Auth] Экстренная обработка блокировки аккаунта:', data);
try {
// Создаем объект с информацией о блокировке // Создаем объект с информацией о блокировке
const blockInfo = { const blockInfo = {
blocked: true, blocked: true,
@ -142,24 +143,50 @@ function handleAccountBlocked(data) {
// Сохраняем информацию о блокировке в localStorage для отображения на странице логина // Сохраняем информацию о блокировке в localStorage для отображения на странице логина
localStorage.setItem('accountBlockedInfo', JSON.stringify(blockInfo)); localStorage.setItem('accountBlockedInfo', JSON.stringify(blockInfo));
// Немедленно выполняем выход пользователя - сначала очищаем данные // Немедленно очищаем все данные пользователя
console.log('[Auth] Очистка данных пользователя из-за блокировки');
user.value = null; user.value = null;
token.value = null; token.value = null;
// Очистка localStorage - удаляем все ключи, которые могут содержать пользовательские данные
localStorage.removeItem('userToken'); localStorage.removeItem('userToken');
localStorage.removeItem('userSettings');
localStorage.removeItem('userPreferences');
// Удаляем токен из заголовков для будущих запросов API // Удаляем токены доступа для API запросов
if (typeof api.setAuthToken === 'function') {
api.setAuthToken(null); api.setAuthToken(null);
console.log('[Auth] Токен авторизации API удален');
}
// Принудительно прерываем все активные запросы API, если они есть // Пробуем отменить активные запросы
api.cancelActiveRequests && api.cancelActiveRequests(); if (typeof api.cancelActiveRequests === 'function') {
api.cancelActiveRequests();
console.log('[Auth] Активные запросы API отменены');
}
// Перенаправляем на страницу входа с уведомлением о блокировке // Устанавливаем флаг в SessionStorage для индикации блокировки при перезагрузке страницы
router.push({ sessionStorage.setItem('forcedLogout', 'blocked');
name: 'Login',
query: { blocked: 'true' }
});
console.log('Пользователь был разлогинен из-за блокировки аккаунта.'); // Принудительный перенаправление на страницу логина с флагом блокировки
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;
}
} }
// Функция обработки события разблокировки аккаунта // Функция обработки события разблокировки аккаунта

View File

@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { useAuth } from '../auth'; // Импортируем функцию авторизации import { useAuth } from '../auth'; // Импортируем функцию авторизации
import { isAccountBlocked } from './socketService'; // Импортируем проверку блокировки из socketService
// Создаем экземпляр Axios с базовым URL нашего API // Создаем экземпляр Axios с базовым URL нашего API
// Настраиваем базовый URL в зависимости от окружения // Настраиваем базовый URL в зависимости от окружения
@ -7,6 +8,31 @@ import { useAuth } from '../auth'; // Импортируем функцию ав
// Храним токены отмены запросов для возможности отмены при блокировке // Храним токены отмены запросов для возможности отмены при блокировке
const cancelTokenSources = new Map(); 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 в зависимости от среды выполнения // Определяем базовый URL в зависимости от среды выполнения
const getBaseUrl = () => { const getBaseUrl = () => {
// Всегда используем относительный путь /api. // Всегда используем относительный путь /api.
@ -26,6 +52,24 @@ const apiClient = axios.create({
// (Опционально, но очень полезно) Перехватчик для добавления JWT токена к запросам // (Опционально, но очень полезно) Перехватчик для добавления JWT токена к запросам
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { (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 const token = localStorage.getItem('userToken'); // Предполагаем, что токен хранится в localStorage
if (token) { if (token) {
console.log('[API] Добавление токена к запросу:', config.url); console.log('[API] Добавление токена к запросу:', config.url);
@ -42,11 +86,6 @@ apiClient.interceptors.request.use(
const requestKey = `${config.method}-${config.url}`; const requestKey = `${config.method}-${config.url}`;
cancelTokenSources.set(requestKey, source); cancelTokenSources.set(requestKey, source);
// Добавляем обработчик, который удаляет источник токена отмены после завершения запроса
config.onFinally = () => {
cancelTokenSources.delete(requestKey);
};
return config; return config;
}, },
(error) => { (error) => {
@ -70,6 +109,27 @@ apiClient.interceptors.response.use(
cancelTokenSources.delete(requestKey); 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) { if (error.response && error.response.status === 401) {
console.error('[API] Неавторизованный запрос (401):', error.config.url); console.error('[API] Неавторизованный запрос (401):', error.config.url);
@ -108,6 +168,7 @@ export default {
// Добавляем новые утилитарные методы // Добавляем новые утилитарные методы
cancelActiveRequests, cancelActiveRequests,
setAuthToken, setAuthToken,
checkAccountBlockStatus,
register(userData) { register(userData) {
return apiClient.post('/auth/register', userData); return apiClient.post('/auth/register', userData);

View File

@ -1,33 +1,28 @@
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { useAuth } from '@/auth'; // Чтобы получить ID текущего пользователя import { useAuth } from '@/auth';
let socket; let socket;
// const SOCKET_URL = 'http://localhost:5000'; // Закомментируем, так как Vite будет проксировать // Для блокировки аккаунта, добавляем состояние
let wasAccountBlocked = false;
// const SOCKET_URL = 'http://localhost:5000';
// Для разработки через Vite proxy, URL должен быть относительным к домену фронтенда // Для разработки через Vite proxy, URL должен быть относительным к домену фронтенда
const SOCKET_URL = '/'; // Vite будет перехватывать /socket.io/ путь const SOCKET_URL = '/';
export const connectSocket = () => { export const connectSocket = () => {
const { user, isAuthenticated, handleAccountBlocked, handleAccountUnblocked } = useAuth(); const { user, isAuthenticated, handleAccountBlocked, handleAccountUnblocked } = useAuth();
if (wasAccountBlocked) {
console.log('[SocketService] Попытка подключения отклонена - аккаунт был заблокирован');
return null;
}
if (isAuthenticated.value && user.value && user.value._id) { if (isAuthenticated.value && user.value && user.value._id) {
// Подключаемся только если пользователь аутентифицирован и есть 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, { socket = io(SOCKET_URL, {
path: '/socket.io/', // Явно указываем путь, который будет проксироваться path: '/socket.io/',
transports: ['websocket', 'polling'] // Явно указываем транспорты для лучшей совместимости transports: ['websocket', 'polling']
}); });
console.log('[SocketService] Попытка подключения к Socket.IO серверу через Vite proxy...'); console.log('[SocketService] Попытка подключения к Socket.IO серверу через Vite proxy...');
@ -40,32 +35,60 @@ export const connectSocket = () => {
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.log('[SocketService] Отключен от Socket.IO. Причина:', 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) => { socket.on('connect_error', (error) => {
console.error('[SocketService] Ошибка подключения к Socket.IO:', error.message, error.data); console.error('[SocketService] Ошибка подключения к Socket.IO:', error.message, error.data);
}); });
// Добавляем обработчик для события блокировки аккаунта // ПЕРЕРАБОТАННЫЙ обработчик блокировки аккаунта
socket.on('account_blocked', (data) => { socket.on('account_blocked', (data) => {
console.log('[SocketService] Получено уведомление о блокировке аккаунта:', data); console.log('[SocketService] Получено уведомление о блокировке аккаунта:', data);
// Немедленно отключаем сокет // Устанавливаем флаг блокировки аккаунта для предотвращения повторных подключений
wasAccountBlocked = true;
// Принудительно прерываем соединение
try {
console.log('[SocketService] Принудительное отключение сокета из-за блокировки аккаунта');
socket.disconnect(); socket.disconnect();
console.log('[SocketService] Сокет принудительно отключен из-за блокировки аккаунта'); } catch (error) {
console.error('[SocketService] Ошибка при отключении сокета:', error);
}
// Сразу вызываем функцию обработки блокировки из auth.js, без таймаута // Вызываем обработчик блокировки для немедленного выхода
if (typeof handleAccountBlocked === 'function') {
// Оборачиваем в try-catch для надежности
try {
handleAccountBlocked(data); 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 = null;
}); });
// Добавляем обработчик для события разблокировки аккаунта // Обработчик разблокировки аккаунта
socket.on('account_unblocked', (data) => { socket.on('account_unblocked', (data) => {
console.log('[SocketService] Получено уведомление о разблокировке аккаунта:', data); console.log('[SocketService] Получено уведомление о разблокировке аккаунта:', data);
wasAccountBlocked = false;
// Сразу вызываем обработчик разблокировки, без таймаута
if (typeof handleAccountUnblocked === 'function') { if (typeof handleAccountUnblocked === 'function') {
handleAccountUnblocked(data); handleAccountUnblocked(data);
} else { } else {
@ -73,12 +96,6 @@ export const connectSocket = () => {
} }
}); });
// Можно здесь же слушать глобальные события, если нужно
// socket.on('getUsers', (users) => {
// console.log('[SocketService] Получен список активных пользователей:', users);
// // Тут можно обновить какой-нибудь стор активных пользователей
// });
return socket; return socket;
} else { } else {
console.warn('[SocketService] Не удалось подключиться: пользователь не аутентифицирован или нет ID.'); console.warn('[SocketService] Не удалось подключиться: пользователь не аутентифицирован или нет ID.');
@ -95,34 +112,19 @@ export const disconnectSocket = () => {
}; };
export const getSocket = () => { export const getSocket = () => {
// Если аккаунт был заблокирован, возвращаем null
if (wasAccountBlocked) {
console.warn('[SocketService] getSocket: Аккаунт заблокирован, сокет недоступен');
return null;
}
if (!socket) { if (!socket) {
console.warn('[SocketService] getSocket вызван, но сокет не инициализирован. Попытка подключения...'); console.warn('[SocketService] getSocket вызван, но сокет не инициализирован. Попытка подключения...');
// Можно попробовать подключиться здесь, если это желаемое поведение,
// но лучше управлять подключением более явно.
// return connectSocket(); // Будь осторожен с рекурсией или множественными подключениями
} }
return socket; return socket;
}; };
// Функции для отправки и прослушивания специфичных событий можно добавить здесь // Новая функция для проверки статуса блокировки
// или использовать getSocket() в компонентах и вызывать socket.emit / socket.on там. export const isAccountBlocked = () => {
return wasAccountBlocked;
// Пример: };
// 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 () => {}; // Пустая функция отписки, если сокета нет
// };