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