добавление блокировки по железу

This commit is contained in:
Professional 2025-05-26 17:45:32 +07:00
parent c97efd7a5a
commit 2317ecafca
20 changed files with 6362 additions and 73 deletions

View File

@ -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
};

View File

@ -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
};

View File

@ -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);

View File

@ -9,6 +9,12 @@ const {
updateReportStatus, updateReportStatus,
getReportsStats getReportsStats
} = require('../controllers/reportController'); } = require('../controllers/reportController');
const {
blockDevice,
unblockDeviceAdmin,
getBlockedDevicesList,
getBlockedDeviceDetails
} = require('../controllers/deviceSecurityController');
// Все маршруты защищены middleware для проверки авторизации и прав администратора // Все маршруты защищены middleware для проверки авторизации и прав администратора
router.use(protect, adminMiddleware); router.use(protect, adminMiddleware);
@ -32,4 +38,10 @@ router.get('/conversations', adminController.getAllConversations);
router.get('/conversations/:id', adminController.getConversationById); // Новый маршрут для получения диалога по ID router.get('/conversations/:id', adminController.getConversationById); // Новый маршрут для получения диалога по ID
router.get('/conversations/:id/messages', adminController.getConversationMessages); 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; module.exports = router;

View File

@ -11,6 +11,7 @@ const {
// Импортируем middleware для защиты маршрутов // Импортируем middleware для защиты маршрутов
const { protect } = require('../middleware/authMiddleware'); const { protect } = require('../middleware/authMiddleware');
const { checkDeviceBlock, recordDeviceActivity } = require('../middleware/deviceBlockMiddleware');
// --- НАЧАЛО ИЗМЕНЕНИЯ --- // --- НАЧАЛО ИЗМЕНЕНИЯ ---
// Дополнительная отладка: Проверяем, что protect действительно функция // Дополнительная отладка: Проверяем, что 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 loginUser:', typeof loginUser);
console.log('[DEBUG] authRoutes.js - typeof getMe:', typeof getMe); console.log('[DEBUG] authRoutes.js - typeof getMe:', typeof getMe);
console.log('[DEBUG] authRoutes.js - typeof protect:', typeof protect); 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; module.exports = router;

View File

@ -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;

View File

@ -15,6 +15,7 @@ const userRoutes = require('./routes/userRoutes'); // 1. Импортируем
const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes
const adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа const adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа
const reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб const reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб
const securityRoutes = require('./routes/securityRoutes'); // Импортируем маршруты безопасности
const Message = require('./models/Message'); // Импорт модели Message const Message = require('./models/Message'); // Импорт модели Message
const Conversation = require('./models/Conversation'); // Импорт модели Conversation const Conversation = require('./models/Conversation'); // Импорт модели Conversation
const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта
@ -87,6 +88,7 @@ app.use('/api/actions', actionRoutes);
app.use('/api/conversations', conversationRoutes); app.use('/api/conversations', conversationRoutes);
app.use('/api/reports', reportRoutes); // Подключаем маршруты для жалоб app.use('/api/reports', reportRoutes); // Подключаем маршруты для жалоб
app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели
app.use('/api/security', securityRoutes); // Подключаем маршруты для системы безопасности устройств
// Socket.IO логика // Socket.IO логика
let activeUsers = []; let activeUsers = [];

View File

@ -1,6 +1,7 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import api from './services/api'; // Наш API сервис import api from './services/api'; // Наш API сервис
import router from './router'; // Vue Router для перенаправлений import router from './router'; // Vue Router для перенаправлений
import deviceSecurityService from './services/deviceSecurity'; // Система безопасности устройств
// Реактивные переменные для состояния пользователя и токена // Реактивные переменные для состояния пользователя и токена
const user = ref(null); // Данные пользователя (или null, если не вошел) const user = ref(null); // Данные пользователя (или null, если не вошел)
@ -58,6 +59,11 @@ async function login(credentials) {
try { try {
console.log('Попытка входа с учетными данными:', { email: credentials.email }); console.log('Попытка входа с учетными данными:', { email: credentials.email });
// Проверяем, не заблокировано ли устройство перед попыткой входа
if (deviceSecurityService.isDeviceBlocked()) {
throw new Error('Доступ с данного устройства заблокирован');
}
const response = await api.login(credentials); const response = await api.login(credentials);
console.log('Ответ сервера при входе:', response); console.log('Ответ сервера при входе:', response);
@ -79,6 +85,11 @@ async function login(credentials) {
// Сохраняем данные пользователя и токен // Сохраняем данные пользователя и токен
setUserData(userData, userToken); setUserData(userData, userToken);
// Записываем успешную попытку входа в систему безопасности
if (deviceSecurityService.isInitialized()) {
deviceSecurityService.recordLoginAttempt(true);
}
// После сохранения данных, но до перенаправления // После сохранения данных, но до перенаправления
console.log('isAuthenticated ПОСЛЕ входа:', isAuthenticated.value); console.log('isAuthenticated ПОСЛЕ входа:', isAuthenticated.value);
console.log('Данные пользователя ПОСЛЕ входа:', user.value); console.log('Данные пользователя ПОСЛЕ входа:', user.value);
@ -93,6 +104,25 @@ async function login(credentials) {
return true; return true;
} catch (error) { } catch (error) {
console.error('Ошибка входа:', error.response ? error.response.data : error.message); 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('Ошибка сети или сервера при входе'); throw error.response ? error.response.data : new Error('Ошибка сети или сервера при входе');
} }
@ -120,6 +150,12 @@ async function logout() {
user.value = null; user.value = null;
token.value = null; token.value = null;
localStorage.removeItem('userToken'); localStorage.removeItem('userToken');
// Очищаем данные в системе безопасности устройств
if (deviceSecurityService.isInitialized()) {
deviceSecurityService.clearUserSession();
}
// Удаляем токен из заголовков Axios (если устанавливали его напрямую) // Удаляем токен из заголовков Axios (если устанавливали его напрямую)
// delete apiClient.defaults.headers.common['Authorization']; // apiClient не экспортируется из api.js напрямую для этого // delete apiClient.defaults.headers.common['Authorization']; // apiClient не экспортируется из api.js напрямую для этого
// Наш интерсептор запросов в api.js перестанет добавлять токен, так как его не будет в localStorage. // Наш интерсептор запросов в api.js перестанет добавлять токен, так как его не будет в localStorage.
@ -211,6 +247,53 @@ function handleAccountUnblocked(data) {
console.log('Аккаунт пользователя разблокирован.'); 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() { export function useAuth() {
return { return {
@ -222,6 +305,7 @@ export function useAuth() {
logout, logout,
fetchUser, // Эту функцию нужно будет вызвать при инициализации приложения fetchUser, // Эту функцию нужно будет вызвать при инициализации приложения
handleAccountBlocked, // Добавляем функцию обработки блокировки аккаунта handleAccountBlocked, // Добавляем функцию обработки блокировки аккаунта
handleAccountUnblocked // Добавляем функцию обработки разблокировки аккаунта handleAccountUnblocked, // Добавляем функцию обработки разблокировки аккаунта
handleDeviceBlocked // Добавляем функцию обработки блокировки устройства
}; };
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import router from './router';
import { useAuth } from './auth'; import { useAuth } from './auth';
import Vue3TouchEvents from 'vue3-touch-events'; import Vue3TouchEvents from 'vue3-touch-events';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import deviceSecurityService from './services/deviceSecurity';
// Создаем приложение // Создаем приложение
const app = createApp(App); const app = createApp(App);
@ -15,11 +16,27 @@ app.use(Vue3TouchEvents);
app.use(router); app.use(router);
// Получаем экземпляр нашего auth "стора" // Получаем экземпляр нашего auth "стора"
const { fetchUser } = useAuth(); const { fetchUser, handleDeviceBlocked } = useAuth();
// Асинхронная самовызывающаяся функция для инициализации // Асинхронная самовызывающаяся функция для инициализации
(async () => { (async () => {
try { 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 await fetchUser(); // Пытаемся загрузить пользователя по токену из localStorage
} catch (error) { } catch (error) {
console.error("Ошибка при начальной загрузке пользователя в main.js:", error); console.error("Ошибка при начальной загрузке пользователя в main.js:", error);

View File

@ -57,6 +57,12 @@ const routes = [
component: () => import('../views/PreferencesView.vue'), component: () => import('../views/PreferencesView.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
// Страница блокировки устройства
{
path: '/device-blocked',
name: 'DeviceBlocked',
component: () => import('../views/DeviceBlockedView.vue'),
},
// Маршруты для админ-панели // Маршруты для админ-панели
{ {
path: '/admin', path: '/admin',
@ -109,6 +115,12 @@ const routes = [
component: () => import('../views/admin/AdminReportDetail.vue'), component: () => import('../views/admin/AdminReportDetail.vue'),
meta: { requiresAuth: true, requiresAdmin: true }, meta: { requiresAuth: true, requiresAdmin: true },
props: 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 ? 'Да' : 'Нет'); 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] Пользователь - администратор, перенаправляем на панель администратора'); console.log('[ROUTER GUARD] Пользователь - администратор, перенаправляем на панель администратора');
return next({ name: 'AdminDashboard' }); return next({ name: 'AdminDashboard' });
} }

View File

@ -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();

View File

@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { useAuth } from '../auth'; // Импортируем функцию авторизации import { useAuth } from '../auth'; // Импортируем функцию авторизации
import { isAccountBlocked } from './socketService'; // Импортируем проверку блокировки из socketService import { isAccountBlocked } from './socketService'; // Импортируем проверку блокировки из socketService
import deviceSecurityService from './deviceSecurityService'; // Импортируем сервис безопасности устройств
// Создаем экземпляр Axios с базовым URL нашего API // Создаем экземпляр Axios с базовым URL нашего API
// Настраиваем базовый URL в зависимости от окружения // Настраиваем базовый URL в зависимости от окружения
@ -26,6 +27,12 @@ const checkAccountBlockStatus = () => {
return true; return true;
} }
// Проверяем блокировку устройства
if (deviceSecurityService && deviceSecurityService.isDeviceBlocked()) {
console.warn('[API] Устройство заблокировано, запросы невозможны');
return true;
}
return false; return false;
} catch (e) { } catch (e) {
console.error('[API] Ошибка при проверке статуса блокировки:', e); console.error('[API] Ошибка при проверке статуса блокировки:', e);
@ -37,81 +44,88 @@ const checkAccountBlockStatus = () => {
const getBaseUrl = () => { const getBaseUrl = () => {
// Всегда используем относительный путь /api. // Всегда используем относительный путь /api.
// В режиме разработки Vite dev server будет проксировать эти запросы согласно настройке server.proxy в vite.config.js. // В режиме разработки Vite dev server будет проксировать эти запросы согласно настройке server.proxy в vite.config.js.
// В продакшене предполагается, что веб-сервер (например, Nginx) настроен аналогично для обработки /api. return '/api';
// return '/api';
return import.meta.env.VITE_API_BASE_URL; // Используем переменную окружения Vite
}; };
// Создаем экземпляр Axios
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: getBaseUrl(), baseURL: getBaseUrl(),
timeout: 30000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
// (Опционально, но очень полезно) Перехватчик для добавления JWT токена к запросам // Интерсептор запросов для добавления токена авторизации и device fingerprint
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { async (config) => {
// Проверяем, заблокирован ли аккаунт, ПЕРЕД выполнением запроса // Проверяем блокировку перед отправкой запроса
if (checkAccountBlockStatus()) { if (checkAccountBlockStatus()) {
// Если аккаунт заблокирован, отменяем запрос const error = new Error('Запрос отменен: аккаунт или устройство заблокированы');
const source = axios.CancelToken.source(); error.code = 'ACCOUNT_OR_DEVICE_BLOCKED';
config.cancelToken = source.token; throw error;
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 token = localStorage.getItem('userToken'); // Предполагаем, что токен хранится в localStorage // Добавляем токен авторизации
const token = localStorage.getItem('userToken');
if (token) { if (token) {
console.log('[API] Добавление токена к запросу:', config.url); config.headers.Authorization = `Bearer ${token}`;
config.headers['Authorization'] = `Bearer ${token}`;
} else {
console.log('[API] Токен не найден для запроса:', config.url);
} }
// Добавляем 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(); const source = axios.CancelToken.source();
config.cancelToken = source.token; config.cancelToken = source.token;
// Сохраняем источник токена отмены в Map по URL запроса как ключу
const requestKey = `${config.method}-${config.url}`;
cancelTokenSources.set(requestKey, source); cancelTokenSources.set(requestKey, source);
console.log(`[API] Отправка ${config.method?.toUpperCase()} запроса:`, config.url);
return config; return config;
}, },
(error) => { (error) => {
console.error('[API] Ошибка в перехватчике запросов:', error); console.error('[API] Ошибка в интерсепторе запросов:', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// (Опционально) Перехватчик ответов для обработки глобальных ошибок, например, 401 // Интерсептор ответов для обработки ошибок
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => { (response) => {
// Удаляем источник токена отмены после успешного ответа // Удаляем источник токена отмены при успешном ответе
const requestKey = `${response.config.method}-${response.config.url}`; const requestKey = `${response.config.method}-${response.config.url}`;
cancelTokenSources.delete(requestKey); cancelTokenSources.delete(requestKey);
console.log(`[API] Успешный ответ от ${response.config.url}:`, response.status);
return response; return response;
}, },
(error) => { async (error) => {
// Удаляем источник токена отмены при ошибке // Удаляем источник токена отмены при ошибке
if (error.config) { if (error.config) {
const requestKey = `${error.config.method}-${error.config.url}`; const requestKey = `${error.config.method}-${error.config.url}`;
cancelTokenSources.delete(requestKey); cancelTokenSources.delete(requestKey);
} }
// Проверяем на блокировку аккаунта // Проверяем на блокировку устройства
if (error.response && error.response.status === 403) { if (error.response && error.response.status === 403) {
const errorData = error.response.data; 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 && ( if (errorData && (
errorData.blocked === true || errorData.blocked === true ||
@ -125,30 +139,60 @@ apiClient.interceptors.response.use(
const { handleAccountBlocked } = useAuth(); const { handleAccountBlocked } = useAuth();
if (typeof handleAccountBlocked === 'function') { if (typeof handleAccountBlocked === 'function') {
handleAccountBlocked(errorData); 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) { if (error.response && error.response.status === 401) {
console.error('[API] Неавторизованный запрос (401):', error.config.url); console.warn('[API] Ошибка авторизации, возможно токен истек');
// Обходим замыкание, чтобы избежать проблем с импортом // Вызываем logout для очистки состояния
// При использовании такого подхода, logout будет вызван после текущего цикла выполнения JS
setTimeout(() => { setTimeout(() => {
const { logout } = useAuth(); const { logout } = useAuth();
if (typeof logout === 'function') {
logout(); logout();
}, 0);
} }
}, 100);
}
console.error(`[API] Ошибка запроса к ${error.config?.url}:`, error.response?.status, error.response?.data);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// Функция для отмены всех активных запросов // Функция для отмены всех активных запросов (при блокировке)
const cancelActiveRequests = () => { const cancelActiveRequests = () => {
console.log('[API] Отмена всех активных запросов. Количество: ' + cancelTokenSources.size);
cancelTokenSources.forEach((source, key) => { cancelTokenSources.forEach((source, key) => {
source.cancel('Запрос отменен из-за блокировки аккаунта'); source.cancel(`Запрос ${key} отменен из-за блокировки`);
console.log(`[API] Запрос ${key} отменен`); console.log(`[API] Запрос ${key} отменен`);
}); });
cancelTokenSources.clear(); cancelTokenSources.clear();
@ -243,16 +287,36 @@ export default {
getReports(params = {}) { getReports(params = {}) {
return apiClient.get('/admin/reports', { params }); return apiClient.get('/admin/reports', { params });
}, },
getReportById(reportId) { getReportById(reportId) {
return apiClient.get(`/admin/reports/${reportId}`); return apiClient.get(`/admin/reports/${reportId}`);
}, },
reviewReport(reportId, reviewData) {
updateReportStatus(reportId, updateData) { return apiClient.put(`/admin/reports/${reportId}/review`, reviewData);
return apiClient.put(`/admin/reports/${reportId}`, updateData); },
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 });
} }
}; };

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,666 @@
<template>
<div class="device-blocked-page">
<div class="blocked-container">
<div class="blocked-icon">
<svg viewBox="0 0 24 24" width="80" height="80" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM13 17h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>
</div>
<div class="blocked-content">
<h1>Устройство заблокировано</h1>
<div class="block-info">
<p class="main-message">
Ваше устройство было заблокировано администрацией сервиса.
</p>
<div v-if="blockInfo" class="block-details">
<div class="detail-item">
<strong>Причина блокировки:</strong>
<span>{{ blockInfo.reason || 'Нарушение правил сервиса' }}</span>
</div>
<div v-if="blockInfo.blockedAt" class="detail-item">
<strong>Дата блокировки:</strong>
<span>{{ formatDate(blockInfo.blockedAt) }}</span>
</div>
<div v-if="blockInfo.deviceId" class="detail-item">
<strong>ID устройства:</strong>
<code>{{ blockInfo.deviceId.slice(-8) }}</code>
</div>
</div>
</div>
<div class="help-section">
<h3>Что это означает?</h3>
<ul>
<li>Ваше устройство заблокировано для использования сервиса</li>
<li>Доступ к функциям приложения ограничен</li>
<li>Блокировка может быть связана с нарушением правил</li>
</ul>
</div>
<div class="contact-section">
<h3>Как обжаловать блокировку?</h3>
<p>
Если вы считаете, что блокировка была применена ошибочно,
вы можете обратиться в службу поддержки:
</p>
<div class="contact-methods">
<div class="contact-item">
<strong>Email:</strong>
<a href="mailto:support@datingapp.com">support@datingapp.com</a>
</div>
<div class="contact-item">
<strong>Telegram:</strong>
<a href="https://t.me/datingapp_support" target="_blank">@datingapp_support</a>
</div>
</div>
<div class="appeal-form">
<h4>Отправить обращение</h4>
<form @submit.prevent="submitAppeal" class="appeal-form-content">
<div class="form-group">
<label for="email">Ваш email:</label>
<input
type="email"
id="email"
v-model="appealForm.email"
required
placeholder="your@email.com"
>
</div>
<div class="form-group">
<label for="message">Сообщение:</label>
<textarea
id="message"
v-model="appealForm.message"
required
placeholder="Опишите ситуацию и почему считаете блокировку ошибочной..."
rows="4"
></textarea>
</div>
<button
type="submit"
:disabled="appealLoading"
class="btn btn-primary"
>
{{ appealLoading ? 'Отправка...' : 'Отправить обращение' }}
</button>
</form>
<div v-if="appealSent" class="appeal-success">
<p> Ваше обращение отправлено! Мы рассмотрим его в ближайшее время.</p>
</div>
</div>
</div>
<div class="additional-info">
<h3>Дополнительная информация</h3>
<div class="info-grid">
<div class="info-card">
<h4>Правила сервиса</h4>
<p>Ознакомьтесь с правилами использования нашего сервиса</p>
<a href="/terms" class="link">Читать правила</a>
</div>
<div class="info-card">
<h4>Безопасность</h4>
<p>Узнайте больше о мерах безопасности</p>
<a href="/security" class="link">О безопасности</a>
</div>
</div>
</div>
<div class="bypass-warning">
<div class="warning-icon"></div>
<div class="warning-text">
<strong>Внимание!</strong>
Попытки обойти блокировку устройства могут привести к
дополнительным ограничениям и продлению срока блокировки.
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import deviceSecurityService from '@/services/deviceSecurityService';
export default {
name: 'DeviceBlockedPage',
setup() {
const blockInfo = ref(null);
const appealSent = ref(false);
const appealLoading = ref(false);
const appealForm = reactive({
email: '',
message: ''
});
// Загрузка информации о блокировке
const loadBlockInfo = () => {
try {
// Получаем информацию о блокировке из deviceSecurityService
if (deviceSecurityService && typeof deviceSecurityService.getBlockInfo === 'function') {
const info = deviceSecurityService.getBlockInfo();
blockInfo.value = info;
console.log('[DeviceBlockedPage] Информация о блокировке:', info);
}
// Также проверяем localStorage
const storedBlockInfo = localStorage.getItem('deviceBlockInfo');
if (storedBlockInfo) {
try {
const parsedInfo = JSON.parse(storedBlockInfo);
if (!blockInfo.value) {
blockInfo.value = parsedInfo;
}
} catch (e) {
console.error('[DeviceBlockedPage] Ошибка парсинга данных блокировки:', e);
}
}
// Если нет информации, создаем базовую
if (!blockInfo.value) {
blockInfo.value = {
reason: 'Нарушение правил сервиса',
blockedAt: new Date().toISOString(),
deviceId: 'unknown'
};
}
} catch (error) {
console.error('[DeviceBlockedPage] Ошибка загрузки информации о блокировке:', error);
}
};
// Отправка обращения
const submitAppeal = async () => {
if (!appealForm.email || !appealForm.message) {
alert('Пожалуйста, заполните все поля');
return;
}
appealLoading.value = true;
try {
// Здесь можно отправить обращение на email или в систему
// Пока что симулируем отправку
console.log('[DeviceBlockedPage] Отправка обращения:', appealForm);
// Симуляция отправки
await new Promise(resolve => setTimeout(resolve, 2000));
// Сохраняем информацию о том, что обращение отправлено
localStorage.setItem('appealSent', JSON.stringify({
email: appealForm.email,
message: appealForm.message,
sentAt: new Date().toISOString(),
deviceId: blockInfo.value?.deviceId
}));
appealSent.value = true;
// Очищаем форму
appealForm.email = '';
appealForm.message = '';
} catch (error) {
console.error('[DeviceBlockedPage] Ошибка отправки обращения:', error);
alert('Ошибка при отправке обращения. Попробуйте позже.');
} finally {
appealLoading.value = false;
}
};
// Форматирование даты
const formatDate = (dateString) => {
if (!dateString) return 'Неизвестно';
try {
return new Date(dateString).toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (err) {
return 'Неверная дата';
}
};
// Проверка, было ли уже отправлено обращение
const checkAppealStatus = () => {
try {
const storedAppeal = localStorage.getItem('appealSent');
if (storedAppeal) {
const appealData = JSON.parse(storedAppeal);
// Проверяем, было ли обращение отправлено в последние 24 часа
const sentAt = new Date(appealData.sentAt);
const now = new Date();
const hoursDiff = (now - sentAt) / (1000 * 60 * 60);
if (hoursDiff < 24) {
appealSent.value = true;
appealForm.email = appealData.email;
}
}
} catch (error) {
console.error('[DeviceBlockedPage] Ошибка проверки статуса обращения:', error);
}
};
onMounted(() => {
loadBlockInfo();
checkAppealStatus();
// Отправляем событие аналитики
if (typeof gtag !== 'undefined') {
gtag('event', 'device_blocked_page_view', {
'event_category': 'security',
'event_label': 'device_blocked'
});
}
});
return {
blockInfo,
appealForm,
appealSent,
appealLoading,
submitAppeal,
formatDate
};
}
};
</script>
<style scoped>
.device-blocked-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.blocked-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 700px;
width: 100%;
overflow: hidden;
}
.blocked-icon {
text-align: center;
padding: 40px 20px 20px;
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
}
.blocked-content {
padding: 30px;
}
.blocked-content h1 {
text-align: center;
color: #333;
margin: 0 0 25px 0;
font-size: 28px;
font-weight: 700;
}
.block-info {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
border-left: 4px solid #dc3545;
}
.main-message {
font-size: 16px;
color: #555;
margin: 0 0 20px 0;
line-height: 1.6;
}
.block-details {
border-top: 1px solid #dee2e6;
padding-top: 15px;
}
.detail-item {
display: flex;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.detail-item strong {
color: #333;
min-width: 140px;
}
.detail-item code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.help-section {
margin-bottom: 30px;
}
.help-section h3 {
color: #333;
margin: 0 0 15px 0;
font-size: 20px;
}
.help-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.help-section li {
padding: 8px 0;
color: #555;
position: relative;
padding-left: 20px;
}
.help-section li::before {
content: '•';
color: #dc3545;
font-size: 18px;
position: absolute;
left: 0;
}
.contact-section {
background: #f8f9fa;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
}
.contact-section h3 {
color: #333;
margin: 0 0 15px 0;
font-size: 20px;
}
.contact-methods {
margin: 20px 0;
}
.contact-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
flex-wrap: wrap;
}
.contact-item strong {
color: #333;
min-width: 80px;
}
.contact-item a {
color: #007bff;
text-decoration: none;
}
.contact-item a:hover {
text-decoration: underline;
}
.appeal-form {
margin-top: 25px;
border-top: 1px solid #dee2e6;
padding-top: 20px;
}
.appeal-form h4 {
color: #333;
margin: 0 0 20px 0;
font-size: 18px;
}
.appeal-form-content {
max-width: 100%;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.appeal-success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
border: 1px solid #c3e6cb;
}
.additional-info {
margin-bottom: 30px;
}
.additional-info h3 {
color: #333;
margin: 0 0 20px 0;
font-size: 20px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.info-card {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
text-align: center;
}
.info-card h4 {
color: #333;
margin: 0 0 10px 0;
font-size: 16px;
}
.info-card p {
color: #666;
margin: 0 0 15px 0;
line-height: 1.5;
}
.link {
color: #007bff;
text-decoration: none;
font-weight: 500;
}
.link:hover {
text-decoration: underline;
}
.bypass-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: flex-start;
gap: 15px;
}
.warning-icon {
font-size: 24px;
flex-shrink: 0;
}
.warning-text {
color: #856404;
line-height: 1.5;
}
.warning-text strong {
color: #721c24;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
display: inline-block;
text-decoration: none;
}
.btn:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background: #007bff;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
/* Responsive design */
@media (max-width: 768px) {
.device-blocked-page {
padding: 10px;
}
.blocked-content {
padding: 20px;
}
.blocked-content h1 {
font-size: 24px;
}
.detail-item {
flex-direction: column;
gap: 5px;
}
.detail-item strong {
min-width: auto;
}
.contact-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.contact-item strong {
min-width: auto;
}
.info-grid {
grid-template-columns: 1fr;
}
.bypass-warning {
flex-direction: column;
gap: 10px;
}
}
@media (max-width: 480px) {
.blocked-container {
border-radius: 8px;
}
.blocked-icon {
padding: 30px 20px 15px;
}
.blocked-content {
padding: 15px;
}
.block-info,
.contact-section {
padding: 15px;
}
.form-group input,
.form-group textarea {
padding: 10px;
}
}
</style>

View File

@ -28,6 +28,18 @@
</div> </div>
</div> </div>
<!-- Добавляем показ информации о блокировке устройства -->
<div v-if="deviceBlocked" class="blocked-account-message">
<i class="bi-shield-exclamation"></i>
<div class="blocked-message-content">
<h3>Доступ с устройства заблокирован</h3>
<p>{{ deviceBlockedInfo.message }}</p>
<p v-if="deviceBlockedInfo.reason" class="block-reason">
<strong>Причина:</strong> {{ deviceBlockedInfo.reason }}
</p>
</div>
</div>
<form @submit.prevent="handleLogin" class="login-form"> <form @submit.prevent="handleLogin" class="login-form">
<div class="form-group"> <div class="form-group">
<label for="email"> <label for="email">
@ -95,27 +107,38 @@
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useAuth } from '@/auth'; import { useAuth } from '@/auth';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import deviceSecurityService from '@/services/deviceSecurity';
const route = useRoute(); const route = useRoute();
const email = ref(''); const email = ref('');
const password = ref(''); const password = ref('');
const errorMessage = ref(''); const errorMessage = ref('');
const loading = ref(false); const loading = ref(false);
const securityLoading = ref(true);
const blockedInfo = ref({ const blockedInfo = ref({
blocked: false, blocked: false,
message: 'Ваш аккаунт был заблокирован администратором.', message: 'Ваш аккаунт был заблокирован администратором.',
reason: 'Нарушение правил сервиса', reason: 'Нарушение правил сервиса',
timestamp: '' timestamp: ''
}); });
const deviceBlockedInfo = ref({
blocked: false,
message: 'Доступ с данного устройства заблокирован.',
reason: 'Подозрительная активность',
timestamp: ''
});
const { login } = useAuth(); const { login } = useAuth();
// Проверяем, был ли аккаунт заблокирован // Проверяем, был ли аккаунт или устройство заблокированы
const accountBlocked = computed(() => { const accountBlocked = computed(() => {
// Проверяем query параметр и информацию из localStorage
return route.query.blocked === 'true' || blockedInfo.value.blocked; return route.query.blocked === 'true' || blockedInfo.value.blocked;
}); });
const deviceBlocked = computed(() => {
return route.query.type === 'device' || deviceBlockedInfo.value.blocked || deviceSecurityService.isDeviceBlocked();
});
// Функция для блокировки прокрутки страницы // Функция для блокировки прокрутки страницы
const preventScroll = () => { const preventScroll = () => {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@ -132,18 +155,59 @@ const enableScroll = () => {
document.body.style.width = ''; document.body.style.width = '';
}; };
onMounted(() => { onMounted(async () => {
preventScroll(); // Блокируем прокрутку при монтировании компонента 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'); const storedBlockInfo = localStorage.getItem('accountBlockedInfo');
if (storedBlockInfo) { if (storedBlockInfo) {
try { try {
const parsedInfo = JSON.parse(storedBlockInfo); const parsedInfo = JSON.parse(storedBlockInfo);
blockedInfo.value = parsedInfo; blockedInfo.value = parsedInfo;
// Удаляем дублирующее сообщение об ошибке, так как эта информация уже
// отображается в блоке blocked-account-message выше кнопки "Войти"
} catch (e) { } catch (e) {
console.error('Ошибка при парсинге информации о блокировке:', e); console.error('Ошибка при парсинге информации о блокировке:', e);
} }
@ -151,7 +215,7 @@ onMounted(() => {
}); });
onUnmounted(() => { onUnmounted(() => {
enableScroll(); // Восстанавливаем прокрутку при размонтировании компонента enableScroll();
}); });
const handleLogin = async () => { const handleLogin = async () => {
@ -165,9 +229,96 @@ const handleLogin = async () => {
} }
try { 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) { } catch (error) {
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 || 'Произошла неизвестная ошибка при входе.'; errorMessage.value = error.message || 'Произошла неизвестная ошибка при входе.';
}
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -0,0 +1,682 @@
<template>
<div class="admin-blocked-devices">
<div class="page-header">
<h1>Заблокированные устройства</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{ blockedDevices.length }}</div>
<div class="stat-label">Всего заблокировано</div>
</div>
</div>
</div>
<div class="search-filters">
<div class="search-bar">
<input
v-model="searchQuery"
type="text"
placeholder="Поиск по device ID, причине или дате..."
@input="filterDevices"
>
</div>
<div class="filters">
<select v-model="statusFilter" @change="filterDevices">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="expired">Истекшие</option>
</select>
<select v-model="reasonFilter" @change="filterDevices">
<option value="">Все причины</option>
<option value="multiple_violations">Множественные нарушения</option>
<option value="spam">Спам</option>
<option value="harassment">Домогательства</option>
<option value="fake_profile">Фейковый профиль</option>
<option value="other">Другое</option>
</select>
</div>
</div>
<div class="devices-table" v-if="filteredDevices.length > 0">
<table>
<thead>
<tr>
<th>Device ID</th>
<th>Причина</th>
<th>Заблокировано</th>
<th>Истекает</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="device in paginatedDevices" :key="device._id">
<td>
<div class="device-id">
<code>{{ device.deviceId.substring(0, 16) }}...</code>
<button
@click="copyToClipboard(device.deviceId)"
class="copy-btn"
title="Копировать полный ID"
>
📋
</button>
</div>
</td>
<td>
<span class="reason-badge" :class="device.reason">
{{ getReasonText(device.reason) }}
</span>
</td>
<td>{{ formatDate(device.blockedAt) }}</td>
<td>
<span v-if="device.expiresAt" :class="{ 'expired': isExpired(device.expiresAt) }">
{{ formatDate(device.expiresAt) }}
</span>
<span v-else class="permanent">Навсегда</span>
</td>
<td>
<span class="status-badge" :class="getDeviceStatus(device)">
{{ getDeviceStatusText(device) }}
</span>
</td>
<td>
<div class="actions">
<button
@click="viewDeviceDetails(device)"
class="btn btn-sm btn-outline"
>
Детали
</button>
<button
@click="unblockDevice(device._id)"
class="btn btn-sm btn-danger"
:disabled="isExpired(device.expiresAt)"
>
Разблокировать
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Пагинация -->
<div class="pagination" v-if="totalPages > 1">
<button
@click="currentPage--"
:disabled="currentPage === 1"
class="btn btn-outline"
>
Назад
</button>
<span class="page-info">
Страница {{ currentPage }} из {{ totalPages }}
</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
class="btn btn-outline"
>
Далее
</button>
</div>
</div>
<div v-else class="no-data">
<div class="no-data-icon">🚫</div>
<h3>Заблокированных устройств не найдено</h3>
<p>{{ searchQuery ? 'Попробуйте изменить параметры поиска' : 'Пока нет заблокированных устройств' }}</p>
</div>
<!-- Модальное окно с деталями устройства -->
<div v-if="selectedDevice" class="modal-overlay" @click="closeModal">
<div class="modal" @click.stop>
<div class="modal-header">
<h3>Детали заблокированного устройства</h3>
<button @click="closeModal" class="close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="device-details">
<div class="detail-row">
<label>Device ID:</label>
<code>{{ selectedDevice.deviceId }}</code>
</div>
<div class="detail-row">
<label>Причина блокировки:</label>
<span class="reason-badge" :class="selectedDevice.reason">
{{ getReasonText(selectedDevice.reason) }}
</span>
</div>
<div class="detail-row" v-if="selectedDevice.description">
<label>Описание:</label>
<p>{{ selectedDevice.description }}</p>
</div>
<div class="detail-row">
<label>Дата блокировки:</label>
<span>{{ formatDate(selectedDevice.blockedAt) }}</span>
</div>
<div class="detail-row" v-if="selectedDevice.expiresAt">
<label>Дата истечения:</label>
<span :class="{ 'expired': isExpired(selectedDevice.expiresAt) }">
{{ formatDate(selectedDevice.expiresAt) }}
</span>
</div>
<div class="detail-row">
<label>Статус:</label>
<span class="status-badge" :class="getDeviceStatus(selectedDevice)">
{{ getDeviceStatusText(selectedDevice) }}
</span>
</div>
<div class="detail-row" v-if="selectedDevice.blockedBy">
<label>Заблокировал:</label>
<span>{{ selectedDevice.blockedBy.username || selectedDevice.blockedBy.email }}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="closeModal" class="btn btn-outline">Закрыть</button>
<button
@click="unblockDevice(selectedDevice._id)"
class="btn btn-danger"
:disabled="isExpired(selectedDevice.expiresAt)"
>
Разблокировать
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import api from '@/services/api'
export default {
name: 'AdminBlockedDevices',
data() {
return {
blockedDevices: [],
filteredDevices: [],
searchQuery: '',
statusFilter: '',
reasonFilter: '',
currentPage: 1,
itemsPerPage: 20,
selectedDevice: null,
loading: false
}
},
computed: {
paginatedDevices() {
const start = (this.currentPage - 1) * this.itemsPerPage
const end = start + this.itemsPerPage
return this.filteredDevices.slice(start, end)
},
totalPages() {
return Math.ceil(this.filteredDevices.length / this.itemsPerPage)
}
},
async mounted() {
await this.loadBlockedDevices()
},
methods: {
async loadBlockedDevices() {
try {
this.loading = true
const response = await api.getBlockedDevices()
this.blockedDevices = response.data.devices || []
this.filteredDevices = [...this.blockedDevices]
} catch (error) {
console.error('Ошибка загрузки заблокированных устройств:', error)
this.$toast.error('Ошибка загрузки данных')
} finally {
this.loading = false
}
},
filterDevices() {
let filtered = [...this.blockedDevices]
// Поиск по тексту
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase()
filtered = filtered.filter(device =>
device.deviceId.toLowerCase().includes(query) ||
this.getReasonText(device.reason).toLowerCase().includes(query) ||
(device.description && device.description.toLowerCase().includes(query))
)
}
// Фильтр по статусу
if (this.statusFilter) {
filtered = filtered.filter(device => {
const status = this.getDeviceStatus(device)
return status === this.statusFilter
})
}
// Фильтр по причине
if (this.reasonFilter) {
filtered = filtered.filter(device => device.reason === this.reasonFilter)
}
this.filteredDevices = filtered
this.currentPage = 1
},
getDeviceStatus(device) {
if (device.expiresAt && this.isExpired(device.expiresAt)) {
return 'expired'
}
return 'active'
},
getDeviceStatusText(device) {
const status = this.getDeviceStatus(device)
return {
active: 'Активная',
expired: 'Истекшая'
}[status]
},
getReasonText(reason) {
const reasons = {
multiple_violations: 'Множественные нарушения',
spam: 'Спам',
harassment: 'Домогательства',
fake_profile: 'Фейковый профиль',
other: 'Другое'
}
return reasons[reason] || reason
},
isExpired(expiresAt) {
if (!expiresAt) return false
return new Date(expiresAt) < new Date()
},
formatDate(dateString) {
return new Date(dateString).toLocaleString('ru-RU')
},
viewDeviceDetails(device) {
this.selectedDevice = device
},
closeModal() {
this.selectedDevice = null
},
async unblockDevice(deviceId) {
if (!confirm('Вы уверены, что хотите разблокировать это устройство?')) {
return
}
try {
await api.unblockDevice(deviceId)
this.$toast.success('Устройство разблокировано')
await this.loadBlockedDevices()
this.closeModal()
} catch (error) {
console.error('Ошибка разблокировки устройства:', error)
this.$toast.error('Ошибка разблокировки устройства')
}
},
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
this.$toast.success('ID скопирован в буфер обмена')
} catch (error) {
console.error('Ошибка копирования в буфер обмена:', error)
this.$toast.error('Ошибка копирования')
}
}
}
}
</script>
<style scoped>
.admin-blocked-devices {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.page-header h1 {
margin: 0;
color: #333;
}
.stats {
display: flex;
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
min-width: 120px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
.search-filters {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.search-bar {
margin-bottom: 15px;
}
.search-bar input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.filters {
display: flex;
gap: 15px;
}
.filters select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background: white;
min-width: 150px;
}
.devices-table {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
}
.device-id {
display: flex;
align-items: center;
gap: 10px;
}
.device-id code {
background: #f1f3f4;
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
}
.copy-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
}
.copy-btn:hover {
opacity: 1;
}
.reason-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.reason-badge.multiple_violations {
background: #fee2e2;
color: #dc2626;
}
.reason-badge.spam {
background: #fef3c7;
color: #d97706;
}
.reason-badge.harassment {
background: #fecaca;
color: #dc2626;
}
.reason-badge.fake_profile {
background: #e0e7ff;
color: #3730a3;
}
.reason-badge.other {
background: #f3f4f6;
color: #374151;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.status-badge.active {
background: #fecaca;
color: #dc2626;
}
.status-badge.expired {
background: #f3f4f6;
color: #6b7280;
}
.permanent {
color: #dc2626;
font-weight: 500;
}
.expired {
color: #6b7280;
text-decoration: line-through;
}
.actions {
display: flex;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.btn-outline {
background: white;
border: 1px solid #ddd;
color: #333;
}
.btn-outline:hover {
background: #f8f9fa;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
padding: 20px;
border-top: 1px solid #eee;
}
.page-info {
color: #666;
}
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
}
.no-data-icon {
font-size: 4em;
margin-bottom: 20px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 10px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.modal-body {
padding: 20px;
}
.device-details {
display: flex;
flex-direction: column;
gap: 15px;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-row label {
font-weight: 600;
color: #333;
}
.detail-row code {
background: #f1f3f4;
padding: 8px;
border-radius: 4px;
font-family: monospace;
word-break: break-all;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 20px;
border-top: 1px solid #eee;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.filters {
flex-direction: column;
}
.devices-table {
overflow-x: auto;
}
table {
min-width: 800px;
}
.actions {
flex-direction: column;
}
}
</style>