167 lines
5.7 KiB
JavaScript
167 lines
5.7 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)
|
||
*/
|
||
// `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 };
|