275 lines
7.2 KiB
JavaScript
275 lines
7.2 KiB
JavaScript
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); |