From 0156b554e7c12ca2e9482376c1bee70a5ef0bad2 Mon Sep 17 00:00:00 2001 From: Professional Date: Mon, 26 May 2025 00:47:32 +0700 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B6=D0=B0=D0=BB=D0=BE=D0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/reportController.js | 246 +++++++++++++++++ backend/models/Report.js | 80 ++++++ backend/routes/adminRoutes.js | 12 + backend/routes/reportRoutes.js | 13 + backend/server.js | 2 + src/components/ReportModal.vue | 352 ++++++++++++++++++++++++ src/services/api.js | 22 ++ src/views/SwipeView.vue | 69 +++++ src/views/UserProfileView.vue | 47 ++++ 9 files changed, 843 insertions(+) create mode 100644 backend/controllers/reportController.js create mode 100644 backend/models/Report.js create mode 100644 backend/routes/reportRoutes.js create mode 100644 src/components/ReportModal.vue diff --git a/backend/controllers/reportController.js b/backend/controllers/reportController.js new file mode 100644 index 0000000..0c2d557 --- /dev/null +++ b/backend/controllers/reportController.js @@ -0,0 +1,246 @@ +const Report = require('../models/Report'); +const User = require('../models/User'); + +// @desc Подать жалобу на пользователя +// @route POST /api/reports +// @access Private +const createReport = async (req, res, next) => { + try { + const { reportedUserId, reason, description } = req.body; + const reporterId = req.user._id; + + // Проверяем, что пользователь не подает жалобу на самого себя + if (reporterId.equals(reportedUserId)) { + const error = new Error('Вы не можете подать жалобу на самого себя'); + error.statusCode = 400; + return next(error); + } + + // Проверяем, существует ли пользователь, на которого подается жалоба + const reportedUser = await User.findById(reportedUserId); + if (!reportedUser) { + const error = new Error('Пользователь не найден'); + error.statusCode = 404; + return next(error); + } + + // Проверяем, нет ли уже жалобы от этого пользователя на этого же пользователя + const existingReport = await Report.findOne({ + reporter: reporterId, + reportedUser: reportedUserId + }); + + if (existingReport) { + const error = new Error('Вы уже подавали жалобу на этого пользователя'); + error.statusCode = 400; + return next(error); + } + + // Создаем новую жалобу + const newReport = new Report({ + reporter: reporterId, + reportedUser: reportedUserId, + reason, + description: description || '' + }); + + await newReport.save(); + + console.log(`[REPORT_CTRL] Создана жалоба ${newReport._id} от пользователя ${reporterId} на пользователя ${reportedUserId}`); + + res.status(201).json({ + message: 'Жалоба успешно подана. Мы рассмотрим её в ближайшее время.', + reportId: newReport._id + }); + + } catch (error) { + console.error('[REPORT_CTRL] Ошибка при создании жалобы:', error.message); + next(error); + } +}; + +// @desc Получить список всех жалоб (для админов) +// @route GET /api/admin/reports +// @access Admin +const getAllReports = async (req, res, next) => { + try { + const { page = 1, limit = 20, status = 'all' } = req.query; + const skip = (page - 1) * limit; + + // Строим фильтр + let filter = {}; + if (status !== 'all') { + filter.status = status; + } + + // Получаем жалобы с информацией о пользователях + const reports = await Report.find(filter) + .populate('reporter', 'name email') + .populate('reportedUser', 'name email isActive') + .populate('reviewedBy', 'name email') + .skip(skip) + .limit(parseInt(limit)) + .sort({ createdAt: -1 }); + + // Получаем общее количество жалоб + const total = await Report.countDocuments(filter); + + res.json({ + reports, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + }); + + } catch (error) { + console.error('[REPORT_CTRL] Ошибка при получении списка жалоб:', error.message); + next(error); + } +}; + +// @desc Получить детали жалобы +// @route GET /api/admin/reports/:id +// @access Admin +const getReportById = async (req, res, next) => { + try { + const { id } = req.params; + + const report = await Report.findById(id) + .populate('reporter', 'name email photos') + .populate('reportedUser', 'name email photos isActive') + .populate('reviewedBy', 'name email'); + + if (!report) { + const error = new Error('Жалоба не найдена'); + error.statusCode = 404; + return next(error); + } + + res.json(report); + + } catch (error) { + console.error('[REPORT_CTRL] Ошибка при получении деталей жалобы:', error.message); + next(error); + } +}; + +// @desc Обновить статус жалобы +// @route PUT /api/admin/reports/:id +// @access Admin +const updateReportStatus = async (req, res, next) => { + try { + const { id } = req.params; + const { status, adminComment, actionTaken } = req.body; + const adminId = req.user._id; + + const report = await Report.findById(id); + if (!report) { + const error = new Error('Жалоба не найдена'); + error.statusCode = 404; + return next(error); + } + + // Обновляем жалобу + report.status = status; + report.reviewedBy = adminId; + report.reviewedAt = new Date(); + + if (adminComment) { + report.adminComment = adminComment; + } + + if (actionTaken) { + report.actionTaken = actionTaken; + } + + await report.save(); + + // Если было предпринято действие, применяем его к пользователю + if (actionTaken && actionTaken !== 'none') { + const reportedUser = await User.findById(report.reportedUser); + if (reportedUser) { + switch (actionTaken) { + case 'temporary_ban': + case 'permanent_ban': + case 'profile_removal': + reportedUser.isActive = false; + await reportedUser.save(); + console.log(`[REPORT_CTRL] Пользователь ${reportedUser._id} заблокирован по жалобе ${id}`); + break; + } + } + } + + console.log(`[REPORT_CTRL] Жалоба ${id} обновлена администратором ${adminId}`); + + res.json({ + message: 'Жалоба успешно обновлена', + report: await Report.findById(id) + .populate('reporter', 'name email') + .populate('reportedUser', 'name email isActive') + .populate('reviewedBy', 'name email') + }); + + } catch (error) { + console.error('[REPORT_CTRL] Ошибка при обновлении жалобы:', error.message); + next(error); + } +}; + +// @desc Получить статистику жалоб +// @route GET /api/admin/reports/stats +// @access Admin +const getReportsStats = async (req, res, next) => { + try { + // Общее количество жалоб + const totalReports = await Report.countDocuments(); + + // Жалобы по статусам + const pendingReports = await Report.countDocuments({ status: 'pending' }); + const reviewedReports = await Report.countDocuments({ status: 'reviewed' }); + const resolvedReports = await Report.countDocuments({ status: 'resolved' }); + const dismissedReports = await Report.countDocuments({ status: 'dismissed' }); + + // Жалобы за последние 30 дней + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const recentReports = await Report.countDocuments({ + createdAt: { $gte: thirtyDaysAgo } + }); + + // Топ причин жалоб + const topReasons = await Report.aggregate([ + { $group: { _id: '$reason', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 5 } + ]); + + const stats = { + total: totalReports, + byStatus: { + pending: pendingReports, + reviewed: reviewedReports, + resolved: resolvedReports, + dismissed: dismissedReports + }, + recent: recentReports, + topReasons + }; + + res.json(stats); + + } catch (error) { + console.error('[REPORT_CTRL] Ошибка при получении статистики жалоб:', error.message); + next(error); + } +}; + +module.exports = { + createReport, + getAllReports, + getReportById, + updateReportStatus, + getReportsStats +}; \ No newline at end of file diff --git a/backend/models/Report.js b/backend/models/Report.js new file mode 100644 index 0000000..8524248 --- /dev/null +++ b/backend/models/Report.js @@ -0,0 +1,80 @@ +const mongoose = require('mongoose'); + +const reportSchema = new mongoose.Schema({ + // Кто подает жалобу + reporter: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + + // На кого подается жалоба + reportedUser: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + + // Причина жалобы + reason: { + type: String, + required: true, + enum: [ + 'inappropriate_content', + 'fake_profile', + 'harassment', + 'spam', + 'offensive_behavior', + 'underage', + 'other' + ] + }, + + // Дополнительное описание + description: { + type: String, + maxlength: 500 + }, + + // Статус жалобы + status: { + type: String, + enum: ['pending', 'reviewed', 'resolved', 'dismissed'], + default: 'pending' + }, + + // Администратор, который рассмотрел жалобу + reviewedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + + // Дата рассмотрения + reviewedAt: { + type: Date + }, + + // Комментарий администратора + adminComment: { + type: String, + maxlength: 500 + }, + + // Действие, предпринятое администратором + actionTaken: { + type: String, + enum: ['none', 'warning', 'temporary_ban', 'permanent_ban', 'profile_removal'] + } +}, { + timestamps: true +}); + +// Индексы для оптимизации запросов +reportSchema.index({ reporter: 1, reportedUser: 1 }); +reportSchema.index({ reportedUser: 1, status: 1 }); +reportSchema.index({ status: 1, createdAt: -1 }); + +// Предотвращаем дублирование жалоб от одного пользователя на другого +reportSchema.index({ reporter: 1, reportedUser: 1 }, { unique: true }); + +module.exports = mongoose.model('Report', reportSchema); \ No newline at end of file diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 64552c0..b4eb23a 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -3,6 +3,12 @@ const router = express.Router(); const adminController = require('../controllers/adminController'); const { protect } = require('../middleware/authMiddleware'); const adminMiddleware = require('../middleware/adminMiddleware'); +const { + getAllReports, + getReportById, + updateReportStatus, + getReportsStats +} = require('../controllers/reportController'); // Все маршруты защищены middleware для проверки авторизации и прав администратора router.use(protect, adminMiddleware); @@ -15,6 +21,12 @@ router.put('/users/:id/toggle-active', adminController.toggleUserActive); // Маршруты для просмотра статистики router.get('/statistics', adminController.getAppStatistics); +// Маршруты для управления жалобами +router.get('/reports', getAllReports); +router.get('/reports/stats', getReportsStats); +router.get('/reports/:id', getReportById); +router.put('/reports/:id', updateReportStatus); + // Маршруты для просмотра диалогов и сообщений router.get('/conversations', adminController.getAllConversations); router.get('/conversations/:id/messages', adminController.getConversationMessages); diff --git a/backend/routes/reportRoutes.js b/backend/routes/reportRoutes.js new file mode 100644 index 0000000..a47d58f --- /dev/null +++ b/backend/routes/reportRoutes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const { protect } = require('../middleware/authMiddleware'); +const { + createReport +} = require('../controllers/reportController'); + +// @desc Подать жалобу на пользователя +// @route POST /api/reports +// @access Private +router.post('/', protect, createReport); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index cd6e324..a25a789 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,6 +14,7 @@ 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 reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб const Message = require('./models/Message'); // Импорт модели Message const Conversation = require('./models/Conversation'); // Импорт модели Conversation const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта @@ -79,6 +80,7 @@ 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/reports', reportRoutes); // Подключаем маршруты для жалоб app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели // Socket.IO логика diff --git a/src/components/ReportModal.vue b/src/components/ReportModal.vue new file mode 100644 index 0000000..3a84a41 --- /dev/null +++ b/src/components/ReportModal.vue @@ -0,0 +1,352 @@ + + + + + \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index d67bbc8..971e240 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -121,5 +121,27 @@ export default { // Новый метод для записи просмотра профиля recordProfileView(userId, source = 'other') { return apiClient.post(`/users/${userId}/view`, { source }); + }, + + // Методы для работы с жалобами + createReport(reportData) { + return apiClient.post('/reports', reportData); + }, + + // Админские методы для жалоб + getReports(params = {}) { + return apiClient.get('/admin/reports', { params }); + }, + + getReportById(reportId) { + return apiClient.get(`/admin/reports/${reportId}`); + }, + + updateReportStatus(reportId, updateData) { + return apiClient.put(`/admin/reports/${reportId}`, updateData); + }, + + getReportsStats() { + return apiClient.get('/admin/reports/stats'); } }; \ No newline at end of file diff --git a/src/views/SwipeView.vue b/src/views/SwipeView.vue index 8b0ad8f..b028f81 100644 --- a/src/views/SwipeView.vue +++ b/src/views/SwipeView.vue @@ -140,6 +140,15 @@

{{ user.name }}, {{ user.age || '?' }}

{{ formatGender(user.gender) }}

+ + + @@ -181,6 +190,15 @@ class="overlay" @click="continueSearching" > + + + @@ -189,6 +207,7 @@ import { ref, onMounted, computed, reactive, watch, onUnmounted, nextTick } from import api from '@/services/api'; import { useAuth } from '@/auth'; import router from '@/router'; +import ReportModal from '@/components/ReportModal.vue'; const { isAuthenticated, logout } = useAuth(); @@ -202,6 +221,10 @@ const matchOccurred = ref(false); const matchedUserId = ref(null); const swipeArea = ref(null); +// Для системы жалоб +const showReportModal = ref(false); +const selectedUserForReport = ref(null); + // Для карусели фото const currentPhotoIndex = reactive({}); @@ -797,6 +820,22 @@ watch(currentUserToSwipe, async (newUser, oldUser) => { } } }); + +// Методы для работы с модальным окном жалоб +const openReportModal = (user) => { + selectedUserForReport.value = user; + showReportModal.value = true; +}; + +const closeReportModal = () => { + showReportModal.value = false; + selectedUserForReport.value = null; +}; + +const handleReportSubmitted = () => { + // Просто закрываем модальное окно, оно само покажет сообщение об успехе + console.log('[SwipeView] Жалоба отправлена'); +};