165 lines
5.5 KiB
JavaScript
165 lines
5.5 KiB
JavaScript
const AuditLog = require('../models/AuditLog');
|
|
|
|
// Map resource name → Mongoose model (lazy-loaded to avoid circular deps)
|
|
const getModel = (resource) => {
|
|
const map = {
|
|
User: () => require('../models/User'),
|
|
Config: () => require('../models/Config'),
|
|
Admin: () => require('../models/Admin')
|
|
};
|
|
return map[resource] ? map[resource]() : null;
|
|
};
|
|
|
|
// Remove internal Mongoose / sensitive fields for clean snapshots
|
|
const stripInternals = (obj) => {
|
|
if (!obj) return null;
|
|
const { __v, _id, createdAt, updatedAt, deleted, deletedAt, deletedBy,
|
|
password, passwordHash, ...clean } = obj;
|
|
return clean;
|
|
};
|
|
|
|
// Compute field-level diff between two plain objects
|
|
const computeDiff = (before, after) => {
|
|
if (!before || !after) return null;
|
|
const diff = {};
|
|
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
for (const key of allKeys) {
|
|
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
|
diff[key] = { before: before[key], after: after[key] };
|
|
}
|
|
}
|
|
return Object.keys(diff).length > 0 ? diff : null;
|
|
};
|
|
|
|
/**
|
|
* Middleware to automatically log admin actions with full before/after state.
|
|
* Usage: router.post('/users', authenticateToken, auditLog('CREATE', 'User'), createUser);
|
|
*/
|
|
const auditLog = (action, resource) => {
|
|
return async (req, res, next) => {
|
|
const startTime = Date.now();
|
|
|
|
// Capture before-state for UPDATE actions
|
|
let beforeState = null;
|
|
if (action === 'UPDATE' && req.params?.id) {
|
|
try {
|
|
const Model = getModel(resource);
|
|
if (Model) {
|
|
const doc = await Model.findById(req.params.id).lean();
|
|
beforeState = stripInternals(doc);
|
|
}
|
|
} catch (_) { /* non-fatal */ }
|
|
}
|
|
|
|
const originalJson = res.json.bind(res);
|
|
|
|
res.json = function(data) {
|
|
const duration = Date.now() - startTime;
|
|
const statusCode = res.statusCode;
|
|
|
|
setImmediate(async () => {
|
|
try {
|
|
const isSuccess = data.success !== false && statusCode < 400;
|
|
|
|
const logData = {
|
|
action,
|
|
resource,
|
|
adminId: req.user?.id || null,
|
|
adminUsername: req.user?.username || 'unknown',
|
|
ipAddress: req.ip || req.connection?.remoteAddress,
|
|
userAgent: req.get('user-agent'),
|
|
requestMethod: req.method,
|
|
requestPath: req.originalUrl?.split('?')[0],
|
|
statusCode,
|
|
duration,
|
|
success: isSuccess
|
|
};
|
|
|
|
// Resource ID
|
|
if (data.data?._id) logData.resourceId = data.data._id;
|
|
else if (req.params?.id) logData.resourceId = req.params.id;
|
|
|
|
// Resource name
|
|
if (data.data?.name) logData.resourceName = data.data.name;
|
|
else if (data.data?.username) logData.resourceName = data.data.username;
|
|
|
|
// Before / after diff for UPDATE
|
|
if (action === 'UPDATE') {
|
|
const raw = data.data ? (data.data.toObject ? data.data.toObject() : data.data) : null;
|
|
const afterState = stripInternals(raw);
|
|
logData.before = beforeState;
|
|
logData.after = afterState;
|
|
logData.changes = computeDiff(beforeState, afterState);
|
|
}
|
|
|
|
// IMPORT: store import statistics in metadata
|
|
if (action === 'IMPORT' && data.results) {
|
|
logData.metadata = {
|
|
imported: data.results.imported,
|
|
skipped: data.results.skipped,
|
|
errors: data.results.errors?.length || 0
|
|
};
|
|
logData.resourceName = `Import (${data.results.imported} importiert, ${data.results.skipped} übersprungen)`;
|
|
}
|
|
|
|
// EXPORT: store format
|
|
if (action === 'EXPORT') {
|
|
const fmt = req.query.format?.toUpperCase() || 'JSON';
|
|
logData.metadata = { format: fmt };
|
|
logData.resourceName = `Export (${fmt})`;
|
|
}
|
|
|
|
// BULK operations: store count + affected fields
|
|
if (action === 'BULK_UPDATE' || action === 'BULK_DELETE') {
|
|
const count = Array.isArray(req.body?.ids) ? req.body.ids.length : (data.data?.matched || 0);
|
|
logData.metadata = {
|
|
count,
|
|
...(req.body?.updates ? { fields: Object.keys(req.body.updates) } : {})
|
|
};
|
|
logData.resourceName = `${action === 'BULK_DELETE' ? 'Massen-Löschung' : 'Massen-Update'} (${count} Einträge)`;
|
|
}
|
|
|
|
// Error message
|
|
if (!isSuccess && data.message) {
|
|
logData.errorMessage = data.message;
|
|
}
|
|
|
|
await AuditLog.log(logData);
|
|
} catch (error) {
|
|
const logger = require('../utils/logger');
|
|
logger.error('Audit logging error:', error);
|
|
}
|
|
});
|
|
|
|
return originalJson(data);
|
|
};
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Log authentication attempts (success and failure)
|
|
*/
|
|
const auditAuth = async (req, isSuccess, username, errorMessage = null) => {
|
|
try {
|
|
await AuditLog.log({
|
|
action: isSuccess ? 'LOGIN' : 'LOGIN_FAILED',
|
|
resource: 'Admin',
|
|
adminUsername: username,
|
|
ipAddress: req.ip || req.connection?.remoteAddress,
|
|
userAgent: req.get('user-agent'),
|
|
requestMethod: req.method,
|
|
requestPath: req.originalUrl?.split('?')[0],
|
|
statusCode: isSuccess ? 200 : 401,
|
|
success: isSuccess,
|
|
errorMessage
|
|
});
|
|
} catch (error) {
|
|
const logger = require('../utils/logger');
|
|
logger.error('Auth audit logging error:', error);
|
|
}
|
|
};
|
|
|
|
module.exports = { auditLog, auditAuth };
|