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

167 lines
5.7 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 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)
*/
// `action` überschreibt die Vorbelegung z. B. 'LOGOUT' für die Abmeldung,
// die sonst fälschlich als LOGIN im Protokoll landen würde.
const auditAuth = async (req, isSuccess, username, errorMessage = null, action = null) => {
try {
await AuditLog.log({
action: 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 };