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