From 2317ecafcac9d029ea67732a13896ac5c9ff384b Mon Sep 17 00:00:00 2001 From: Professional Date: Mon, 26 May 2025 17:45: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=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=B6=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/deviceSecurityController.js | 361 +++++ backend/middleware/deviceBlockMiddleware.js | 231 +++ backend/models/BlockedDevice.js | 275 ++++ backend/routes/adminRoutes.js | 12 + backend/routes/authRoutes.js | 14 +- backend/routes/securityRoutes.js | 19 + backend/server.js | 2 + src/auth.js | 86 +- src/components/BlockedDevices.vue | 1033 +++++++++++++ .../admin/BlockedDevicesManagement.vue | 1282 +++++++++++++++++ src/main.js | 19 +- src/router/index.js | 14 +- src/services/adminDeviceSecurityService.js | 235 +++ src/services/api.js | 170 ++- src/services/deviceSecurity.js | 429 ++++++ src/services/deviceSecurityService.js | 338 +++++ src/utils/deviceFingerprint.js | 394 +++++ src/views/DeviceBlockedPage.vue | 666 +++++++++ src/views/LoginView.vue | 173 ++- src/views/admin/AdminBlockedDevices.vue | 682 +++++++++ 20 files changed, 6362 insertions(+), 73 deletions(-) create mode 100644 backend/controllers/deviceSecurityController.js create mode 100644 backend/middleware/deviceBlockMiddleware.js create mode 100644 backend/models/BlockedDevice.js create mode 100644 backend/routes/securityRoutes.js create mode 100644 src/components/BlockedDevices.vue create mode 100644 src/components/admin/BlockedDevicesManagement.vue create mode 100644 src/services/adminDeviceSecurityService.js create mode 100644 src/services/deviceSecurity.js create mode 100644 src/services/deviceSecurityService.js create mode 100644 src/utils/deviceFingerprint.js create mode 100644 src/views/DeviceBlockedPage.vue create mode 100644 src/views/admin/AdminBlockedDevices.vue diff --git a/backend/controllers/deviceSecurityController.js b/backend/controllers/deviceSecurityController.js new file mode 100644 index 0000000..8af2cc0 --- /dev/null +++ b/backend/controllers/deviceSecurityController.js @@ -0,0 +1,361 @@ +const BlockedDevice = require('../models/BlockedDevice'); +const User = require('../models/User'); +const { blockUserDevice, unblockDevice, getBlockedDevices } = require('../middleware/deviceBlockMiddleware'); + +/** + * Контроллер для управления блокировками устройств + */ + +// @desc Проверка блокировки устройства +// @route POST /api/security/check-device +// @access Public +const checkDeviceBlockStatus = async (req, res) => { + try { + const { fingerprint, deviceInfo } = req.body; + const deviceFingerprint = fingerprint || req.deviceFingerprint; + + if (!deviceFingerprint) { + return res.status(400).json({ + message: 'Device fingerprint требуется для проверки безопасности', + code: 'DEVICE_FINGERPRINT_REQUIRED' + }); + } + + console.log('[SECURITY] Проверка блокировки устройства:', deviceFingerprint.substring(0, 16) + '...'); + + // Проверяем, заблокировано ли устройство + const blockedDevice = await BlockedDevice.isDeviceBlocked(deviceFingerprint); + + if (blockedDevice) { + console.log('[SECURITY] Устройство заблокировано:', blockedDevice._id); + + // Записываем попытку доступа + await blockedDevice.addBypassAttempt({ + type: 'api_request', + ipAddress: req.ip || req.connection.remoteAddress, + userAgent: req.headers['user-agent'] + }); + + return res.status(403).json({ + blocked: true, + message: 'Доступ с данного устройства заблокирован', + reason: blockedDevice.reason, + blockedAt: blockedDevice.blockedAt, + code: 'DEVICE_BLOCKED' + }); + } + + // Обновляем информацию об устройстве + if (deviceInfo) { + // Можно сохранить информацию об активном устройстве для мониторинга + console.log('[SECURITY] Активное устройство:', { + fingerprint: deviceFingerprint.substring(0, 16) + '...', + userAgent: deviceInfo.userAgent, + platform: deviceInfo.platform + }); + } + + res.json({ + blocked: false, + message: 'Устройство не заблокировано' + }); + + } catch (error) { + console.error('[SECURITY] Ошибка при проверке блокировки устройства:', error); + res.status(500).json({ + message: 'Ошибка системы безопасности', + code: 'SECURITY_CHECK_ERROR' + }); + } +}; + +// @desc Отчет о подозрительной активности +// @route POST /api/security/report-suspicious +// @access Private +const reportSuspiciousActivity = async (req, res) => { + try { + const { activities, deviceInfo } = req.body; + const deviceFingerprint = req.deviceFingerprint; + + if (!deviceFingerprint) { + return res.status(400).json({ + message: 'Device fingerprint требуется для отчета', + code: 'DEVICE_FINGERPRINT_REQUIRED' + }); + } + + console.log('[SECURITY] Получен отчет о подозрительной активности:', { + fingerprint: deviceFingerprint.substring(0, 16) + '...', + activitiesCount: activities?.length || 0, + userId: req.user?._id + }); + + // Анализируем активность на предмет угроз + const riskScore = calculateRiskScore(activities, deviceInfo); + + if (riskScore > 80) { + console.warn('[SECURITY] Высокий уровень риска обнаружен:', riskScore); + + // Автоматическая блокировка при высоком риске + if (req.user) { + await blockUserDevice( + req.user._id, + req.user._id, // Автоматическая блокировка + `Автоматическая блокировка: высокий уровень риска (${riskScore})`, + deviceFingerprint + ); + + console.log('[SECURITY] Устройство автоматически заблокировано из-за высокого риска'); + + return res.status(403).json({ + blocked: true, + message: 'Устройство заблокировано из-за подозрительной активности', + reason: 'Автоматическая блокировка системы безопасности', + code: 'DEVICE_BLOCKED' + }); + } + } + + // Сохраняем отчет для анализа администраторами + // Можно создать отдельную модель SuspiciousActivity или логировать + console.log('[SECURITY] Отчет о подозрительной активности сохранен'); + + res.json({ + message: 'Отчет получен и обработан', + riskScore, + action: riskScore > 80 ? 'blocked' : 'monitored' + }); + + } catch (error) { + console.error('[SECURITY] Ошибка при обработке отчета о подозрительной активности:', error); + res.status(500).json({ + message: 'Ошибка обработки отчета безопасности' + }); + } +}; + +// @desc Блокировка устройства (админ) +// @route POST /api/admin/block-device +// @access Private/Admin +const blockDevice = async (req, res) => { + try { + const { userId, deviceFingerprint, reason } = req.body; + + if (!userId || !deviceFingerprint || !reason) { + return res.status(400).json({ + message: 'ID пользователя, device fingerprint и причина обязательны' + }); + } + + // Проверяем, существует ли пользователь + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ + message: 'Пользователь не найден' + }); + } + + // Блокируем устройство + const blockedDevice = await blockUserDevice( + userId, + req.user._id, + reason, + deviceFingerprint + ); + + console.log('[ADMIN] Устройство заблокировано администратором:', { + deviceId: blockedDevice._id, + userId, + adminId: req.user._id, + reason + }); + + res.status(201).json({ + message: 'Устройство успешно заблокировано', + blockedDevice: { + id: blockedDevice._id, + deviceFingerprint: blockedDevice.deviceFingerprint, + reason: blockedDevice.reason, + blockedAt: blockedDevice.blockedAt + } + }); + + } catch (error) { + console.error('[ADMIN] Ошибка при блокировке устройства:', error); + res.status(500).json({ + message: 'Ошибка при блокировке устройства', + error: error.message + }); + } +}; + +// @desc Разблокировка устройства (админ) +// @route POST /api/admin/unblock-device +// @access Private/Admin +const unblockDeviceAdmin = async (req, res) => { + try { + const { deviceFingerprint, reason } = req.body; + + if (!deviceFingerprint) { + return res.status(400).json({ + message: 'Device fingerprint обязателен' + }); + } + + // Разблокируем устройство + const device = await unblockDevice( + deviceFingerprint, + req.user._id, + reason || 'Разблокировано администратором' + ); + + console.log('[ADMIN] Устройство разблокировано администратором:', { + deviceId: device._id, + adminId: req.user._id, + reason + }); + + res.json({ + message: 'Устройство успешно разблокировано', + device: { + id: device._id, + deviceFingerprint: device.deviceFingerprint, + unblockedAt: new Date() + } + }); + + } catch (error) { + console.error('[ADMIN] Ошибка при разблокировке устройства:', error); + res.status(500).json({ + message: 'Ошибка при разблокировке устройства', + error: error.message + }); + } +}; + +// @desc Получение списка заблокированных устройств +// @route GET /api/admin/blocked-devices +// @access Private/Admin +const getBlockedDevicesList = async (req, res) => { + try { + const { + page = 1, + limit = 20, + sortBy = 'blockedAt', + sortOrder = 'desc', + isActive = 'true', + userId + } = req.query; + + const filters = { + page: parseInt(page), + limit: parseInt(limit), + sortBy, + sortOrder, + isActive: isActive === 'true', + userId + }; + + const result = await getBlockedDevices(filters); + + console.log('[ADMIN] Запрос списка заблокированных устройств:', { + adminId: req.user._id, + filters, + totalItems: result.pagination.totalItems + }); + + res.json({ + success: true, + data: result.devices, + pagination: result.pagination + }); + + } catch (error) { + console.error('[ADMIN] Ошибка при получении списка заблокированных устройств:', error); + res.status(500).json({ + message: 'Ошибка при получении списка заблокированных устройств', + error: error.message + }); + } +}; + +// @desc Получение детальной информации о заблокированном устройстве +// @route GET /api/admin/blocked-devices/:id +// @access Private/Admin +const getBlockedDeviceDetails = async (req, res) => { + try { + const { id } = req.params; + + const device = await BlockedDevice.findById(id) + .populate('blockedBy', 'name email') + .populate('blockedUserId', 'name email') + .populate('unblockRequests.requestedBy', 'name email') + .populate('unblockRequests.reviewedBy', 'name email'); + + if (!device) { + return res.status(404).json({ + message: 'Заблокированное устройство не найдено' + }); + } + + console.log('[ADMIN] Запрос деталей заблокированного устройства:', { + deviceId: id, + adminId: req.user._id + }); + + res.json({ + success: true, + data: device + }); + + } catch (error) { + console.error('[ADMIN] Ошибка при получении деталей заблокированного устройства:', error); + res.status(500).json({ + message: 'Ошибка при получении информации об устройстве', + error: error.message + }); + } +}; + +/** + * Вычисляет уровень риска на основе подозрительной активности + */ +function calculateRiskScore(activities, deviceInfo) { + let score = 0; + + if (!activities || activities.length === 0) return 0; + + activities.forEach(activity => { + switch (activity.type) { + case 'fingerprint_change': + score += 30; // Изменение fingerprint - высокий риск + break; + case 'multiple_login_attempts': + score += 20; + break; + case 'suspicious_user_agent': + score += 15; + break; + case 'rapid_requests': + score += 10; + break; + default: + score += 5; + } + }); + + // Дополнительные факторы риска + if (activities.length > 5) score += 10; // Много событий + if (deviceInfo?.visitCount === 1) score += 15; // Новое устройство + + return Math.min(score, 100); // Максимум 100 +} + +module.exports = { + checkDeviceBlockStatus, + reportSuspiciousActivity, + blockDevice, + unblockDeviceAdmin, + getBlockedDevicesList, + getBlockedDeviceDetails +}; \ No newline at end of file diff --git a/backend/middleware/deviceBlockMiddleware.js b/backend/middleware/deviceBlockMiddleware.js new file mode 100644 index 0000000..8109c4d --- /dev/null +++ b/backend/middleware/deviceBlockMiddleware.js @@ -0,0 +1,231 @@ +const BlockedDevice = require('../models/BlockedDevice'); + +/** + * Middleware для проверки блокировки устройства + * Должен использоваться перед аутентификацией + */ +const checkDeviceBlock = async (req, res, next) => { + try { + console.log('[DEVICE_CHECK] Проверка блокировки устройства'); + + // Получаем fingerprint устройства из заголовков запроса + const deviceFingerprint = req.headers['x-device-fingerprint']; + + if (!deviceFingerprint) { + console.log('[DEVICE_CHECK] Device fingerprint не предоставлен'); + return res.status(400).json({ + message: 'Требуется идентификация устройства для обеспечения безопасности', + code: 'DEVICE_FINGERPRINT_REQUIRED' + }); + } + + console.log('[DEVICE_CHECK] Проверяем fingerprint:', deviceFingerprint.substring(0, 16) + '...'); + + // Проверяем, заблокировано ли устройство + const blockedDevice = await BlockedDevice.isDeviceBlocked(deviceFingerprint); + + if (blockedDevice) { + console.log('[DEVICE_CHECK] Устройство заблокировано:', { + deviceId: blockedDevice._id, + blockedAt: blockedDevice.blockedAt, + reason: blockedDevice.reason + }); + + // Записываем попытку обхода блокировки + await blockedDevice.addBypassAttempt({ + type: req.route?.path?.includes('/login') ? 'login' : + req.route?.path?.includes('/register') ? 'register' : 'api_request', + ipAddress: req.ip || req.connection.remoteAddress, + userAgent: req.headers['user-agent'] + }); + + // Возвращаем ошибку блокировки + return res.status(403).json({ + blocked: true, + message: 'Доступ с данного устройства заблокирован', + reason: blockedDevice.reason, + blockedAt: blockedDevice.blockedAt, + code: 'DEVICE_BLOCKED' + }); + } + + // Проверяем похожие заблокированные устройства + const similarDevices = await BlockedDevice.findSimilarDevices(deviceFingerprint, 0.85); + + if (similarDevices.length > 0) { + console.log('[DEVICE_CHECK] Найдены похожие заблокированные устройства:', similarDevices.length); + + // Повышаем уровень безопасности для похожих устройств + req.securityRisk = 'HIGH'; + req.similarBlockedDevices = similarDevices; + + // Можно добавить дополнительные проверки для подозрительных устройств + // Например, CAPTCHA или дополнительную верификацию + } + + // Сохраняем fingerprint в запросе для дальнейшего использования + req.deviceFingerprint = deviceFingerprint; + req.deviceInfo = { + userAgent: req.headers['user-agent'], + ipAddress: req.ip || req.connection.remoteAddress, + screenResolution: req.headers['x-screen-resolution'], + platform: req.headers['x-platform'], + language: req.headers['x-language'], + timezone: req.headers['x-timezone'] + }; + + console.log('[DEVICE_CHECK] Устройство не заблокировано, продолжаем'); + next(); + + } catch (error) { + console.error('[DEVICE_CHECK] Ошибка при проверке блокировки устройства:', error); + + // В случае ошибки системы безопасности, блокируем доступ + return res.status(500).json({ + message: 'Ошибка системы безопасности. Попробуйте позже.', + code: 'SECURITY_CHECK_ERROR' + }); + } +}; + +/** + * Middleware для записи информации об устройстве при успешной аутентификации + */ +const recordDeviceActivity = async (req, res, next) => { + try { + if (req.deviceFingerprint && req.user) { + console.log('[DEVICE_RECORD] Записываем активность устройства для пользователя:', req.user._id); + + // Можно сохранить информацию об устройстве пользователя для анализа + // Например, в отдельной коллекции UserDevices + + // Пока просто логируем + console.log('[DEVICE_RECORD] Device fingerprint:', req.deviceFingerprint.substring(0, 16) + '...'); + console.log('[DEVICE_RECORD] Device info:', req.deviceInfo); + } + + next(); + } catch (error) { + console.error('[DEVICE_RECORD] Ошибка при записи активности устройства:', error); + // Не блокируем запрос из-за ошибки записи + next(); + } +}; + +/** + * Функция для блокировки устройства пользователя + */ +const blockUserDevice = async (userId, adminId, reason, deviceFingerprint = null) => { + try { + console.log('[BLOCK_DEVICE] Блокировка устройства пользователя:', userId); + + if (!deviceFingerprint) { + // Если fingerprint не предоставлен, нужно найти его в активных сессиях + // Это более сложная задача, требующая отслеживания активных сессий + throw new Error('Device fingerprint не предоставлен для блокировки'); + } + + const deviceInfo = { + // Получаем дополнительную информацию об устройстве из базы данных или сессии + // Это может быть реализовано через отслеживание активных подключений + }; + + const blockedDevice = await BlockedDevice.blockDevice({ + deviceFingerprint, + blockedBy: adminId, + blockedUserId: userId, + reason, + deviceInfo + }); + + console.log('[BLOCK_DEVICE] Устройство заблокировано:', blockedDevice._id); + return blockedDevice; + + } catch (error) { + console.error('[BLOCK_DEVICE] Ошибка при блокировке устройства:', error); + throw error; + } +}; + +/** + * Функция для разблокировки устройства + */ +const unblockDevice = async (deviceFingerprint, adminId, reason) => { + try { + console.log('[UNBLOCK_DEVICE] Разблокировка устройства:', deviceFingerprint.substring(0, 16) + '...'); + + const blockedDevice = await BlockedDevice.findOne({ + deviceFingerprint, + isActive: true + }); + + if (!blockedDevice) { + throw new Error('Заблокированное устройство не найдено'); + } + + await blockedDevice.unblock(adminId, reason); + + console.log('[UNBLOCK_DEVICE] Устройство разблокировано'); + return blockedDevice; + + } catch (error) { + console.error('[UNBLOCK_DEVICE] Ошибка при разблокировке устройства:', error); + throw error; + } +}; + +/** + * Функция для получения информации о заблокированных устройствах + */ +const getBlockedDevices = async (filters = {}) => { + try { + const { + page = 1, + limit = 20, + sortBy = 'blockedAt', + sortOrder = 'desc', + isActive = true, + userId + } = filters; + + const query = { isActive }; + + if (userId) { + query.blockedUserId = userId; + } + + const skip = (page - 1) * limit; + const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 }; + + const devices = await BlockedDevice.find(query) + .populate('blockedBy', 'name email') + .populate('blockedUserId', 'name email') + .sort(sort) + .skip(skip) + .limit(limit); + + const total = await BlockedDevice.countDocuments(query); + + return { + devices, + pagination: { + currentPage: page, + totalPages: Math.ceil(total / limit), + totalItems: total, + itemsPerPage: limit + } + }; + + } catch (error) { + console.error('[GET_BLOCKED_DEVICES] Ошибка при получении заблокированных устройств:', error); + throw error; + } +}; + +module.exports = { + checkDeviceBlock, + recordDeviceActivity, + blockUserDevice, + unblockDevice, + getBlockedDevices +}; \ No newline at end of file diff --git a/backend/models/BlockedDevice.js b/backend/models/BlockedDevice.js new file mode 100644 index 0000000..8aa07fe --- /dev/null +++ b/backend/models/BlockedDevice.js @@ -0,0 +1,275 @@ +const mongoose = require('mongoose'); + +const blockedDeviceSchema = new mongoose.Schema( + { + deviceFingerprint: { + type: String, + required: true, + index: true, + unique: true + }, + blockedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + blockedUserId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + reason: { + type: String, + required: true, + maxlength: 500 + }, + blockedAt: { + type: Date, + default: Date.now + }, + isActive: { + type: Boolean, + default: true + }, + // Дополнительная информация об устройстве для анализа + deviceInfo: { + userAgent: String, + screenResolution: String, + platform: String, + language: String, + timezone: String, + ipAddress: String, // Сохраняем IP для дополнительного анализа + lastSeen: { + type: Date, + default: Date.now + } + }, + // Попытки обхода блокировки + bypassAttempts: [{ + timestamp: { + type: Date, + default: Date.now + }, + attemptType: { + type: String, + enum: ['login', 'register', 'api_request'], + required: true + }, + ipAddress: String, + userAgent: String, + blocked: { + type: Boolean, + default: true + } + }], + // Связанные fingerprints (для обнаружения похожих устройств) + relatedFingerprints: [{ + fingerprint: String, + similarity: Number, // Процент схожести + detectedAt: { + type: Date, + default: Date.now + } + }], + unblockRequests: [{ + requestedAt: { + type: Date, + default: Date.now + }, + requestedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + reason: String, + status: { + type: String, + enum: ['pending', 'approved', 'rejected'], + default: 'pending' + }, + reviewedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + reviewedAt: Date, + reviewNote: String + }] + }, + { + timestamps: true, + } +); + +// Индексы для оптимизации поиска +blockedDeviceSchema.index({ deviceFingerprint: 1, isActive: 1 }); +blockedDeviceSchema.index({ blockedUserId: 1 }); +blockedDeviceSchema.index({ blockedAt: 1 }); +blockedDeviceSchema.index({ 'deviceInfo.ipAddress': 1 }); + +// Методы модели +blockedDeviceSchema.methods.addBypassAttempt = function(attemptData) { + this.bypassAttempts.push({ + attemptType: attemptData.type, + ipAddress: attemptData.ipAddress, + userAgent: attemptData.userAgent, + blocked: true + }); + + // Ограничиваем количество записей попыток + if (this.bypassAttempts.length > 100) { + this.bypassAttempts = this.bypassAttempts.slice(-100); + } + + return this.save(); +}; + +blockedDeviceSchema.methods.addRelatedFingerprint = function(fingerprint, similarity) { + // Проверяем, не добавлен ли уже этот fingerprint + const exists = this.relatedFingerprints.find(rf => rf.fingerprint === fingerprint); + if (!exists) { + this.relatedFingerprints.push({ + fingerprint, + similarity + }); + + // Ограничиваем количество связанных fingerprints + if (this.relatedFingerprints.length > 50) { + this.relatedFingerprints = this.relatedFingerprints.slice(-50); + } + } + + return this.save(); +}; + +blockedDeviceSchema.methods.unblock = function(unblockedBy, reason) { + this.isActive = false; + this.unblockRequests.push({ + requestedBy: unblockedBy, + reason: reason || 'Разблокировано администратором', + status: 'approved', + reviewedBy: unblockedBy, + reviewedAt: new Date(), + reviewNote: 'Разблокировано напрямую' + }); + + return this.save(); +}; + +// Статические методы +blockedDeviceSchema.statics.isDeviceBlocked = async function(deviceFingerprint) { + const blockedDevice = await this.findOne({ + deviceFingerprint, + isActive: true + }).populate('blockedUserId', 'name email'); + + return blockedDevice; +}; + +blockedDeviceSchema.statics.blockDevice = async function(deviceData) { + const { + deviceFingerprint, + blockedBy, + blockedUserId, + reason, + deviceInfo + } = deviceData; + + // Проверяем, не заблокировано ли уже устройство + const existing = await this.findOne({ deviceFingerprint }); + + if (existing) { + if (!existing.isActive) { + // Реактивируем блокировку + existing.isActive = true; + existing.reason = reason; + existing.blockedBy = blockedBy; + existing.blockedAt = new Date(); + if (deviceInfo) { + existing.deviceInfo = { ...existing.deviceInfo, ...deviceInfo }; + } + return await existing.save(); + } else { + // Устройство уже заблокировано + return existing; + } + } + + // Создаем новую блокировку + const blockedDevice = new this({ + deviceFingerprint, + blockedBy, + blockedUserId, + reason, + deviceInfo + }); + + return await blockedDevice.save(); +}; + +blockedDeviceSchema.statics.findSimilarDevices = async function(deviceFingerprint, threshold = 0.8) { + // Простой алгоритм поиска похожих fingerprints + const allDevices = await this.find({ isActive: true }); + const similar = []; + + for (const device of allDevices) { + const similarity = this.calculateSimilarity(deviceFingerprint, device.deviceFingerprint); + if (similarity >= threshold) { + similar.push({ + device, + similarity + }); + } + } + + return similar.sort((a, b) => b.similarity - a.similarity); +}; + +blockedDeviceSchema.statics.calculateSimilarity = function(fp1, fp2) { + if (fp1 === fp2) return 1.0; + + // Простой алгоритм сравнения строк + const longer = fp1.length > fp2.length ? fp1 : fp2; + const shorter = fp1.length > fp2.length ? fp2 : fp1; + + if (longer.length === 0) return 1.0; + + const editDistance = this.levenshteinDistance(longer, shorter); + return (longer.length - editDistance) / longer.length; +}; + +blockedDeviceSchema.statics.levenshteinDistance = function(str1, str2) { + const matrix = []; + + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[str2.length][str1.length]; +}; + +// Middleware для обновления lastSeen +blockedDeviceSchema.pre('save', function(next) { + if (this.deviceInfo) { + this.deviceInfo.lastSeen = new Date(); + } + next(); +}); + +module.exports = mongoose.model('BlockedDevice', blockedDeviceSchema); \ No newline at end of file diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 71ead9c..ce40333 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -9,6 +9,12 @@ const { updateReportStatus, getReportsStats } = require('../controllers/reportController'); +const { + blockDevice, + unblockDeviceAdmin, + getBlockedDevicesList, + getBlockedDeviceDetails +} = require('../controllers/deviceSecurityController'); // Все маршруты защищены middleware для проверки авторизации и прав администратора router.use(protect, adminMiddleware); @@ -32,4 +38,10 @@ router.get('/conversations', adminController.getAllConversations); router.get('/conversations/:id', adminController.getConversationById); // Новый маршрут для получения диалога по ID router.get('/conversations/:id/messages', adminController.getConversationMessages); +// Маршруты для управления блокировками устройств +router.get('/blocked-devices', getBlockedDevicesList); +router.get('/blocked-devices/:id', getBlockedDeviceDetails); +router.post('/block-device', blockDevice); +router.post('/unblock-device', unblockDeviceAdmin); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 93f79a1..db56485 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -11,6 +11,7 @@ const { // Импортируем middleware для защиты маршрутов const { protect } = require('../middleware/authMiddleware'); +const { checkDeviceBlock, recordDeviceActivity } = require('../middleware/deviceBlockMiddleware'); // --- НАЧАЛО ИЗМЕНЕНИЯ --- // Дополнительная отладка: Проверяем, что protect действительно функция @@ -27,15 +28,16 @@ console.log('[DEBUG] authRoutes.js - typeof registerUser:', typeof registerUser) console.log('[DEBUG] authRoutes.js - typeof loginUser:', typeof loginUser); console.log('[DEBUG] authRoutes.js - typeof getMe:', typeof getMe); console.log('[DEBUG] authRoutes.js - typeof protect:', typeof protect); +console.log('[DEBUG] authRoutes.js - typeof checkDeviceBlock:', typeof checkDeviceBlock); -// Регистрация пользователя - публичный доступ -router.post('/register', registerUser); +// Регистрация пользователя - с проверкой блокировки устройства +router.post('/register', checkDeviceBlock, registerUser); -// Вход пользователя - публичный доступ -router.post('/login', loginUser); +// Вход пользователя - с проверкой блокировки устройства и записью активности +router.post('/login', checkDeviceBlock, loginUser, recordDeviceActivity); -// Получение профиля - защищенный доступ -router.get('/me', protect, getMe); +// Получение профиля - защищенный доступ с записью активности устройства +router.get('/me', protect, recordDeviceActivity, getMe); // Экспортируем маршруты module.exports = router; diff --git a/backend/routes/securityRoutes.js b/backend/routes/securityRoutes.js new file mode 100644 index 0000000..14a0cde --- /dev/null +++ b/backend/routes/securityRoutes.js @@ -0,0 +1,19 @@ +const express = require('express'); +const router = express.Router(); +const { + checkDeviceBlockStatus, + reportSuspiciousActivity +} = require('../controllers/deviceSecurityController'); +const { checkDeviceBlock } = require('../middleware/deviceBlockMiddleware'); + +// @route POST /api/security/check-device +// @desc Проверка блокировки устройства +// @access Public +router.post('/check-device', checkDeviceBlockStatus); + +// @route POST /api/security/report-suspicious +// @desc Отчет о подозрительной активности +// @access Private (требует device fingerprint) +router.post('/report-suspicious', checkDeviceBlock, reportSuspiciousActivity); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 945b02f..7f18f8b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,6 +15,7 @@ const userRoutes = require('./routes/userRoutes'); // 1. Импортируем const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes const adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа const reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб +const securityRoutes = require('./routes/securityRoutes'); // Импортируем маршруты безопасности const Message = require('./models/Message'); // Импорт модели Message const Conversation = require('./models/Conversation'); // Импорт модели Conversation const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта @@ -87,6 +88,7 @@ app.use('/api/actions', actionRoutes); app.use('/api/conversations', conversationRoutes); app.use('/api/reports', reportRoutes); // Подключаем маршруты для жалоб app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели +app.use('/api/security', securityRoutes); // Подключаем маршруты для системы безопасности устройств // Socket.IO логика let activeUsers = []; diff --git a/src/auth.js b/src/auth.js index 3c853ae..0a567db 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,6 +1,7 @@ import { ref, computed } from 'vue'; import api from './services/api'; // Наш API сервис import router from './router'; // Vue Router для перенаправлений +import deviceSecurityService from './services/deviceSecurity'; // Система безопасности устройств // Реактивные переменные для состояния пользователя и токена const user = ref(null); // Данные пользователя (или null, если не вошел) @@ -58,6 +59,11 @@ async function login(credentials) { try { console.log('Попытка входа с учетными данными:', { email: credentials.email }); + // Проверяем, не заблокировано ли устройство перед попыткой входа + if (deviceSecurityService.isDeviceBlocked()) { + throw new Error('Доступ с данного устройства заблокирован'); + } + const response = await api.login(credentials); console.log('Ответ сервера при входе:', response); @@ -79,6 +85,11 @@ async function login(credentials) { // Сохраняем данные пользователя и токен setUserData(userData, userToken); + // Записываем успешную попытку входа в систему безопасности + if (deviceSecurityService.isInitialized()) { + deviceSecurityService.recordLoginAttempt(true); + } + // После сохранения данных, но до перенаправления console.log('isAuthenticated ПОСЛЕ входа:', isAuthenticated.value); console.log('Данные пользователя ПОСЛЕ входа:', user.value); @@ -93,6 +104,25 @@ async function login(credentials) { return true; } catch (error) { console.error('Ошибка входа:', error.response ? error.response.data : error.message); + + // Записываем неудачную попытку входа в систему безопасности + if (deviceSecurityService.isInitialized()) { + deviceSecurityService.recordLoginAttempt(false); + + // Если слишком много неудачных попыток, записываем подозрительную активность + if (error.response?.status === 429) { + deviceSecurityService.recordSuspiciousActivity({ + type: 'rate_limit_exceeded', + endpoint: '/auth/login', + details: { + email: credentials.email, + userAgent: navigator.userAgent + }, + timestamp: Date.now() + }); + } + } + // Возвращаем сообщение об ошибке, чтобы отобразить в компоненте throw error.response ? error.response.data : new Error('Ошибка сети или сервера при входе'); } @@ -120,6 +150,12 @@ async function logout() { user.value = null; token.value = null; localStorage.removeItem('userToken'); + + // Очищаем данные в системе безопасности устройств + if (deviceSecurityService.isInitialized()) { + deviceSecurityService.clearUserSession(); + } + // Удаляем токен из заголовков Axios (если устанавливали его напрямую) // delete apiClient.defaults.headers.common['Authorization']; // apiClient не экспортируется из api.js напрямую для этого // Наш интерсептор запросов в api.js перестанет добавлять токен, так как его не будет в localStorage. @@ -211,6 +247,53 @@ function handleAccountUnblocked(data) { console.log('Аккаунт пользователя разблокирован.'); } +// Функция обработки события блокировки устройства +function handleDeviceBlocked(data) { + console.log('[Auth] Обработка блокировки устройства:', data); + + try { + // Создаем объект с информацией о блокировке устройства + const deviceBlockInfo = { + blocked: true, + message: data?.message || 'Доступ с данного устройства заблокирован.', + reason: data?.reason || 'Подозрительная активность', + timestamp: data?.blockedAt || new Date().toISOString(), + deviceId: data?.deviceId || 'unknown' + }; + + // Сохраняем информацию о блокировке устройства + localStorage.setItem('deviceBlockedInfo', JSON.stringify(deviceBlockInfo)); + + // Обновляем состояние в сервисе безопасности устройств + if (deviceSecurityService.isInitialized()) { + deviceSecurityService.handleDeviceBlocked(data); + } + + // Немедленно очищаем все данные пользователя + console.log('[Auth] Очистка данных пользователя из-за блокировки устройства'); + user.value = null; + token.value = null; + localStorage.removeItem('userToken'); + + // Устанавливаем флаг в SessionStorage + sessionStorage.setItem('forcedLogout', 'device_blocked'); + + // Перенаправляем на страницу входа с информацией о блокировке устройства + console.log('[Auth] Перенаправление на страницу входа с параметром device blocked'); + + window.location.href = '/login?blocked=true&type=device&reason=' + + encodeURIComponent(deviceBlockInfo.reason) + + '&message=' + encodeURIComponent(deviceBlockInfo.message); + + console.log('[Auth] Пользователь был разлогинен из-за блокировки устройства.'); + return true; + } catch (error) { + console.error('[Auth] Ошибка при обработке блокировки устройства:', error); + window.location.href = '/login?blocked=true&type=device&emergency=true'; + return false; + } +} + // Экспортируем то, что понадобится в компонентах export function useAuth() { return { @@ -222,6 +305,7 @@ export function useAuth() { logout, fetchUser, // Эту функцию нужно будет вызвать при инициализации приложения handleAccountBlocked, // Добавляем функцию обработки блокировки аккаунта - handleAccountUnblocked // Добавляем функцию обработки разблокировки аккаунта + handleAccountUnblocked, // Добавляем функцию обработки разблокировки аккаунта + handleDeviceBlocked // Добавляем функцию обработки блокировки устройства }; } \ No newline at end of file diff --git a/src/components/BlockedDevices.vue b/src/components/BlockedDevices.vue new file mode 100644 index 0000000..22dbc4f --- /dev/null +++ b/src/components/BlockedDevices.vue @@ -0,0 +1,1033 @@ + + + + + \ No newline at end of file diff --git a/src/components/admin/BlockedDevicesManagement.vue b/src/components/admin/BlockedDevicesManagement.vue new file mode 100644 index 0000000..331c65d --- /dev/null +++ b/src/components/admin/BlockedDevicesManagement.vue @@ -0,0 +1,1282 @@ + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index 9fd1aa7..3011b50 100644 --- a/src/main.js +++ b/src/main.js @@ -4,6 +4,7 @@ import router from './router'; import { useAuth } from './auth'; import Vue3TouchEvents from 'vue3-touch-events'; import 'bootstrap/dist/css/bootstrap.min.css'; +import deviceSecurityService from './services/deviceSecurity'; // Создаем приложение const app = createApp(App); @@ -15,11 +16,27 @@ app.use(Vue3TouchEvents); app.use(router); // Получаем экземпляр нашего auth "стора" -const { fetchUser } = useAuth(); +const { fetchUser, handleDeviceBlocked } = useAuth(); // Асинхронная самовызывающаяся функция для инициализации (async () => { try { + // Инициализируем систему безопасности устройств + console.log('Инициализация системы безопасности устройств...'); + await deviceSecurityService.initialize(); + + // Подписываемся на события блокировки устройства + deviceSecurityService.onDeviceBlocked(handleDeviceBlocked); + + console.log('Система безопасности устройств инициализирована'); + + // Проверяем, не заблокировано ли устройство + if (deviceSecurityService.isDeviceBlocked()) { + console.warn('Устройство заблокировано, перенаправляем на страницу входа'); + window.location.href = '/login?blocked=true&type=device'; + return; + } + await fetchUser(); // Пытаемся загрузить пользователя по токену из localStorage } catch (error) { console.error("Ошибка при начальной загрузке пользователя в main.js:", error); diff --git a/src/router/index.js b/src/router/index.js index aa98b8d..93ef2f1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -57,6 +57,12 @@ const routes = [ component: () => import('../views/PreferencesView.vue'), meta: { requiresAuth: true } }, + // Страница блокировки устройства + { + path: '/device-blocked', + name: 'DeviceBlocked', + component: () => import('../views/DeviceBlockedView.vue'), + }, // Маршруты для админ-панели { path: '/admin', @@ -109,6 +115,12 @@ const routes = [ component: () => import('../views/admin/AdminReportDetail.vue'), meta: { requiresAuth: true, requiresAdmin: true }, props: true + }, + { + path: 'blocked-devices', + name: 'AdminBlockedDevices', + component: () => import('../views/admin/AdminBlockedDevices.vue'), + meta: { requiresAuth: true, requiresAdmin: true } } ] } @@ -141,7 +153,7 @@ router.beforeEach(async (to, from, next) => { console.log('[ROUTER GUARD] Пользователь является администратором:', user.value?.isAdmin ? 'Да' : 'Нет'); // Переадресация администратора сразу после загрузки данных - if (user.value?.isAdmin && !to.path.startsWith('/admin')) { + if (user.value?.isAdmin && !to.path.startsает('/admin')) { console.log('[ROUTER GUARD] Пользователь - администратор, перенаправляем на панель администратора'); return next({ name: 'AdminDashboard' }); } diff --git a/src/services/adminDeviceSecurityService.js b/src/services/adminDeviceSecurityService.js new file mode 100644 index 0000000..60ccbe6 --- /dev/null +++ b/src/services/adminDeviceSecurityService.js @@ -0,0 +1,235 @@ +import apiClient from './apiClient'; + +class AdminDeviceSecurityService { + // Получение списка заблокированных устройств + async getBlockedDevices(params = {}) { + try { + const response = await apiClient.get('/admin/blocked-devices', { params }); + return response; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения заблокированных устройств:', error); + throw error; + } + } + + // Получение статистики по заблокированным устройствам + async getDeviceSecurityStats() { + try { + const response = await apiClient.get('/admin/device-security/stats'); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения статистики:', error); + throw error; + } + } + + // Блокировка устройства + async blockDevice(deviceData) { + try { + const response = await apiClient.post('/admin/block-device', { + userId: deviceData.userId, + deviceFingerprint: deviceData.deviceFingerprint, + reason: deviceData.reason, + additional: deviceData.additional || {} + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка блокировки устройства:', error); + throw error; + } + } + + // Разблокировка устройства + async unblockDevice(deviceFingerprint, reason) { + try { + const response = await apiClient.post('/admin/unblock-device', { + deviceFingerprint, + reason + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка разблокировки устройства:', error); + throw error; + } + } + + // Получение детальной информации об устройстве + async getDeviceDetails(deviceFingerprint) { + try { + const response = await apiClient.get(`/admin/device/${deviceFingerprint}`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения деталей устройства:', error); + throw error; + } + } + + // Получение истории блокировок устройства + async getDeviceBlockHistory(deviceFingerprint) { + try { + const response = await apiClient.get(`/admin/device/${deviceFingerprint}/history`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения истории блокировок:', error); + throw error; + } + } + + // Получение попыток обхода блокировки + async getBypassAttempts(deviceFingerprint) { + try { + const response = await apiClient.get(`/admin/device/${deviceFingerprint}/bypass-attempts`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения попыток обхода:', error); + throw error; + } + } + + // Массовая блокировка устройств + async blockMultipleDevices(devices, reason) { + try { + const response = await apiClient.post('/admin/block-devices-bulk', { + devices, + reason + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка массовой блокировки:', error); + throw error; + } + } + + // Экспорт списка заблокированных устройств + async exportBlockedDevices(format = 'csv', filters = {}) { + try { + const response = await apiClient.get('/admin/blocked-devices/export', { + params: { format, ...filters }, + responseType: 'blob' + }); + + // Создаем ссылку для скачивания + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `blocked-devices-${new Date().toISOString().split('T')[0]}.${format}`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + return true; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка экспорта:', error); + throw error; + } + } + + // Получение отчета по безопасности устройств + async getSecurityReport(period = '30d') { + try { + const response = await apiClient.get('/admin/device-security/report', { + params: { period } + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения отчета:', error); + throw error; + } + } + + // Обновление настроек безопасности устройств + async updateSecuritySettings(settings) { + try { + const response = await apiClient.put('/admin/device-security/settings', settings); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка обновления настроек:', error); + throw error; + } + } + + // Получение настроек безопасности устройств + async getSecuritySettings() { + try { + const response = await apiClient.get('/admin/device-security/settings'); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения настроек:', error); + throw error; + } + } + + // Поиск устройств по критериям + async searchDevices(query) { + try { + const response = await apiClient.get('/admin/devices/search', { + params: { q: query } + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка поиска устройств:', error); + throw error; + } + } + + // Получение активных сессий пользователя + async getUserActiveSessions(userId) { + try { + const response = await apiClient.get(`/admin/user/${userId}/sessions`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения активных сессий:', error); + throw error; + } + } + + // Завершение всех сессий пользователя + async terminateUserSessions(userId) { + try { + const response = await apiClient.post(`/admin/user/${userId}/terminate-sessions`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка завершения сессий:', error); + throw error; + } + } + + // Добавление устройства в белый список + async whitelistDevice(deviceFingerprint, reason) { + try { + const response = await apiClient.post('/admin/device/whitelist', { + deviceFingerprint, + reason + }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка добавления в белый список:', error); + throw error; + } + } + + // Удаление устройства из белого списка + async removeFromWhitelist(deviceFingerprint) { + try { + const response = await apiClient.delete(`/admin/device/whitelist/${deviceFingerprint}`); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка удаления из белого списка:', error); + throw error; + } + } + + // Получение журнала безопасности + async getSecurityLog(filters = {}) { + try { + const response = await apiClient.get('/admin/security-log', { params: filters }); + return response.data; + } catch (error) { + console.error('[AdminDeviceSecurityService] Ошибка получения журнала безопасности:', error); + throw error; + } + } +} + +export default new AdminDeviceSecurityService(); \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index 3eebc99..6b90848 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,6 +1,7 @@ import axios from 'axios'; import { useAuth } from '../auth'; // Импортируем функцию авторизации import { isAccountBlocked } from './socketService'; // Импортируем проверку блокировки из socketService +import deviceSecurityService from './deviceSecurityService'; // Импортируем сервис безопасности устройств // Создаем экземпляр Axios с базовым URL нашего API // Настраиваем базовый URL в зависимости от окружения @@ -26,6 +27,12 @@ const checkAccountBlockStatus = () => { return true; } + // Проверяем блокировку устройства + if (deviceSecurityService && deviceSecurityService.isDeviceBlocked()) { + console.warn('[API] Устройство заблокировано, запросы невозможны'); + return true; + } + return false; } catch (e) { console.error('[API] Ошибка при проверке статуса блокировки:', e); @@ -37,81 +44,88 @@ const checkAccountBlockStatus = () => { const getBaseUrl = () => { // Всегда используем относительный путь /api. // В режиме разработки Vite dev server будет проксировать эти запросы согласно настройке server.proxy в vite.config.js. - // В продакшене предполагается, что веб-сервер (например, Nginx) настроен аналогично для обработки /api. - // return '/api'; - return import.meta.env.VITE_API_BASE_URL; // Используем переменную окружения Vite + return '/api'; }; +// Создаем экземпляр Axios const apiClient = axios.create({ baseURL: getBaseUrl(), + timeout: 30000, headers: { 'Content-Type': 'application/json', }, }); -// (Опционально, но очень полезно) Перехватчик для добавления JWT токена к запросам +// Интерсептор запросов для добавления токена авторизации и device fingerprint apiClient.interceptors.request.use( - (config) => { - // Проверяем, заблокирован ли аккаунт, ПЕРЕД выполнением запроса + async (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 error = new Error('Запрос отменен: аккаунт или устройство заблокированы'); + error.code = 'ACCOUNT_OR_DEVICE_BLOCKED'; + throw error; } - - const token = localStorage.getItem('userToken'); // Предполагаем, что токен хранится в localStorage + + // Добавляем токен авторизации + const token = localStorage.getItem('userToken'); if (token) { - console.log('[API] Добавление токена к запросу:', config.url); - config.headers['Authorization'] = `Bearer ${token}`; - } else { - console.log('[API] Токен не найден для запроса:', config.url); + config.headers.Authorization = `Bearer ${token}`; } - - // Добавляем CancelToken к запросу для возможности отмены + + // Добавляем device fingerprint и информацию об устройстве + if (deviceSecurityService && deviceSecurityService.getCurrentFingerprint()) { + const fingerprintHeaders = deviceSecurityService.injectFingerprintHeaders(); + Object.assign(config.headers, fingerprintHeaders); + } + + // Создаем токен отмены для этого запроса + const requestKey = `${config.method}-${config.url}`; const source = axios.CancelToken.source(); config.cancelToken = source.token; - - // Сохраняем источник токена отмены в Map по URL запроса как ключу - const requestKey = `${config.method}-${config.url}`; cancelTokenSources.set(requestKey, source); - + + console.log(`[API] Отправка ${config.method?.toUpperCase()} запроса:`, config.url); return config; }, (error) => { - console.error('[API] Ошибка в перехватчике запросов:', error); + console.error('[API] Ошибка в интерсепторе запросов:', error); return Promise.reject(error); } ); -// (Опционально) Перехватчик ответов для обработки глобальных ошибок, например, 401 +// Интерсептор ответов для обработки ошибок apiClient.interceptors.response.use( (response) => { - // Удаляем источник токена отмены после успешного ответа + // Удаляем источник токена отмены при успешном ответе const requestKey = `${response.config.method}-${response.config.url}`; cancelTokenSources.delete(requestKey); + + console.log(`[API] Успешный ответ от ${response.config.url}:`, response.status); return response; }, - (error) => { + async (error) => { // Удаляем источник токена отмены при ошибке if (error.config) { const requestKey = `${error.config.method}-${error.config.url}`; cancelTokenSources.delete(requestKey); } - // Проверяем на блокировку аккаунта + // Проверяем на блокировку устройства if (error.response && error.response.status === 403) { const errorData = error.response.data; + + // Проверка на блокировку устройства + if (errorData && errorData.code === 'DEVICE_BLOCKED') { + console.error('[API] Получено уведомление о блокировке устройства:', errorData); + + // Обрабатываем блокировку устройства через deviceSecurityService + if (deviceSecurityService && typeof deviceSecurityService.handleDeviceBlock === 'function') { + deviceSecurityService.handleDeviceBlock(errorData); + } + + return Promise.reject(error); + } + // Проверка на признаки блокировки аккаунта в ответе if (errorData && ( errorData.blocked === true || @@ -125,30 +139,60 @@ apiClient.interceptors.response.use( const { handleAccountBlocked } = useAuth(); if (typeof handleAccountBlocked === 'function') { handleAccountBlocked(errorData); + } else { + console.error('[API] Функция handleAccountBlocked недоступна'); } - }, 0); + }, 100); } } - + + // Проверяем на ошибку требования device fingerprint + if (error.response && error.response.status === 400) { + const errorData = error.response.data; + if (errorData && errorData.code === 'DEVICE_FINGERPRINT_REQUIRED') { + console.warn('[API] Требуется device fingerprint для запроса'); + + // Пытаемся инициализировать систему безопасности устройств + if (deviceSecurityService && typeof deviceSecurityService.initialize === 'function') { + try { + await deviceSecurityService.initialize(); + console.log('[API] Device fingerprint инициализирован, повторяем запрос'); + + // Повторяем запрос с новым fingerprint + const originalRequest = error.config; + const fingerprintHeaders = deviceSecurityService.injectFingerprintHeaders(); + Object.assign(originalRequest.headers, fingerprintHeaders); + + return apiClient(originalRequest); + } catch (initError) { + console.error('[API] Не удалось инициализировать device fingerprint:', initError); + } + } + } + } + + // Обработка ошибки токена авторизации if (error.response && error.response.status === 401) { - console.error('[API] Неавторизованный запрос (401):', error.config.url); + console.warn('[API] Ошибка авторизации, возможно токен истек'); - // Обходим замыкание, чтобы избежать проблем с импортом - // При использовании такого подхода, logout будет вызван после текущего цикла выполнения JS + // Вызываем logout для очистки состояния setTimeout(() => { const { logout } = useAuth(); - logout(); - }, 0); + if (typeof logout === 'function') { + logout(); + } + }, 100); } + + console.error(`[API] Ошибка запроса к ${error.config?.url}:`, error.response?.status, error.response?.data); return Promise.reject(error); } ); -// Функция для отмены всех активных запросов +// Функция для отмены всех активных запросов (при блокировке) const cancelActiveRequests = () => { - console.log('[API] Отмена всех активных запросов. Количество: ' + cancelTokenSources.size); cancelTokenSources.forEach((source, key) => { - source.cancel('Запрос отменен из-за блокировки аккаунта'); + source.cancel(`Запрос ${key} отменен из-за блокировки`); console.log(`[API] Запрос ${key} отменен`); }); cancelTokenSources.clear(); @@ -243,16 +287,36 @@ export default { 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); + reviewReport(reportId, reviewData) { + return apiClient.put(`/admin/reports/${reportId}/review`, reviewData); + }, + blockUser(userId, blockData) { + return apiClient.post(`/admin/users/${userId}/block`, blockData); + }, + unblockUser(userId, reason) { + return apiClient.post(`/admin/users/${userId}/unblock`, { reason }); }, - getReportsStats() { - return apiClient.get('/admin/reports/stats'); + // Новые методы для управления блокировками устройств + checkDeviceBlock(deviceData) { + return apiClient.post('/security/check-device', deviceData); + }, + reportSuspiciousActivity(activityData) { + return apiClient.post('/security/report-suspicious', activityData); + }, + getBlockedDevices(params = {}) { + return apiClient.get('/admin/blocked-devices', { params }); + }, + getBlockedDeviceDetails(deviceId) { + return apiClient.get(`/admin/blocked-devices/${deviceId}`); + }, + blockDevice(deviceData) { + return apiClient.post('/admin/block-device', deviceData); + }, + unblockDevice(deviceFingerprint, reason) { + return apiClient.post('/admin/unblock-device', { deviceFingerprint, reason }); } }; \ No newline at end of file diff --git a/src/services/deviceSecurity.js b/src/services/deviceSecurity.js new file mode 100644 index 0000000..b5e82f7 --- /dev/null +++ b/src/services/deviceSecurity.js @@ -0,0 +1,429 @@ +import DeviceFingerprint from '../utils/deviceFingerprint.js'; +import { api } from './api.js'; + +/** + * Сервис для работы с системой блокировки устройств + */ +class DeviceSecurityService { + constructor() { + this.deviceFingerprint = new DeviceFingerprint(); + this.currentFingerprint = null; + this.isBlocked = false; + this.initialized = false; + this.suspiciousActivities = []; + this.lastFingerprintCheck = null; + this.checkInterval = null; + + // Настройки мониторинга + this.config = { + checkIntervalMs: 30000, // Проверка каждые 30 секунд + maxSuspiciousActivities: 10, + reportThreshold: 5, // Отправлять отчет после 5 подозрительных активностей + fingerprintChangeThreshold: 0.1 // Порог изменения отпечатка (10%) + }; + } + + /** + * Инициализация сервиса безопасности + */ + async initialize() { + try { + console.log('[DeviceSecurity] Инициализация сервиса безопасности устройств...'); + + // Генерируем отпечаток устройства + this.currentFingerprint = await this.deviceFingerprint.initialize(); + + // Проверяем статус блокировки + await this.checkDeviceBlockStatus(); + + // Запускаем периодический мониторинг + this.startMonitoring(); + + this.initialized = true; + console.log('[DeviceSecurity] Сервис безопасности инициализирован'); + + return { + fingerprint: this.currentFingerprint, + isBlocked: this.isBlocked, + initialized: true + }; + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при инициализации:', error); + throw error; + } + } + + /** + * Проверка статуса блокировки устройства + */ + async checkDeviceBlockStatus() { + try { + const response = await api.post('/security/check-device', { + fingerprint: this.currentFingerprint, + deviceInfo: this.getDeviceInfo() + }); + + this.isBlocked = response.data.blocked || false; + this.lastFingerprintCheck = Date.now(); + + if (this.isBlocked) { + console.warn('[DeviceSecurity] Устройство заблокировано:', response.data.reason); + this.handleDeviceBlocked(response.data); + return false; + } + + console.log('[DeviceSecurity] Устройство не заблокировано'); + return true; + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при проверке блокировки:', error); + + // При ошибке API считаем устройство потенциально заблокированным + if (error.response?.status === 403) { + this.isBlocked = true; + this.handleDeviceBlocked(error.response.data); + return false; + } + + return true; // При других ошибках разрешаем доступ + } + } + + /** + * Получение информации об устройстве + */ + getDeviceInfo() { + return { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + screenResolution: `${screen.width}x${screen.height}`, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timestamp: Date.now(), + visitCount: this.getVisitCount() + }; + } + + /** + * Получение количества посещений (из localStorage) + */ + getVisitCount() { + try { + const visits = localStorage.getItem('device_visits'); + const count = visits ? parseInt(visits) + 1 : 1; + localStorage.setItem('device_visits', count.toString()); + return count; + } catch (e) { + return 1; + } + } + + /** + * Запуск периодического мониторинга + */ + startMonitoring() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + } + + this.checkInterval = setInterval(async () => { + try { + await this.performSecurityCheck(); + } catch (error) { + console.error('[DeviceSecurity] Ошибка при периодической проверке:', error); + } + }, this.config.checkIntervalMs); + + console.log('[DeviceSecurity] Мониторинг запущен'); + } + + /** + * Остановка мониторинга + */ + stopMonitoring() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + console.log('[DeviceSecurity] Мониторинг остановлен'); + } + } + + /** + * Выполнение периодической проверки безопасности + */ + async performSecurityCheck() { + try { + // Проверяем, не изменился ли отпечаток устройства + await this.checkFingerprintChanges(); + + // Проверяем статус блокировки + await this.checkDeviceBlockStatus(); + + // Отправляем отчет о подозрительных активностях, если накопилось достаточно + if (this.suspiciousActivities.length >= this.config.reportThreshold) { + await this.reportSuspiciousActivity(); + } + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при периодической проверке:', error); + } + } + + /** + * Проверка изменений отпечатка устройства + */ + async checkFingerprintChanges() { + try { + const newFingerprint = await this.deviceFingerprint.refresh(); + + if (newFingerprint !== this.currentFingerprint) { + console.warn('[DeviceSecurity] Обнаружено изменение отпечатка устройства'); + + this.recordSuspiciousActivity({ + type: 'fingerprint_change', + oldFingerprint: this.currentFingerprint.substring(0, 16) + '...', + newFingerprint: newFingerprint.substring(0, 16) + '...', + timestamp: Date.now() + }); + + this.currentFingerprint = newFingerprint; + } + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при проверке изменений отпечатка:', error); + } + } + + /** + * Запись подозрительной активности + */ + recordSuspiciousActivity(activity) { + this.suspiciousActivities.push({ + ...activity, + id: Date.now() + Math.random(), + recorded: new Date().toISOString() + }); + + // Ограничиваем размер массива + if (this.suspiciousActivities.length > this.config.maxSuspiciousActivities) { + this.suspiciousActivities = this.suspiciousActivities.slice(-this.config.maxSuspiciousActivities); + } + + console.warn('[DeviceSecurity] Зафиксирована подозрительная активность:', activity.type); + } + + /** + * Отправка отчета о подозрительных активностях + */ + async reportSuspiciousActivity() { + try { + if (this.suspiciousActivities.length === 0) return; + + console.log('[DeviceSecurity] Отправка отчета о подозрительных активностях...'); + + const response = await api.post('/security/report-suspicious', { + activities: this.suspiciousActivities, + deviceInfo: this.getDeviceInfo(), + fingerprint: this.currentFingerprint + }); + + // Если устройство было заблокировано автоматически + if (response.data.blocked) { + this.isBlocked = true; + this.handleDeviceBlocked(response.data); + } + + // Очищаем отправленные активности + this.suspiciousActivities = []; + + console.log('[DeviceSecurity] Отчет отправлен, статус:', response.data.action); + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при отправке отчета:', error); + + if (error.response?.status === 403) { + this.isBlocked = true; + this.handleDeviceBlocked(error.response.data); + } + } + } + + /** + * Обработка блокировки устройства + */ + handleDeviceBlocked(blockInfo) { + console.error('[DeviceSecurity] Устройство заблокировано:', blockInfo); + + // Останавливаем мониторинг + this.stopMonitoring(); + + // Очищаем локальные данные + this.clearLocalData(); + + // Показываем уведомление пользователю + this.showBlockedNotification(blockInfo); + + // Перенаправляем на страницу блокировки или выходим + this.redirectToBlockedPage(blockInfo); + } + + /** + * Очистка локальных данных при блокировке + */ + clearLocalData() { + try { + // Очищаем localStorage (кроме critical данных) + const keysToKeep = ['device_visits']; + const allKeys = Object.keys(localStorage); + + allKeys.forEach(key => { + if (!keysToKeep.includes(key)) { + localStorage.removeItem(key); + } + }); + + // Очищаем sessionStorage + sessionStorage.clear(); + + console.log('[DeviceSecurity] Локальные данные очищены'); + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при очистке данных:', error); + } + } + + /** + * Показ уведомления о блокировке + */ + showBlockedNotification(blockInfo) { + const message = blockInfo.message || 'Доступ с данного устройства заблокирован'; + const reason = blockInfo.reason || 'Нарушение правил безопасности'; + + // Используем встроенное API уведомлений браузера, если доступно + if ('Notification' in window && Notification.permission === 'granted') { + new Notification('Устройство заблокировано', { + body: message, + icon: '/pwa-192x192.png' + }); + } + + // Также показываем alert для гарантии + alert(`Устройство заблокировано\n\nПричина: ${reason}\n\n${message}`); + } + + /** + * Перенаправление на страницу блокировки + */ + redirectToBlockedPage(blockInfo) { + // Создаем URL с информацией о блокировке + const params = new URLSearchParams({ + reason: blockInfo.reason || 'security_violation', + code: blockInfo.code || 'DEVICE_BLOCKED', + blocked_at: blockInfo.blockedAt || new Date().toISOString() + }); + + // Перенаправляем на страницу блокировки + window.location.href = `/blocked?${params.toString()}`; + } + + /** + * Мониторинг множественных попыток входа + */ + recordLoginAttempt(success) { + const timestamp = Date.now(); + const attempts = this.getRecentAttempts(); + + attempts.push({ timestamp, success }); + + // Сохраняем последние 10 попыток + const recentAttempts = attempts.filter(a => timestamp - a.timestamp < 300000); // 5 минут + localStorage.setItem('login_attempts', JSON.stringify(recentAttempts)); + + // Проверяем на подозрительную активность + const failedAttempts = recentAttempts.filter(a => !a.success); + if (failedAttempts.length >= 3) { + this.recordSuspiciousActivity({ + type: 'multiple_login_attempts', + count: failedAttempts.length, + timespan: '5 minutes', + timestamp + }); + } + } + + /** + * Получение недавних попыток входа + */ + getRecentAttempts() { + try { + const attempts = localStorage.getItem('login_attempts'); + return attempts ? JSON.parse(attempts) : []; + } catch (e) { + return []; + } + } + + /** + * Проверка инициализации + */ + isInitialized() { + return this.initialized; + } + + /** + * Получение текущего отпечатка + */ + getCurrentFingerprint() { + return this.currentFingerprint; + } + + /** + * Проверка блокировки + */ + isDeviceBlocked() { + return this.isBlocked; + } + + /** + * Получение компонентов отпечатка (для отладки) + */ + getFingerprintComponents() { + return this.deviceFingerprint.getComponents(); + } + + /** + * Получение статистики подозрительных активностей + */ + getSuspiciousActivitiesStats() { + return { + total: this.suspiciousActivities.length, + activities: this.suspiciousActivities.slice(), + lastCheck: this.lastFingerprintCheck + }; + } + + /** + * Очистка истории (для отладки) + */ + clearHistory() { + this.suspiciousActivities = []; + localStorage.removeItem('login_attempts'); + console.log('[DeviceSecurity] История очищена'); + } + + /** + * Деструктор + */ + destroy() { + this.stopMonitoring(); + this.suspiciousActivities = []; + this.initialized = false; + console.log('[DeviceSecurity] Сервис уничтожен'); + } +} + +// Создаем единственный экземпляр сервиса +const deviceSecurityService = new DeviceSecurityService(); + +export default deviceSecurityService; \ No newline at end of file diff --git a/src/services/deviceSecurityService.js b/src/services/deviceSecurityService.js new file mode 100644 index 0000000..5da6f08 --- /dev/null +++ b/src/services/deviceSecurityService.js @@ -0,0 +1,338 @@ +import deviceFingerprint from '@/utils/deviceFingerprint'; + +/** + * Сервис для работы с блокировкой устройств на клиентской стороне + */ +class DeviceSecurityService { + constructor() { + this.currentFingerprint = null; + this.isBlocked = false; + this.lastCheck = null; + this.checkInterval = 5 * 60 * 1000; // 5 минут + this.blockCheckCallbacks = []; + } + + /** + * Инициализация сервиса безопасности устройств + */ + async initialize() { + try { + console.log('[DeviceSecurity] Инициализация системы безопасности устройств'); + + // Генерируем fingerprint устройства + this.currentFingerprint = await deviceFingerprint.getDeviceFingerprint(); + deviceFingerprint.saveFingerprint(this.currentFingerprint); + + console.log('[DeviceSecurity] Device fingerprint создан:', this.currentFingerprint.substring(0, 16) + '...'); + + // Проверяем, изменился ли fingerprint с последнего визита + const hasChanged = await deviceFingerprint.hasChanged(); + if (hasChanged) { + console.log('[DeviceSecurity] Device fingerprint изменился с последнего визита'); + this.handleFingerprintChange(); + } + + // Устанавливаем периодическую проверку + this.startPeriodicCheck(); + + return true; + } catch (error) { + console.error('[DeviceSecurity] Ошибка инициализации:', error); + return false; + } + } + + /** + * Получает текущий fingerprint устройства + */ + getCurrentFingerprint() { + return this.currentFingerprint; + } + + /** + * Проверяет, заблокировано ли текущее устройство + */ + async checkDeviceBlock() { + try { + if (!this.currentFingerprint) { + await this.initialize(); + } + + // Отправляем запрос на сервер для проверки блокировки + const response = await fetch('/api/security/check-device', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Device-Fingerprint': this.currentFingerprint + }, + body: JSON.stringify({ + fingerprint: this.currentFingerprint, + deviceInfo: await this.getDeviceInfo() + }) + }); + + const result = await response.json(); + + if (response.status === 403 && result.blocked) { + this.isBlocked = true; + this.handleDeviceBlock(result); + return { blocked: true, reason: result.reason }; + } + + this.isBlocked = false; + this.lastCheck = Date.now(); + return { blocked: false }; + + } catch (error) { + console.error('[DeviceSecurity] Ошибка при проверке блокировки устройства:', error); + return { blocked: false, error: true }; + } + } + + /** + * Получает дополнительную информацию об устройстве + */ + async getDeviceInfo() { + try { + const basicFingerprint = await deviceFingerprint.getBasicFingerprint(); + + return { + userAgent: navigator.userAgent, + screenResolution: `${screen.width}x${screen.height}`, + platform: navigator.platform, + language: navigator.language, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timestamp: Date.now(), + visitCount: deviceFingerprint.getVisitCount(), + // Дополнительные метрики для анализа + connection: basicFingerprint.connection, + webgl: basicFingerprint.webgl?.renderer || 'unknown', + canvas: basicFingerprint.canvas ? 'available' : 'unavailable' + }; + } catch (error) { + console.error('[DeviceSecurity] Ошибка при получении информации об устройстве:', error); + return {}; + } + } + + /** + * Обрабатывает блокировку устройства + */ + handleDeviceBlock(blockInfo) { + console.log('[DeviceSecurity] Устройство заблокировано:', blockInfo); + + // Сохраняем информацию о блокировке + localStorage.setItem('deviceBlocked', JSON.stringify({ + blocked: true, + reason: blockInfo.reason, + blockedAt: blockInfo.blockedAt, + fingerprint: this.currentFingerprint, + timestamp: Date.now() + })); + + // Очищаем чувствительные данные + this.clearSensitiveData(); + + // Уведомляем подписчиков о блокировке + this.notifyBlockCallbacks(blockInfo); + + // Перенаправляем на страницу блокировки + this.redirectToBlockedPage(blockInfo); + } + + /** + * Обрабатывает изменение fingerprint устройства + */ + handleFingerprintChange() { + console.log('[DeviceSecurity] Device fingerprint изменился - возможна попытка обхода'); + + // Логируем подозрительную активность + const suspiciousActivity = { + type: 'fingerprint_change', + oldFingerprint: deviceFingerprint.loadFingerprint()?.fingerprint, + newFingerprint: this.currentFingerprint, + timestamp: Date.now(), + userAgent: navigator.userAgent + }; + + // Сохраняем в localStorage для отправки на сервер + const existing = JSON.parse(localStorage.getItem('suspiciousActivity') || '[]'); + existing.push(suspiciousActivity); + localStorage.setItem('suspiciousActivity', JSON.stringify(existing.slice(-10))); // Последние 10 событий + } + + /** + * Очищает чувствительные данные при блокировке + */ + clearSensitiveData() { + try { + // Очищаем пользовательские данные + localStorage.removeItem('userToken'); + localStorage.removeItem('userSettings'); + localStorage.removeItem('userPreferences'); + + // Очищаем кеши + if ('caches' in window) { + caches.keys().then(names => { + names.forEach(name => { + if (name.includes('user-data') || name.includes('api-cache')) { + caches.delete(name); + } + }); + }); + } + + // Очищаем IndexedDB (если используется) + if ('indexedDB' in window) { + // Здесь можно добавить очистку IndexedDB + } + + console.log('[DeviceSecurity] Чувствительные данные очищены'); + } catch (error) { + console.error('[DeviceSecurity] Ошибка при очистке данных:', error); + } + } + + /** + * Добавляет fingerprint в заголовки запросов + */ + injectFingerprintHeaders(headers = {}) { + if (this.currentFingerprint) { + headers['X-Device-Fingerprint'] = this.currentFingerprint; + headers['X-Screen-Resolution'] = `${screen.width}x${screen.height}`; + headers['X-Platform'] = navigator.platform; + headers['X-Language'] = navigator.language; + headers['X-Timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone; + } + return headers; + } + + /** + * Запускает периодическую проверку блокировки + */ + startPeriodicCheck() { + setInterval(async () => { + if (!this.isBlocked && this.currentFingerprint) { + await this.checkDeviceBlock(); + } + }, this.checkInterval); + } + + /** + * Добавляет callback для уведомления о блокировке + */ + onDeviceBlock(callback) { + this.blockCheckCallbacks.push(callback); + } + + /** + * Уведомляет подписчиков о блокировке + */ + notifyBlockCallbacks(blockInfo) { + this.blockCheckCallbacks.forEach(callback => { + try { + callback(blockInfo); + } catch (error) { + console.error('[DeviceSecurity] Ошибка в callback блокировки:', error); + } + }); + } + + /** + * Перенаправляет на страницу блокировки + */ + redirectToBlockedPage(blockInfo) { + const params = new URLSearchParams({ + blocked: 'true', + reason: blockInfo.reason || 'Устройство заблокировано', + type: 'device' + }); + + // Используем window.location для принудительного перенаправления + window.location.href = `/login?${params.toString()}`; + } + + /** + * Проверяет, заблокировано ли устройство (синхронная проверка) + */ + isDeviceBlocked() { + const stored = localStorage.getItem('deviceBlocked'); + if (stored) { + try { + const blockInfo = JSON.parse(stored); + return blockInfo.blocked === true; + } catch (e) { + return false; + } + } + return this.isBlocked; + } + + /** + * Отправляет подозрительную активность на сервер + */ + async reportSuspiciousActivity() { + try { + const activities = JSON.parse(localStorage.getItem('suspiciousActivity') || '[]'); + if (activities.length === 0) return; + + const response = await fetch('/api/security/report-suspicious', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.injectFingerprintHeaders() + }, + body: JSON.stringify({ + activities, + deviceInfo: await this.getDeviceInfo() + }) + }); + + if (response.ok) { + localStorage.removeItem('suspiciousActivity'); + console.log('[DeviceSecurity] Подозрительная активность отправлена на сервер'); + } + } catch (error) { + console.error('[DeviceSecurity] Ошибка при отправке подозрительной активности:', error); + } + } + + /** + * Проверяет целостность fingerprint + */ + async verifyFingerprintIntegrity() { + try { + const currentFingerprint = await deviceFingerprint.getDeviceFingerprint(); + + if (this.currentFingerprint !== currentFingerprint) { + console.warn('[DeviceSecurity] Обнаружено изменение fingerprint во время сессии'); + this.handleFingerprintChange(); + this.currentFingerprint = currentFingerprint; + return false; + } + + return true; + } catch (error) { + console.error('[DeviceSecurity] Ошибка при проверке целостности fingerprint:', error); + return false; + } + } + + /** + * Получает метрики безопасности для отправки на сервер + */ + getSecurityMetrics() { + return { + fingerprintStability: this.lastCheck ? Date.now() - this.lastCheck : 0, + visitCount: deviceFingerprint.getVisitCount(), + blockStatus: this.isBlocked, + lastFingerprintCheck: this.lastCheck, + suspiciousActivityCount: JSON.parse(localStorage.getItem('suspiciousActivity') || '[]').length + }; + } +} + +// Создаем единственный экземпляр сервиса +const deviceSecurityService = new DeviceSecurityService(); + +export default deviceSecurityService; \ No newline at end of file diff --git a/src/utils/deviceFingerprint.js b/src/utils/deviceFingerprint.js new file mode 100644 index 0000000..74892d9 --- /dev/null +++ b/src/utils/deviceFingerprint.js @@ -0,0 +1,394 @@ +/** + * Утилита для создания уникального отпечатка устройства (Device Fingerprinting) + * Используется для блокировки на уровне железа + */ + +class DeviceFingerprint { + constructor() { + this.components = new Map(); + this.fingerprint = null; + this.initialized = false; + } + + /** + * Инициализация и создание отпечатка устройства + */ + async initialize() { + try { + console.log('[DeviceFingerprint] Начинаем инициализацию отпечатка устройства...'); + + // Очищаем предыдущие компоненты + this.components.clear(); + + // Собираем компоненты отпечатка + await this.collectScreenInfo(); + await this.collectNavigatorInfo(); + await this.collectCanvasFingerprint(); + await this.collectWebGLFingerprint(); + await this.collectAudioFingerprint(); + await this.collectTimezoneInfo(); + await this.collectLanguageInfo(); + await this.collectPlatformInfo(); + await this.collectHardwareInfo(); + await this.collectStorageInfo(); + await this.collectNetworkInfo(); + + // Генерируем финальный отпечаток + this.fingerprint = await this.generateFingerprint(); + this.initialized = true; + + console.log('[DeviceFingerprint] Отпечаток устройства создан:', this.fingerprint.substring(0, 16) + '...'); + return this.fingerprint; + + } catch (error) { + console.error('[DeviceFingerprint] Ошибка при создании отпечатка:', error); + // Создаем базовый отпечаток даже при ошибках + this.fingerprint = await this.createFallbackFingerprint(); + this.initialized = true; + return this.fingerprint; + } + } + + /** + * Информация об экране + */ + async collectScreenInfo() { + try { + this.components.set('screen', { + width: screen.width, + height: screen.height, + availWidth: screen.availWidth, + availHeight: screen.availHeight, + colorDepth: screen.colorDepth, + pixelDepth: screen.pixelDepth, + devicePixelRatio: window.devicePixelRatio || 1 + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить информацию об экране:', e); + } + } + + /** + * Информация о навигаторе + */ + async collectNavigatorInfo() { + try { + this.components.set('navigator', { + userAgent: navigator.userAgent, + language: navigator.language, + languages: navigator.languages ? Array.from(navigator.languages) : [], + platform: navigator.platform, + cookieEnabled: navigator.cookieEnabled, + doNotTrack: navigator.doNotTrack, + maxTouchPoints: navigator.maxTouchPoints || 0, + hardwareConcurrency: navigator.hardwareConcurrency || 0, + deviceMemory: navigator.deviceMemory || 0 + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить информацию навигатора:', e); + } + } + + /** + * Canvas fingerprinting + */ + async collectCanvasFingerprint() { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Рисуем уникальный паттерн + ctx.textBaseline = 'top'; + ctx.font = '14px Arial'; + ctx.fillStyle = '#f60'; + ctx.fillRect(125, 1, 62, 20); + ctx.fillStyle = '#069'; + ctx.fillText('Device fingerprint text 🔐', 2, 15); + ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; + ctx.fillText('Device fingerprint text 🔐', 4, 17); + + // Добавляем геометрические фигуры + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = 'rgb(255,0,255)'; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI * 2, true); + ctx.closePath(); + ctx.fill(); + + const canvasData = canvas.toDataURL(); + this.components.set('canvas', this.hashString(canvasData)); + + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e); + } + } + + /** + * WebGL fingerprinting + */ + async collectWebGLFingerprint() { + try { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + + if (gl) { + const webglInfo = { + version: gl.getParameter(gl.VERSION), + vendor: gl.getParameter(gl.VENDOR), + renderer: gl.getParameter(gl.RENDERER), + shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION), + maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), + maxViewportDims: gl.getParameter(gl.MAX_VIEWPORT_DIMS) + }; + + this.components.set('webgl', webglInfo); + } + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить WebGL информацию:', e); + } + } + + /** + * Audio fingerprinting + */ + async collectAudioFingerprint() { + return new Promise((resolve) => { + try { + if (!window.AudioContext && !window.webkitAudioContext) { + resolve(); + return; + } + + const AudioContext = window.AudioContext || window.webkitAudioContext; + const context = new AudioContext(); + + const oscillator = context.createOscillator(); + const analyser = context.createAnalyser(); + const gainNode = context.createGain(); + const scriptProcessor = context.createScriptProcessor(4096, 1, 1); + + oscillator.type = 'triangle'; + oscillator.frequency.value = 10000; + + gainNode.gain.value = 0; + + oscillator.connect(analyser); + analyser.connect(scriptProcessor); + scriptProcessor.connect(gainNode); + gainNode.connect(context.destination); + + scriptProcessor.onaudioprocess = (event) => { + const bins = new Float32Array(analyser.frequencyBinCount); + analyser.getFloatFrequencyData(bins); + + const audioHash = this.hashString(bins.slice(0, 50).join(',')); + this.components.set('audio', audioHash); + + oscillator.disconnect(); + context.close(); + resolve(); + }; + + oscillator.start(); + + // Таймаут на случай проблем + setTimeout(() => { + try { + oscillator.disconnect(); + context.close(); + } catch (e) {} + resolve(); + }, 1000); + + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось создать audio fingerprint:', e); + resolve(); + } + }); + } + + /** + * Информация о временной зоне + */ + async collectTimezoneInfo() { + try { + this.components.set('timezone', { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timezoneOffset: new Date().getTimezoneOffset(), + locale: Intl.DateTimeFormat().resolvedOptions().locale + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить информацию о временной зоне:', e); + } + } + + /** + * Языковые настройки + */ + async collectLanguageInfo() { + try { + this.components.set('language', { + language: navigator.language, + languages: navigator.languages ? Array.from(navigator.languages) : [] + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить языковую информацию:', e); + } + } + + /** + * Платформа и ОС + */ + async collectPlatformInfo() { + try { + this.components.set('platform', { + platform: navigator.platform, + oscpu: navigator.oscpu, + appVersion: navigator.appVersion + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить информацию о платформе:', e); + } + } + + /** + * Аппаратная информация + */ + async collectHardwareInfo() { + try { + this.components.set('hardware', { + hardwareConcurrency: navigator.hardwareConcurrency || 0, + deviceMemory: navigator.deviceMemory || 0, + maxTouchPoints: navigator.maxTouchPoints || 0 + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить аппаратную информацию:', e); + } + } + + /** + * Информация о хранилище + */ + async collectStorageInfo() { + try { + let storageQuota = 0; + if ('storage' in navigator && 'estimate' in navigator.storage) { + const estimate = await navigator.storage.estimate(); + storageQuota = estimate.quota || 0; + } + + this.components.set('storage', { + localStorage: !!window.localStorage, + sessionStorage: !!window.sessionStorage, + indexedDB: !!window.indexedDB, + storageQuota + }); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить информацию о хранилище:', e); + } + } + + /** + * Сетевая информация + */ + async collectNetworkInfo() { + try { + const networkInfo = {}; + + if ('connection' in navigator) { + const conn = navigator.connection; + networkInfo.effectiveType = conn.effectiveType; + networkInfo.downlink = conn.downlink; + networkInfo.rtt = conn.rtt; + } + + this.components.set('network', networkInfo); + } catch (e) { + console.warn('[DeviceFingerprint] Не удалось получить сетевую информацию:', e); + } + } + + /** + * Генерация финального отпечатка + */ + async generateFingerprint() { + const componentsString = JSON.stringify(Array.from(this.components.entries()).sort()); + return await this.hashString(componentsString); + } + + /** + * Создание резервного отпечатка при ошибках + */ + async createFallbackFingerprint() { + const fallbackData = { + userAgent: navigator.userAgent, + screen: `${screen.width}x${screen.height}`, + timezone: new Date().getTimezoneOffset(), + language: navigator.language, + timestamp: Date.now() + }; + + return await this.hashString(JSON.stringify(fallbackData)); + } + + /** + * Хеширование строки + */ + async hashString(str) { + if (crypto && crypto.subtle) { + const encoder = new TextEncoder(); + const data = encoder.encode(str); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } else { + // Fallback для старых браузеров + return this.simpleHash(str); + } + } + + /** + * Простое хеширование для старых браузеров + */ + simpleHash(str) { + let hash = 0; + if (str.length === 0) return hash.toString(); + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Конвертируем в 32-битное целое + } + return Math.abs(hash).toString(16); + } + + /** + * Получение текущего отпечатка + */ + getFingerprint() { + return this.fingerprint; + } + + /** + * Проверка инициализации + */ + isInitialized() { + return this.initialized; + } + + /** + * Получение компонентов отпечатка (для отладки) + */ + getComponents() { + return Object.fromEntries(this.components); + } + + /** + * Обновление отпечатка + */ + async refresh() { + return await this.initialize(); + } +} + +// Экспортируем класс +export default DeviceFingerprint; \ No newline at end of file diff --git a/src/views/DeviceBlockedPage.vue b/src/views/DeviceBlockedPage.vue new file mode 100644 index 0000000..b8d3e4a --- /dev/null +++ b/src/views/DeviceBlockedPage.vue @@ -0,0 +1,666 @@ + + + + + \ No newline at end of file diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index d3c40f3..af45be9 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -27,6 +27,18 @@

+ + +
+ +
+

Доступ с устройства заблокирован

+

{{ deviceBlockedInfo.message }}

+

+ Причина: {{ deviceBlockedInfo.reason }} +

+
+
@@ -95,27 +107,38 @@ import { ref, onMounted, onUnmounted, computed } from 'vue'; import { useAuth } from '@/auth'; import { useRoute } from 'vue-router'; +import deviceSecurityService from '@/services/deviceSecurity'; const route = useRoute(); const email = ref(''); const password = ref(''); const errorMessage = ref(''); const loading = ref(false); +const securityLoading = ref(true); const blockedInfo = ref({ blocked: false, message: 'Ваш аккаунт был заблокирован администратором.', reason: 'Нарушение правил сервиса', timestamp: '' }); +const deviceBlockedInfo = ref({ + blocked: false, + message: 'Доступ с данного устройства заблокирован.', + reason: 'Подозрительная активность', + timestamp: '' +}); const { login } = useAuth(); -// Проверяем, был ли аккаунт заблокирован +// Проверяем, был ли аккаунт или устройство заблокированы const accountBlocked = computed(() => { - // Проверяем query параметр и информацию из localStorage return route.query.blocked === 'true' || blockedInfo.value.blocked; }); +const deviceBlocked = computed(() => { + return route.query.type === 'device' || deviceBlockedInfo.value.blocked || deviceSecurityService.isDeviceBlocked(); +}); + // Функция для блокировки прокрутки страницы const preventScroll = () => { document.body.style.overflow = 'hidden'; @@ -132,18 +155,59 @@ const enableScroll = () => { document.body.style.width = ''; }; -onMounted(() => { - preventScroll(); // Блокируем прокрутку при монтировании компонента +onMounted(async () => { + preventScroll(); - // Проверяем, есть ли информация о блокировке в localStorage + try { + console.log('[LoginView] Инициализация системы безопасности устройств...'); + + // Инициализируем систему безопасности устройств + const securityResult = await deviceSecurityService.initialize(); + + console.log('[LoginView] Результат инициализации безопасности:', securityResult); + + // Проверяем, не заблокировано ли устройство + if (securityResult.isBlocked) { + deviceBlockedInfo.value = { + blocked: true, + message: 'Доступ с данного устройства заблокирован', + reason: 'Устройство находится в черном списке', + timestamp: new Date().toISOString() + }; + } + + } catch (error) { + console.error('[LoginView] Ошибка при инициализации системы безопасности:', error); + + // При критической ошибке системы безопасности показываем предупреждение + if (error.response?.status === 403) { + deviceBlockedInfo.value = { + blocked: true, + message: 'Доступ с данного устройства заблокирован', + reason: error.response.data?.reason || 'Подозрительная активность', + timestamp: new Date().toISOString() + }; + } + } finally { + securityLoading.value = false; + } + + // Проверяем query параметры для блокировки устройства + if (route.query.type === 'device' && route.query.blocked === 'true') { + deviceBlockedInfo.value = { + blocked: true, + message: decodeURIComponent(route.query.message || 'Доступ с данного устройства заблокирован'), + reason: decodeURIComponent(route.query.reason || 'Подозрительная активность'), + timestamp: route.query.blocked_at || new Date().toISOString() + }; + } + + // Проверяем информацию о блокировке аккаунта из localStorage const storedBlockInfo = localStorage.getItem('accountBlockedInfo'); if (storedBlockInfo) { try { const parsedInfo = JSON.parse(storedBlockInfo); blockedInfo.value = parsedInfo; - - // Удаляем дублирующее сообщение об ошибке, так как эта информация уже - // отображается в блоке blocked-account-message выше кнопки "Войти" } catch (e) { console.error('Ошибка при парсинге информации о блокировке:', e); } @@ -151,7 +215,7 @@ onMounted(() => { }); onUnmounted(() => { - enableScroll(); // Восстанавливаем прокрутку при размонтировании компонента + enableScroll(); }); const handleLogin = async () => { @@ -165,9 +229,96 @@ const handleLogin = async () => { } try { - await login({ email: email.value, password: password.value }); + // Проверяем, инициализирована ли система безопасности + if (!deviceSecurityService.isInitialized()) { + errorMessage.value = 'Система безопасности не инициализирована. Обновите страницу.'; + loading.value = false; + return; + } + + // Проверяем блокировку устройства перед попыткой входа + if (deviceSecurityService.isDeviceBlocked()) { + errorMessage.value = 'Доступ с данного устройства заблокирован.'; + loading.value = false; + return; + } + + // Получаем отпечаток устройства для отправки на сервер + const deviceFingerprint = deviceSecurityService.getCurrentFingerprint(); + + if (!deviceFingerprint) { + errorMessage.value = 'Не удалось создать отпечаток устройства. Обновите страницу.'; + loading.value = false; + return; + } + + // Выполняем вход с информацией о устройстве + const loginResult = await login({ + email: email.value, + password: password.value, + deviceFingerprint: deviceFingerprint, + deviceInfo: { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + screenResolution: `${screen.width}x${screen.height}`, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timestamp: Date.now() + } + }); + + // Записываем успешную попытку входа + deviceSecurityService.recordLoginAttempt(true); + + console.log('[LoginView] Успешный вход:', loginResult); + } catch (error) { - errorMessage.value = error.message || 'Произошла неизвестная ошибка при входе.'; + console.error('[LoginView] Ошибка при входе:', error); + + // Записываем неудачную попытку входа + deviceSecurityService.recordLoginAttempt(false); + + // Проверяем, не связана ли ошибка с блокировкой устройства + if (error.response?.status === 403) { + const responseData = error.response.data; + + if (responseData?.code === 'DEVICE_BLOCKED') { + deviceBlockedInfo.value = { + blocked: true, + message: responseData.message || 'Доступ с данного устройства заблокирован', + reason: responseData.reason || 'Подозрительная активность', + timestamp: responseData.blockedAt || new Date().toISOString() + }; + + // Обновляем состояние в сервисе безопасности + deviceSecurityService.handleDeviceBlocked(responseData); + + } else if (responseData?.code === 'ACCOUNT_BLOCKED') { + blockedInfo.value = { + blocked: true, + message: responseData.message || 'Ваш аккаунт был заблокирован', + reason: responseData.reason || 'Нарушение правил сервиса', + timestamp: responseData.blockedAt || new Date().toISOString() + }; + } else { + errorMessage.value = responseData?.message || 'Доступ запрещен.'; + } + } else if (error.response?.status === 429) { + // Слишком много попыток входа + errorMessage.value = 'Слишком много попыток входа. Попробуйте позже.'; + + // Записываем подозрительную активность + deviceSecurityService.recordSuspiciousActivity({ + type: 'rate_limit_exceeded', + endpoint: '/auth/login', + timestamp: Date.now() + }); + + } else if (error.response?.status === 401) { + errorMessage.value = 'Неверный email или пароль.'; + } else { + errorMessage.value = error.message || 'Произошла неизвестная ошибка при входе.'; + } } finally { loading.value = false; } diff --git a/src/views/admin/AdminBlockedDevices.vue b/src/views/admin/AdminBlockedDevices.vue new file mode 100644 index 0000000..fd83bbb --- /dev/null +++ b/src/views/admin/AdminBlockedDevices.vue @@ -0,0 +1,682 @@ + + + + + \ No newline at end of file