2026-03-20 17:15:14 +08:00
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
const LOG_DIR = path.join(__dirname, '../../logs');
|
|
|
|
|
const DANGEROUS_OPERATIONS_LOG = path.join(LOG_DIR, 'dangerous-operations.log');
|
|
|
|
|
|
|
|
|
|
const ensureLogDir = () => {
|
|
|
|
|
if (!fs.existsSync(LOG_DIR)) {
|
|
|
|
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const formatLogEntry = entry => {
|
2026-03-20 17:15:14 +08:00
|
|
|
const timestamp = new Date().toISOString();
|
2026-03-27 19:12:16 +08:00
|
|
|
return (
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
timestamp,
|
|
|
|
|
...entry,
|
|
|
|
|
}) + '\n'
|
|
|
|
|
);
|
2026-03-20 17:15:14 +08:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const logDangerousOperation = async (
|
|
|
|
|
req,
|
|
|
|
|
{
|
|
|
|
|
operationType,
|
|
|
|
|
operationName,
|
|
|
|
|
targetType,
|
|
|
|
|
targetId,
|
|
|
|
|
targetName,
|
|
|
|
|
beforeState,
|
|
|
|
|
metadata = {},
|
|
|
|
|
success = true,
|
|
|
|
|
errorMessage = null,
|
|
|
|
|
}
|
|
|
|
|
) => {
|
2026-03-20 17:15:14 +08:00
|
|
|
ensureLogDir();
|
|
|
|
|
|
|
|
|
|
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
|
|
|
|
|
const userAgent = req?.get?.('User-Agent') || 'unknown';
|
|
|
|
|
const userId = req?.user?.userId || req?.session?.userId || 'anonymous';
|
|
|
|
|
const username = req?.user?.username || req?.session?.username || 'anonymous';
|
|
|
|
|
|
|
|
|
|
const logEntry = {
|
|
|
|
|
operationType,
|
|
|
|
|
operationName,
|
|
|
|
|
targetType,
|
|
|
|
|
targetId,
|
|
|
|
|
targetName,
|
|
|
|
|
beforeState: beforeState ? JSON.stringify(beforeState) : null,
|
|
|
|
|
metadata,
|
|
|
|
|
success,
|
|
|
|
|
errorMessage,
|
|
|
|
|
clientIp,
|
|
|
|
|
userAgent,
|
|
|
|
|
userId,
|
|
|
|
|
username,
|
|
|
|
|
riskLevel: metadata.riskLevel || 'UNKNOWN',
|
|
|
|
|
relatedDataCount: metadata.relatedDataCount || 0,
|
|
|
|
|
itemCount: metadata.itemCount || 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
|
2026-03-27 19:12:16 +08:00
|
|
|
console.log(
|
|
|
|
|
`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`
|
|
|
|
|
);
|
2026-03-20 17:15:14 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to write dangerous operation log:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getDangerousOperationsLogs = (filters = {}) => {
|
|
|
|
|
ensureLogDir();
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(DANGEROUS_OPERATIONS_LOG)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
|
|
|
|
|
const lines = content.split('\n').filter(line => line.trim());
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
let logs = lines
|
|
|
|
|
.map(line => {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(line);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.filter(log => log !== null);
|
2026-03-20 17:15:14 +08:00
|
|
|
|
|
|
|
|
if (filters.operationType) {
|
|
|
|
|
logs = logs.filter(log => log.operationType === filters.operationType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.targetType) {
|
|
|
|
|
logs = logs.filter(log => log.targetType === filters.targetType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.success !== undefined) {
|
|
|
|
|
logs = logs.filter(log => log.success === filters.success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.startDate) {
|
|
|
|
|
logs = logs.filter(log => new Date(log.timestamp) >= new Date(filters.startDate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.endDate) {
|
|
|
|
|
logs = logs.filter(log => new Date(log.timestamp) <= new Date(filters.endDate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.username) {
|
2026-03-27 19:12:16 +08:00
|
|
|
logs = logs.filter(log =>
|
|
|
|
|
log.username?.toLowerCase().includes(filters.username.toLowerCase())
|
|
|
|
|
);
|
2026-03-20 17:15:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.riskLevel) {
|
|
|
|
|
logs = logs.filter(log => log.riskLevel === filters.riskLevel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return logs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to read dangerous operations log:', error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cleanOldLogs = (daysToKeep = 90) => {
|
|
|
|
|
ensureLogDir();
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(DANGEROUS_OPERATIONS_LOG)) {
|
|
|
|
|
return { deletedCount: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
|
|
|
|
|
const lines = content.split('\n').filter(line => line.trim());
|
|
|
|
|
|
|
|
|
|
const cutoffDate = new Date();
|
|
|
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
|
|
|
|
|
|
|
|
const remainingLogs = [];
|
|
|
|
|
let deletedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
try {
|
|
|
|
|
const logEntry = JSON.parse(line);
|
|
|
|
|
if (new Date(logEntry.timestamp) >= cutoffDate) {
|
|
|
|
|
remainingLogs.push(line);
|
|
|
|
|
} else {
|
|
|
|
|
deletedCount++;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
deletedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(DANGEROUS_OPERATIONS_LOG, remainingLogs.join('\n') + '\n');
|
|
|
|
|
|
|
|
|
|
return { deletedCount, remainingCount: remainingLogs.length };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to clean old logs:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DANGEROUS_OPERATION_TYPES = {
|
|
|
|
|
DELETE_SINGLE: 'DELETE_SINGLE',
|
|
|
|
|
DELETE_BATCH: 'DELETE_BATCH',
|
|
|
|
|
DELETE_ALL: 'DELETE_ALL',
|
|
|
|
|
UPDATE_BATCH: 'UPDATE_BATCH',
|
|
|
|
|
RESTORE: 'RESTORE',
|
|
|
|
|
PURGE: 'PURGE',
|
|
|
|
|
BATCH_RESTORE: 'BATCH_RESTORE',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const RISK_LEVELS = {
|
|
|
|
|
EXTREME: 'EXTREME',
|
|
|
|
|
HIGH: 'HIGH',
|
|
|
|
|
MEDIUM: 'MEDIUM',
|
|
|
|
|
LOW: 'LOW',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateRiskLevel = (operationType, itemCount, options = {}) => {
|
|
|
|
|
const { hasRelatedData = false, isSystemLevel = false } = options;
|
|
|
|
|
|
|
|
|
|
if (operationType === DANGEROUS_OPERATION_TYPES.DELETE_ALL || isSystemLevel) {
|
|
|
|
|
return RISK_LEVELS.EXTREME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (operationType === DANGEROUS_OPERATION_TYPES.DELETE_BATCH) {
|
|
|
|
|
if (itemCount > 10) {
|
|
|
|
|
return RISK_LEVELS.EXTREME;
|
|
|
|
|
}
|
|
|
|
|
return itemCount > 3 ? RISK_LEVELS.HIGH : RISK_LEVELS.MEDIUM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasRelatedData) {
|
|
|
|
|
return RISK_LEVELS.MEDIUM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RISK_LEVELS.LOW;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
logDangerousOperation,
|
|
|
|
|
getDangerousOperationsLogs,
|
|
|
|
|
cleanOldLogs,
|
|
|
|
|
DANGEROUS_OPERATION_TYPES,
|
|
|
|
|
RISK_LEVELS,
|
|
|
|
|
calculateRiskLevel,
|
|
|
|
|
};
|