Reflex/backend/models/BlockedDevice.js

275 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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