diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js new file mode 100644 index 0000000..d551217 --- /dev/null +++ b/backend/controllers/adminController.js @@ -0,0 +1,298 @@ +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 user = await User.findById(userId); + + if (!user) { + return res.status(404).json({ message: 'Пользователь не найден' }); + } + + // Проверка, чтобы нельзя было заблокировать админа + if (user.isAdmin) { + return res.status(403).json({ message: 'Невозможно заблокировать администратора' }); + } + + // Изменяем статус активности на противоположный + user.isActive = !user.isActive; + await user.save(); + + 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 { + const { page = 1, limit = 20, userId } = req.query; + const skip = (page - 1) * limit; + + // Создание фильтра + const filter = {}; + + // Фильтр по пользователю, если указан + if (userId) { + filter.participants = userId; + } + + // Получаем диалоги с пагинацией и данными участников + const conversations = await Conversation.find(filter) + .populate('participants', 'name email photos') + .populate('lastMessage') + .skip(skip) + .limit(parseInt(limit)) + .sort({ updatedAt: -1 }); + + // Получаем общее количество диалогов для пагинации + const total = await Conversation.countDocuments(filter); + + res.json({ + conversations, + 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/conversations/:id/messages + * @access Admin + */ +const getConversationMessages = async (req, res) => { + try { + const conversationId = req.params.id; + const { page = 1, limit = 50 } = req.query; + const skip = (page - 1) * limit; + + // Проверяем существование диалога + const conversation = await Conversation.findById(conversationId); + if (!conversation) { + return res.status(404).json({ message: 'Диалог не найден' }); + } + + // Получаем сообщения с пагинацией + const messages = await Message.find({ conversationId }) + .populate('sender', 'name email') + .skip(skip) + .limit(parseInt(limit)) + .sort({ createdAt: -1 }); + + // Получаем общее количество сообщений для пагинации + const total = await Message.countDocuments({ conversationId }); + + res.json({ + messages, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + }); + } catch (error) { + console.error('Ошибка при получении сообщений диалога:', error); + res.status(500).json({ message: 'Ошибка сервера при получении сообщений диалога' }); + } +}; + +module.exports = { + getAllUsers, + getUserDetails, + toggleUserActive, + getAppStatistics, + getAllConversations, + getConversationMessages +}; \ No newline at end of file diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 279b0bf..766deaf 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -136,11 +136,18 @@ const loginUser = async (req, res, next) => { const isMatch = await user.matchPassword(password); console.log('Результат проверки пароля:', isMatch ? 'Успешно' : 'Неверный пароль'); + // Проверка если аккаунт неактивен (заблокирован) + if (isMatch && !user.isActive && !user.isAdmin) { + res.status(403); + throw new Error('Ваш аккаунт заблокирован. Пожалуйста, обратитесь в поддержку.'); + } + if (isMatch) { res.status(200).json({ _id: user._id, name: user.name, email: user.email, + isAdmin: user.isAdmin || false, // Добавляем информацию о правах администратора token: generateToken(user._id), message: 'Вход выполнен успешно!' }); diff --git a/backend/middleware/adminMiddleware.js b/backend/middleware/adminMiddleware.js new file mode 100644 index 0000000..5f18d79 --- /dev/null +++ b/backend/middleware/adminMiddleware.js @@ -0,0 +1,21 @@ +/** + * Middleware для проверки, является ли пользователь администратором + * Должен использоваться после middleware авторизации (protect), + * который добавляет req.user + */ +const adminMiddleware = (req, res, next) => { + // Проверяем, есть ли пользователь (должен быть добавлен в middleware авторизации) + if (!req.user) { + return res.status(401).json({ message: 'Не авторизован' }); + } + + // Проверяем, является ли пользователь администратором + if (!req.user.isAdmin) { + return res.status(403).json({ message: 'Доступ запрещен. Требуются права администратора' }); + } + + // Если пользователь администратор, продолжаем выполнение запроса + next(); +}; + +module.exports = adminMiddleware; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 86fa06c..a1f3913 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -65,6 +65,10 @@ const userSchema = new mongoose.Schema( type: Boolean, default: true, }, + isAdmin: { + type: Boolean, + default: false, + }, lastSeen: { type: Date, default: Date.now, diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js new file mode 100644 index 0000000..64552c0 --- /dev/null +++ b/backend/routes/adminRoutes.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const adminController = require('../controllers/adminController'); +const { protect } = require('../middleware/authMiddleware'); +const adminMiddleware = require('../middleware/adminMiddleware'); + +// Все маршруты защищены middleware для проверки авторизации и прав администратора +router.use(protect, adminMiddleware); + +// Маршруты для управления пользователями +router.get('/users', adminController.getAllUsers); +router.get('/users/:id', adminController.getUserDetails); +router.put('/users/:id/toggle-active', adminController.toggleUserActive); + +// Маршруты для просмотра статистики +router.get('/statistics', adminController.getAppStatistics); + +// Маршруты для просмотра диалогов и сообщений +router.get('/conversations', adminController.getAllConversations); +router.get('/conversations/:id/messages', adminController.getConversationMessages); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 44579c9..cd6e324 100644 --- a/backend/server.js +++ b/backend/server.js @@ -13,8 +13,10 @@ const connectDBModule = require('./config/db'); const authRoutes = require('./routes/authRoutes'); // <--- 1. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТОТ ИМПОРТ const userRoutes = require('./routes/userRoutes'); // 1. Импортируем userRoutes const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes +const adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа const Message = require('./models/Message'); // Импорт модели Message const Conversation = require('./models/Conversation'); // Импорт модели Conversation +const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта // Загружаем переменные окружения до их использования // dotenv.config(); // Перемещено в начало файла @@ -40,7 +42,15 @@ app.set('io', io); // Подключение к базе данных - проверяем, что импортировали функцию if (typeof connectDBModule === 'function') { try { - connectDBModule(); // Вызов функции подключения + connectDBModule() // Вызов функции подключения + .then(() => { + console.log('Успешное подключение к базе данных'); + // Инициализируем админ-аккаунт после подключения к БД + initAdminAccount(); + }) + .catch(err => { + console.error('Ошибка при подключении к базе данных:', err); + }); console.log('Попытка подключения к базе данных была инициирована...'); // Изменил лог для ясности } catch (err) { // Этот catch здесь, скорее всего, не поймает асинхронные ошибки из connectDBModule, @@ -64,11 +74,12 @@ app.get('/', (req, res) => { res.send('API для Dating App работает!'); }); -// 2. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТИ СТРОКИ для подключения маршрутов аутентификации +// Подключаем маршруты app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); // 2. Подключаем userRoutes с префиксом /api/users app.use('/api/actions', actionRoutes); app.use('/api/conversations', conversationRoutes); +app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели // Socket.IO логика let activeUsers = []; diff --git a/backend/utils/initAdmin.js b/backend/utils/initAdmin.js new file mode 100644 index 0000000..a982c71 --- /dev/null +++ b/backend/utils/initAdmin.js @@ -0,0 +1,39 @@ +const User = require('../models/User'); + +/** + * Инициализирует административный аккаунт в системе + * Аккаунт создается с логином admin и паролем admin124 + * Этот аккаунт будет всегда активен + */ +const initAdminAccount = async () => { + try { + // Проверяем, существует ли уже админ + const adminExists = await User.findOne({ email: 'admin', isAdmin: true }); + + if (!adminExists) { + // Создаем админа, если не существует + const admin = new User({ + name: 'Администратор', + email: 'admin', + password: 'admin124', + dateOfBirth: new Date('1990-01-01'), // Устанавливаем формальную дату рождения + gender: 'other', + isActive: true, + isAdmin: true, + location: { + city: 'Admin', + country: 'System' + } + }); + + await admin.save(); + console.log('Административный аккаунт успешно создан'); + } else { + console.log('Административный аккаунт уже существует'); + } + } catch (error) { + console.error('Ошибка при инициализации админ-аккаунта:', error); + } +}; + +module.exports = initAdminAccount; \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index a1141ad..f67f5d0 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -56,6 +56,48 @@ const routes = [ name: 'Preferences', component: () => import('../views/PreferencesView.vue'), meta: { requiresAuth: true } + }, + // Маршруты для админ-панели + { + path: '/admin', + name: 'AdminDashboard', + component: () => import('../views/admin/AdminDashboard.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + redirect: { name: 'AdminUsers' }, + children: [ + { + path: 'users', + name: 'AdminUsers', + component: () => import('../views/admin/AdminUsers.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'users/:id', + name: 'AdminUserDetail', + component: () => import('../views/admin/AdminUserDetail.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + props: true + }, + { + path: 'conversations', + name: 'AdminConversations', + component: () => import('../views/admin/AdminConversations.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'conversations/:id', + name: 'AdminConversationDetail', + component: () => import('../views/admin/AdminConversationDetail.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + props: true + }, + { + path: 'statistics', + name: 'AdminStatistics', + component: () => import('../views/admin/AdminStatistics.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + } + ] } // ... здесь будут другие маршруты: /profile, /swipe, /chat/:id и т.д. ]; @@ -82,6 +124,12 @@ router.beforeEach(async (to, from, next) => { } } + // Проверка прав администратора + if (to.meta.requiresAdmin && (!user.value || !user.value.isAdmin)) { + console.log(`[Router Guard] Доступ к ${to.path} запрещен. Требуются права администратора.`); + return next({ name: 'Swipe' }); // Перенаправление на обычную страницу пользователей + } + // Проверяем требования маршрута к авторизации if (to.meta.requiresAuth) { if (isAuthenticated.value) { @@ -95,8 +143,13 @@ router.beforeEach(async (to, from, next) => { } else if ((to.name === 'Login' || to.name === 'Register') && isAuthenticated.value) { // Если пользователь уже аутентифицирован и пытается зайти на страницы авторизации - console.log(`[Router Guard] Пользователь аутентифицирован. Перенаправление с ${to.path} на /swipe.`); - next({ name: 'Swipe' }); // Перенаправляем на страницу свайпов вместо главной для лучшего UX + if (user.value && user.value.isAdmin) { + console.log(`[Router Guard] Админ аутентифицирован. Перенаправление с ${to.path} на админ-панель.`); + next({ name: 'AdminDashboard' }); // Перенаправляем на админ-панель для админа + } else { + console.log(`[Router Guard] Пользователь аутентифицирован. Перенаправление с ${to.path} на /swipe.`); + next({ name: 'Swipe' }); // Перенаправляем на страницу свайпов для обычных пользователей + } } else { // Другие страницы, не требующие авторизации diff --git a/src/views/admin/AdminConversationDetail.vue b/src/views/admin/AdminConversationDetail.vue new file mode 100644 index 0000000..cd20325 --- /dev/null +++ b/src/views/admin/AdminConversationDetail.vue @@ -0,0 +1,571 @@ + + + + + \ No newline at end of file diff --git a/src/views/admin/AdminConversations.vue b/src/views/admin/AdminConversations.vue new file mode 100644 index 0000000..db63edc --- /dev/null +++ b/src/views/admin/AdminConversations.vue @@ -0,0 +1,333 @@ + + + + + \ No newline at end of file diff --git a/src/views/admin/AdminDashboard.vue b/src/views/admin/AdminDashboard.vue new file mode 100644 index 0000000..9b28643 --- /dev/null +++ b/src/views/admin/AdminDashboard.vue @@ -0,0 +1,134 @@ + + + + + \ No newline at end of file diff --git a/src/views/admin/AdminStatistics.vue b/src/views/admin/AdminStatistics.vue new file mode 100644 index 0000000..534294b --- /dev/null +++ b/src/views/admin/AdminStatistics.vue @@ -0,0 +1,356 @@ + + + + + \ No newline at end of file diff --git a/src/views/admin/AdminUserDetail.vue b/src/views/admin/AdminUserDetail.vue new file mode 100644 index 0000000..9694e01 --- /dev/null +++ b/src/views/admin/AdminUserDetail.vue @@ -0,0 +1,548 @@ + + + + + \ No newline at end of file diff --git a/src/views/admin/AdminUsers.vue b/src/views/admin/AdminUsers.vue new file mode 100644 index 0000000..a901483 --- /dev/null +++ b/src/views/admin/AdminUsers.vue @@ -0,0 +1,411 @@ + + + + + \ No newline at end of file