Support uploading structured JSON to create courses, classes, and students without wiping existing data, and merge user name fields while adding gender and registration details from source sheets.
105 lines
3.0 KiB
JavaScript
105 lines
3.0 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 || ''}`.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;
|