jagd-apps/nachsuche/backend/middleware/auditLogger.js

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