Reflex/backend/models/BlockedDevice.js

275 lines
7.2 KiB
JavaScript
Raw Normal View History

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