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 @@
+
+
+
+
+
+
+
Загрузка заблокированных устройств...
+
+
+
+
{{ error }}
+
+
+
+
+
+
Заблокированные устройства не найдены
+
+
+
+
+
+
+
+ Пользователь:
+
+ {{ device.blockedUserId.name }} ({{ device.blockedUserId.email }})
+
+ Неизвестен
+
+
+
+ Fingerprint:
+ {{ device.deviceFingerprint.substring(0, 32) }}...
+
+
+
+ Причина блокировки:
+ {{ device.reason }}
+
+
+
+ Заблокировано:
+ {{ formatDate(device.blockedAt) }}
+
+ ({{ device.blockedBy.name }})
+
+
+
+
+ Разблокировано:
+ {{ formatDate(device.unblockedAt) }}
+
+
+
+ Попытки обхода:
+ {{ device.bypassAttempts.length }}
+
+
+
+
Информация об устройстве:
+
+
+ User Agent: {{ device.deviceInfo.userAgent.substring(0, 60) }}...
+
+
+ Платформа: {{ device.deviceInfo.platform }}
+
+
+ Экран: {{ device.deviceInfo.screen.width }}x{{ device.deviceInfo.screen.height }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Основная информация
+
+
+ ID устройства:
+ {{ selectedDevice._id }}
+
+
+ Device Fingerprint:
+ {{ selectedDevice.deviceFingerprint }}
+
+
+ Статус:
+
+ {{ selectedDevice.isActive ? 'Заблокировано' : 'Разблокировано' }}
+
+
+
+ Причина:
+ {{ selectedDevice.reason }}
+
+
+
+
+
+
+ Заблокированный пользователь
+
+
+ Имя:
+ {{ selectedDevice.blockedUserId.name }}
+
+
+ Email:
+ {{ selectedDevice.blockedUserId.email }}
+
+
+ ID:
+ {{ selectedDevice.blockedUserId._id }}
+
+
+
+
+
+
+ Техническая информация
+
+
+ User Agent:
+ {{ selectedDevice.deviceInfo.userAgent }}
+
+
+ Платформа:
+ {{ selectedDevice.deviceInfo.platform }}
+
+
+ Разрешение экрана:
+ {{ selectedDevice.deviceInfo.screen.width }}x{{ selectedDevice.deviceInfo.screen.height }}
+
+
+ Часовой пояс:
+ {{ selectedDevice.deviceInfo.timezone }}
+
+
+ Язык:
+ {{ selectedDevice.deviceInfo.language }}
+
+
+
+
+
+
+ Попытки обхода блокировки ({{ selectedDevice.bypassAttempts.length }})
+
+
+
+
+
+ IP: {{ attempt.ipAddress }}
+
+
+ User Agent: {{ attempt.userAgent }}
+
+
+
+
+
+
+
+
+ Временные метки
+
+
+ Заблокировано:
+ {{ formatDate(selectedDevice.blockedAt) }}
+
+
+ Заблокировал:
+ {{ selectedDevice.blockedBy.name }} ({{ selectedDevice.blockedBy.email }})
+
+
+ Разблокировано:
+ {{ formatDate(selectedDevice.unblockedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
🔒
+
+
{{ statistics.totalBlocked }}
+
Заблокированных устройств
+
+
+
+
⚠️
+
+
{{ statistics.todayBlocked }}
+
Заблокировано сегодня
+
+
+
+
🔓
+
+
{{ statistics.recentUnblocked }}
+
Разблокировано за неделю
+
+
+
+
🎯
+
+
{{ statistics.bypassAttempts }}
+
Попыток обхода
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Загрузка заблокированных устройств...
+
+
+
+
{{ error }}
+
+
+
+
+
Заблокированных устройств не найдено
+
+
+
+
+
+ Устройство |
+ Пользователь |
+ Причина |
+ Дата блокировки |
+ Статус |
+ Попытки обхода |
+ Действия |
+
+
+
+
+
+
+ {{ formatFingerprint(device.deviceFingerprint) }}
+
+
+ {{ formatUserAgent(device.deviceInfo.userAgent) }}
+
+
+ {{ device.deviceInfo.platform }}
+
+
+
+ |
+
+
+
+ {{ device.blockedUserId?.name || 'Неизвестно' }}
+ {{ device.blockedUserId?.email || 'Нет email' }}
+
+ |
+
+
+
+ {{ device.reason }}
+ Авто
+
+ |
+
+
+
+ {{ formatDate(device.blockedAt) }}
+ {{ device.blockedBy?.name || 'Система' }}
+
+ |
+
+
+
+ {{ device.isActive ? 'Активна' : 'Снята' }}
+
+ |
+
+
+
+ {{ device.bypassAttempts?.length || 0 }}
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Основная информация
+
+
+ Device Fingerprint:
+ {{ selectedDevice.deviceFingerprint }}
+
+
+ Дата блокировки:
+ {{ formatDate(selectedDevice.blockedAt) }}
+
+
+ Заблокировал:
+ {{ selectedDevice.blockedBy?.name || 'Система' }}
+
+
+ Причина:
+ {{ selectedDevice.reason }}
+
+
+ Статус:
+
+ {{ selectedDevice.isActive ? 'Активна' : 'Снята' }}
+
+
+
+
+
+
+
Информация об устройстве
+
+
+ User Agent:
+ {{ selectedDevice.deviceInfo.userAgent }}
+
+
+ Платформа:
+ {{ selectedDevice.deviceInfo.platform }}
+
+
+ Разрешение экрана:
+ {{ selectedDevice.deviceInfo.screenResolution }}
+
+
+ Часовой пояс:
+ {{ selectedDevice.deviceInfo.timezone }}
+
+
+
+
+
+
Попытки обхода блокировки ({{ selectedDevice.bypassAttempts.length }})
+
+
+
+
+
+ IP: {{ attempt.ipAddress }}
+
+
+ User Agent: {{ attempt.userAgent }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
Устройство заблокировано
+
+
+
+ Ваше устройство было заблокировано администрацией сервиса.
+
+
+
+
+ Причина блокировки:
+ {{ blockInfo.reason || 'Нарушение правил сервиса' }}
+
+
+
+ Дата блокировки:
+ {{ formatDate(blockInfo.blockedAt) }}
+
+
+
+ ID устройства:
+ {{ blockInfo.deviceId.slice(-8) }}
+
+
+
+
+
+
Что это означает?
+
+ - Ваше устройство заблокировано для использования сервиса
+ - Доступ к функциям приложения ограничен
+ - Блокировка может быть связана с нарушением правил
+
+
+
+
+
+
+
Дополнительная информация
+
+
+
Правила сервиса
+
Ознакомьтесь с правилами использования нашего сервиса
+
Читать правила
+
+
+
+
Безопасность
+
Узнайте больше о мерах безопасности
+
О безопасности
+
+
+
+
+
+
⚠️
+
+ Внимание!
+ Попытки обойти блокировку устройства могут привести к
+ дополнительным ограничениям и продлению срока блокировки.
+
+
+
+
+
+
+
+
+
+
\ 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 }}
+
+
+