Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
105 lines
3.1 KiB
JavaScript
105 lines
3.1 KiB
JavaScript
// /middlewares/activityLogger.js
|
|
'use strict';
|
|
|
|
const activityLogService = require('../components/activityLogs/activityLogService');
|
|
const logger = require('../utils/logger');
|
|
|
|
const SKIP_PREFIXES = [
|
|
'/api/health',
|
|
'/api/activity-logs'
|
|
];
|
|
|
|
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
|
|
const resolveResource = (path = '') => {
|
|
const clean = path.split('?')[0].replace(/^\/api\//, '');
|
|
const [resource] = clean.split('/').filter(Boolean);
|
|
return resource || 'unknown';
|
|
};
|
|
|
|
const resolveResourceId = (req) => {
|
|
if (req.params?.id) return String(req.params.id);
|
|
if (req.params?.userId) return String(req.params.userId);
|
|
if (req.body?._id) return String(req.body._id);
|
|
if (req.body?.id) return String(req.body.id);
|
|
return null;
|
|
};
|
|
|
|
const resolveAction = (method, path = '') => {
|
|
const p = path.toLowerCase();
|
|
|
|
if (p.includes('/auth/login')) return 'login';
|
|
if (p.includes('/auth/logout')) return 'logout';
|
|
if (p.includes('/auth/refresh')) return 'other';
|
|
if (p.includes('enroll') || p.includes('register')) return 'enroll';
|
|
if (p.includes('attendance')) return 'attendance';
|
|
if (p.includes('upload')) return 'upload';
|
|
if (p.includes('retry')) return 'retry';
|
|
|
|
switch (method) {
|
|
case 'POST':
|
|
return 'create';
|
|
case 'PUT':
|
|
case 'PATCH':
|
|
return 'update';
|
|
case 'DELETE':
|
|
return 'delete';
|
|
default:
|
|
return 'other';
|
|
}
|
|
};
|
|
|
|
const shouldSkip = (req) => {
|
|
if (!MUTATING_METHODS.has(req.method)) return true;
|
|
|
|
const path = req.originalUrl || req.url || '';
|
|
if (SKIP_PREFIXES.some((prefix) => path.startsWith(prefix))) return true;
|
|
if (path.includes('/auth/refresh')) return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
const activityLogger = (req, res, next) => {
|
|
if (shouldSkip(req)) {
|
|
return next();
|
|
}
|
|
|
|
res.on('finish', () => {
|
|
// Only persist completed mutating requests (success or business failure)
|
|
if (res.statusCode < 200 || res.statusCode >= 600) return;
|
|
|
|
const path = (req.originalUrl || req.url || '').split('?')[0];
|
|
const action = resolveAction(req.method, path);
|
|
const actor = req.user || null;
|
|
|
|
const payload = {
|
|
actor: actor?._id || null,
|
|
actorUsername: actor?.username || req.body?.username || null,
|
|
actorName: actor
|
|
? `${actor.name || ''} ${actor.surname || ''}`.trim() || actor.username
|
|
: req.body?.username || null,
|
|
action,
|
|
resource: resolveResource(path),
|
|
resourceId: resolveResourceId(req),
|
|
method: req.method,
|
|
path,
|
|
statusCode: res.statusCode,
|
|
ip: req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || null,
|
|
userAgent: req.headers['user-agent'] || null,
|
|
metadata: {
|
|
params: activityLogService.sanitizeValue(req.params || {}),
|
|
query: activityLogService.sanitizeValue(req.query || {}),
|
|
body: activityLogService.sanitizeValue(req.body || {})
|
|
}
|
|
};
|
|
|
|
activityLogService.createActivityLog(payload).catch((err) => {
|
|
logger.error(`[ActivityLog] Failed to write log: ${err.message}`);
|
|
});
|
|
});
|
|
|
|
next();
|
|
};
|
|
|
|
module.exports = activityLogger;
|