Reflex/backend/controllers/adminController.js
Professional 88037f2bf5 фикс
2025-05-26 19:07:11 +07:00

518 lines
22 KiB
JavaScript
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.

const mongoose = require('mongoose');
const User = require('../models/User');
const Conversation = require('../models/Conversation');
const Message = require('../models/Message');
const ProfileView = require('../models/ProfileView');
/**
* @desc Получить список всех пользователей
* @route GET /api/admin/users
* @access Admin
*/
const getAllUsers = async (req, res) => {
try {
const { search, page = 1, limit = 20, isActive } = req.query;
const skip = (page - 1) * limit;
// Создание фильтра
const filter = {};
// Добавляем поиск по имени или email, если указан
if (search) {
filter.$or = [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
];
}
// Фильтр по статусу активности, если указан
if (isActive !== undefined) {
filter.isActive = isActive === 'true';
}
// Исключаем админа из результатов выдачи
filter.isAdmin = { $ne: true };
// Получаем пользователей с пагинацией
const users = await User.find(filter)
.select('-password')
.skip(skip)
.limit(parseInt(limit))
.sort({ createdAt: -1 });
// Получаем общее количество пользователей для пагинации
const total = await User.countDocuments(filter);
res.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Ошибка при получении списка пользователей:', error);
res.status(500).json({ message: 'Ошибка сервера при получении списка пользователей' });
}
};
/**
* @desc Получить детальную информацию о пользователе
* @route GET /api/admin/users/:id
* @access Admin
*/
const getUserDetails = async (req, res) => {
try {
const userId = req.params.id;
const user = await User.findById(userId)
.select('-password')
.lean();
if (!user) {
return res.status(404).json({ message: 'Пользователь не найден' });
}
// Получаем статистику для пользователя
const messagesCount = await Message.countDocuments({ sender: userId });
const conversationsCount = await Conversation.countDocuments({
participants: userId
});
const profileViewsCount = await ProfileView.countDocuments({
profileOwner: userId
});
const matchesCount = user.matches ? user.matches.length : 0;
const likesGivenCount = user.liked ? user.liked.length : 0;
// Добавляем статистику к данным пользователя
const userWithStats = {
...user,
stats: {
messagesCount,
conversationsCount,
profileViewsCount,
matchesCount,
likesGivenCount
}
};
res.json(userWithStats);
} catch (error) {
console.error('Ошибка при получении информации о пользователе:', error);
res.status(500).json({ message: 'Ошибка сервера при получении информации о пользователе' });
}
};
/**
* @desc Заблокировать/разблокировать пользователя
* @route PUT /api/admin/users/:id/toggle-active
* @access Admin
*/
const toggleUserActive = async (req, res) => {
try {
const userId = req.params.id;
const { reason } = req.body; // Добавляем возможность указать причину блокировки
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'Пользователь не найден' });
}
// Проверка, чтобы нельзя было заблокировать админа
if (user.isAdmin) {
return res.status(403).json({ message: 'Невозможно заблокировать администратора' });
}
// Получаем доступ к io для отправки уведомлений
const io = req.app.get('io');
if (!io) {
console.error('[AdminController] Ошибка: экземпляр Socket.IO не доступен через req.app.get("io")');
return res.status(500).json({ message: 'Внутренняя ошибка сервера: Socket.IO недоступен' });
}
// Изменяем статус активности на противоположный
const wasActive = user.isActive;
user.isActive = !user.isActive;
await user.save();
// Получаем глобальный массив активных пользователей из server.js
const activeUsers = req.app.get('activeUsers') || [];
console.log(`[AdminController] Активные пользователи:`, activeUsers);
// Если пользователь был активным и теперь блокируется
if (wasActive && !user.isActive) {
console.log(`[AdminController] Блокировка пользователя ${userId}. Причина: ${reason || 'Не указана'}`);
// Создаем объект с информацией о блокировке
const blockData = {
message: 'Ваш аккаунт был заблокирован администратором.',
reason: reason || 'Нарушение правил сервиса',
timestamp: new Date().toISOString(),
blocked: true,
userId: user._id.toString()
};
// Отправляем глобальное событие блокировки всем клиентам
// Это обеспечит получение уведомления даже если в activeUsers неактуальная информация
io.emit('global_user_blocked', {
userId: user._id.toString(),
timestamp: new Date().toISOString()
});
// Ищем пользователя среди активных пользователей
const userSockets = activeUsers.filter(u => u.userId === userId);
console.log(`[AdminController] Найдено активных записей пользователя: ${userSockets.length}`);
// Пытаемся отправить уведомление через все возможные сокеты
let notificationSent = false;
if (userSockets.length > 0) {
for (const userSocket of userSockets) {
try {
// Безопасно получаем сокет и проверяем его существование
if (io.sockets && io.sockets.sockets) {
const socket = io.sockets.sockets.get(userSocket.socketId);
if (socket && socket.connected) {
console.log(`[AdminController] Отправка уведомления о блокировке на сокет: ${userSocket.socketId}`);
socket.emit("account_blocked", blockData);
notificationSent = true;
// Принудительно отключаем сокет
setTimeout(() => {
try {
console.log(`[AdminController] Принудительное отключение сокета: ${userSocket.socketId}`);
socket.disconnect(true);
} catch (socketError) {
console.error(`[AdminController] Ошибка при отключении сокета ${userSocket.socketId}:`, socketError);
}
}, 500);
} else {
console.log(`[AdminController] Сокет не найден или не подключен для ID: ${userSocket.socketId}`);
}
} else {
console.log(`[AdminController] Коллекция сокетов недоступна`);
}
} catch (socketError) {
console.error(`[AdminController] Ошибка при обработке сокета ${userSocket.socketId}:`, socketError);
}
}
} else {
console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях`);
}
// Очищаем устаревшие записи в списке активных пользователей
try {
const updatedActiveUsers = activeUsers.filter(u => {
// Оставляем записи, которые не принадлежат заблокированному пользователю
return u.userId !== userId;
});
// Обновляем список активных пользователей
req.app.set('activeUsers', updatedActiveUsers);
console.log(`[AdminController] Список активных пользователей обновлен, удалены записи для ${userId}`);
} catch (error) {
console.error(`[AdminController] Ошибка при обновлении списка активных пользователей:`, error);
}
if (!notificationSent) {
console.log(`[AdminController] Уведомление о блокировке не было отправлено напрямую, но было отправлено глобальное уведомление`);
}
}
// Если пользователь был неактивным и теперь разблокируется
else if (!wasActive && user.isActive) {
console.log(`[AdminController] Разблокировка пользователя ${userId}`);
// Формируем объект с информацией о разблокировке для отправки через сокет
const unblockInfo = {
message: 'Ваш аккаунт был разблокирован администратором.',
timestamp: new Date().toISOString(),
userId: user._id.toString(),
unblockType: 'admin_action', // Указываем тип разблокировки
userName: user.name,
unblockData: {
canRestoreSession: true, // Флаг возможности восстановления сессии
}
};
// Отправляем событие разблокировки всем клиентам (анонимно)
io.emit('global_user_unblocked', {
userId: user._id.toString(),
timestamp: new Date().toISOString()
});
// Ищем пользователя среди активных пользователей (на случай, если он онлайн с другим аккаунтом)
const userSockets = activeUsers.filter(u => u.userId === userId);
let notificationSent = false;
if (userSockets.length > 0) {
for (const userSocket of userSockets) {
try {
// Безопасно получаем сокет и проверяем его существование
if (io.sockets && io.sockets.sockets) {
const socket = io.sockets.sockets.get(userSocket.socketId);
if (socket && socket.connected) {
console.log(`[AdminController] Отправка уведомления о разблокировке на сокет: ${userSocket.socketId}`);
socket.emit("account_unblocked", unblockInfo);
notificationSent = true;
} else {
console.log(`[AdminController] Сокет не найден или не подключен для ID: ${userSocket.socketId}`);
}
} else {
console.log(`[AdminController] Коллекция сокетов недоступна`);
}
} catch (socketError) {
console.error(`[AdminController] Ошибка при обработке сокета ${userSocket.socketId}:`, socketError);
}
}
} else {
console.log(`[AdminController] Пользователь ${userId} не найден в активных соединениях для отправки уведомления о разблокировке`);
}
if (!notificationSent) {
console.log(`[AdminController] Уведомление о разблокировке не было отправлено напрямую, но было отправлено глобальное уведомление`);
}
}
res.json({
message: user.isActive ? 'Пользователь разблокирован' : 'Пользователь заблокирован',
isActive: user.isActive
});
} catch (error) {
console.error('Ошибка при изменении статуса пользователя:', error);
res.status(500).json({ message: 'Ошибка сервера при изменении статуса пользователя' });
}
};
/**
* @desc Получить статистику приложения
* @route GET /api/admin/statistics
* @access Admin
*/
const getAppStatistics = async (req, res) => {
try {
// Общая статистика пользователей
const totalUsers = await User.countDocuments({ isAdmin: { $ne: true } });
const activeUsers = await User.countDocuments({ isActive: true, isAdmin: { $ne: true } });
const inactiveUsers = await User.countDocuments({ isActive: false, isAdmin: { $ne: true } });
// Статистика по полу
const maleUsers = await User.countDocuments({ gender: 'male' });
const femaleUsers = await User.countDocuments({ gender: 'female' });
const otherGenderUsers = await User.countDocuments({ gender: 'other' });
// Статистика сообщений и диалогов
const totalMessages = await Message.countDocuments();
const totalConversations = await Conversation.countDocuments();
// Статистика просмотров профилей
const totalProfileViews = await ProfileView.countDocuments();
// Средние значения
const messagesPerUser = totalUsers > 0 ? totalMessages / totalUsers : 0;
const conversationsPerUser = totalUsers > 0 ? totalConversations / totalUsers : 0;
// Новые пользователи за последние 30 дней
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const newUsers30Days = await User.countDocuments({
createdAt: { $gte: thirtyDaysAgo },
isAdmin: { $ne: true }
});
res.json({
users: {
total: totalUsers,
active: activeUsers,
inactive: inactiveUsers,
newIn30Days: newUsers30Days,
genderDistribution: {
male: maleUsers,
female: femaleUsers,
other: otherGenderUsers
}
},
activity: {
totalMessages,
totalConversations,
totalProfileViews,
averages: {
messagesPerUser: messagesPerUser.toFixed(2),
conversationsPerUser: conversationsPerUser.toFixed(2)
}
}
});
} catch (error) {
console.error('Ошибка при получении статистики приложения:', error);
res.status(500).json({ message: 'Ошибка сервера при получении статистики приложения' });
}
};
/**
* @desc Получить список всех диалогов
* @route GET /api/admin/conversations
* @access Admin
*/
const getAllConversations = async (req, res) => {
try {
console.log('[ADMIN] Запрос списка диалогов');
const { page = 1, limit = 20, userId } = req.query;
const skip = (page - 1) * limit;
// Создание фильтра
const filter = {};
// Фильтр по пользователю, если указан
if (userId) {
console.log(`[ADMIN] Фильтр по пользователю: ${userId}`);
if (!mongoose.Types.ObjectId.isValid(userId)) {
console.log(`[ADMIN] Недопустимый формат ID пользователя: ${userId}`);
return res.status(400).json({ message: 'Недопустимый формат ID пользователя' });
}
filter.participants = userId;
}
console.log(`[ADMIN] Поиск диалогов с фильтром:`, filter);
// Получаем диалоги с пагинацией и данными участников
const conversations = await Conversation.find(filter)
.populate('participants', 'name email photos')
.populate('lastMessage')
.skip(skip)
.limit(parseInt(limit))
.sort({ updatedAt: -1 });
console.log(`[ADMIN] Найдено диалогов: ${conversations.length}`);
// Получаем общее количество диалогов для пагинации
const total = await Conversation.countDocuments(filter);
// Добавляем дополнительную информацию о диалогах
const conversationsWithInfo = conversations.map(conv => {
const convObj = conv.toObject();
convObj._id = convObj._id.toString(); // Преобразуем ObjectId в строку для безопасности
return convObj;
});
res.json({
conversations: conversationsWithInfo,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('[ADMIN] Ошибка при получении списка диалогов:', error);
res.status(500).json({ message: 'Ошибка сервера при получении списка диалогов' });
}
};
/**
* @desc Получить сообщения диалога
* @route GET /api/admin/conversations/:id/messages
* @access Admin
*/
const getConversationMessages = async (req, res) => {
try {
const conversationId = req.params.id;
console.log(`[ADMIN] Запрос сообщений для диалога с ID: ${conversationId}`);
if (!mongoose.Types.ObjectId.isValid(conversationId)) {
console.log(`[ADMIN] Недопустимый формат ID диалога: ${conversationId}`);
return res.status(400).json({ message: 'Недопустимый формат ID диалога' });
}
const { page = 1, limit = 50 } = req.query;
const skip = (page - 1) * limit;
// Проверяем существование диалога с подробным логированием
console.log(`[ADMIN] Поиск диалога с ID: ${conversationId}`);
const conversation = await Conversation.findById(conversationId);
if (!conversation) {
console.log(`[ADMIN] Диалог с ID ${conversationId} не найден в базе данных`);
return res.status(404).json({ message: 'Диалог не найден' });
}
console.log(`[ADMIN] Диалог найден, участники: ${conversation.participants}`);
// Получаем сообщения с пагинацией
const messages = await Message.find({ conversationId })
.populate('sender', 'name email')
.skip(skip)
.limit(parseInt(limit))
.sort({ createdAt: -1 });
console.log(`[ADMIN] Найдено сообщений: ${messages.length}`);
// Получаем общее количество сообщений для пагинации
const total = await Message.countDocuments({ conversationId });
res.json({
messages,
conversation: {
id: conversation._id,
participants: conversation.participants
},
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('[ADMIN] Ошибка при получении сообщений диалога:', error);
res.status(500).json({ message: 'Ошибка сервера при получении сообщений диалога' });
}
};
/**
* @desc Получить диалог по ID
* @route GET /api/admin/conversations/:id
* @access Admin
*/
const getConversationById = async (req, res) => {
try {
const conversationId = req.params.id;
console.log(`[ADMIN] Запрос диалога с ID: ${conversationId}`);
if (!mongoose.Types.ObjectId.isValid(conversationId)) {
console.log(`[ADMIN] Недопустимый формат ID диалога: ${conversationId}`);
return res.status(400).json({ message: 'Недопустимый формат ID диалога' });
}
// Получаем диалог с данными участников
const conversation = await Conversation.findById(conversationId)
.populate('participants', 'name email photos')
.populate('lastMessage');
if (!conversation) {
console.log(`[ADMIN] Диалог с ID ${conversationId} не найден в базе данных`);
return res.status(404).json({ message: 'Диалог не найден' });
}
console.log(`[ADMIN] Диалог найден, возвращаем данные`);
res.json(conversation);
} catch (error) {
console.error('[ADMIN] Ошибка при получении диалога:', error);
res.status(500).json({ message: 'Ошибка сервера при получении диалога' });
}
};
module.exports = {
getAllUsers,
getUserDetails,
toggleUserActive,
getAppStatistics,
getAllConversations,
getConversationById,
getConversationMessages
};