Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
147 lines
3.7 KiB
JavaScript
147 lines
3.7 KiB
JavaScript
// /components/activityLogs/activityLogService.js
|
|
'use strict';
|
|
|
|
const ActivityLog = require('./activityLogModel');
|
|
const { ACTIVITY_ACTIONS } = require('./activityLogModel');
|
|
const { parsePaginationAndSort, calculateMeta } = require('../../utils/pagination');
|
|
|
|
const SENSITIVE_KEYS = new Set([
|
|
'password',
|
|
'passwordHash',
|
|
'refreshToken',
|
|
'accessToken',
|
|
'token',
|
|
'secret',
|
|
'smtp_pass',
|
|
'SMTP_PASS'
|
|
]);
|
|
|
|
const sanitizeValue = (value, depth = 0) => {
|
|
if (value == null) return value;
|
|
if (depth > 3) return '[truncated]';
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.slice(0, 20).map((item) => sanitizeValue(item, depth + 1));
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
const cleaned = {};
|
|
Object.keys(value).slice(0, 30).forEach((key) => {
|
|
if (SENSITIVE_KEYS.has(key)) {
|
|
cleaned[key] = '[redacted]';
|
|
} else {
|
|
cleaned[key] = sanitizeValue(value[key], depth + 1);
|
|
}
|
|
});
|
|
return cleaned;
|
|
}
|
|
|
|
if (typeof value === 'string' && value.length > 300) {
|
|
return `${value.slice(0, 300)}…`;
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
const buildDescription = ({ action, resource, actorName, actorUsername, method, path }) => {
|
|
const ACTION_LABELS = {
|
|
create: 'ایجاد',
|
|
update: 'ویرایش',
|
|
delete: 'حذف',
|
|
login: 'ورود',
|
|
logout: 'خروج',
|
|
enroll: 'ثبتنام',
|
|
attendance: 'حضور و غیاب',
|
|
upload: 'آپلود',
|
|
retry: 'تلاش مجدد',
|
|
other: 'عملیات'
|
|
};
|
|
|
|
const RESOURCE_LABELS = {
|
|
auth: 'احراز هویت',
|
|
users: 'کاربران',
|
|
professors: 'اساتید',
|
|
courses: 'دورهها',
|
|
classes: 'کلاسها',
|
|
sessions: 'جلسات',
|
|
payments: 'پرداختها',
|
|
roles: 'نقشها',
|
|
notifications: 'اطلاعیهها',
|
|
certificates: 'گواهینامهها',
|
|
files: 'فایلها',
|
|
dashboard: 'داشبورد',
|
|
'contact-inquiries': 'درخواستهای تماس',
|
|
'activity-logs': 'گزارش فعالیتها'
|
|
};
|
|
|
|
const who = actorName || actorUsername || 'سیستم';
|
|
const actionLabel = ACTION_LABELS[action] || action || 'عملیات';
|
|
const resourceLabel = RESOURCE_LABELS[resource] || resource || 'منبع';
|
|
return `${who} — ${actionLabel} روی ${resourceLabel} (${method} ${path})`;
|
|
};
|
|
|
|
const createActivityLog = async (payload) => {
|
|
const doc = {
|
|
...payload,
|
|
metadata: sanitizeValue(payload.metadata || {}),
|
|
description:
|
|
payload.description ||
|
|
buildDescription(payload)
|
|
};
|
|
return ActivityLog.create(doc);
|
|
};
|
|
|
|
const getAllActivityLogs = async (queryParams = {}) => {
|
|
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
|
|
const filter = {};
|
|
|
|
if (queryParams.action) {
|
|
filter.action = queryParams.action;
|
|
}
|
|
|
|
if (queryParams.resource) {
|
|
filter.resource = queryParams.resource;
|
|
}
|
|
|
|
if (queryParams.actor) {
|
|
filter.actor = queryParams.actor;
|
|
}
|
|
|
|
if (queryParams.q) {
|
|
const searchRegex = new RegExp(queryParams.q, 'i');
|
|
filter.$or = [
|
|
{ description: searchRegex },
|
|
{ actorUsername: searchRegex },
|
|
{ actorName: searchRegex },
|
|
{ path: searchRegex },
|
|
{ resource: searchRegex },
|
|
{ resourceId: searchRegex }
|
|
];
|
|
}
|
|
|
|
const [logs, totalCount] = await Promise.all([
|
|
ActivityLog.find(filter)
|
|
.populate('actor', 'name surname username')
|
|
.sort(sort)
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.lean(),
|
|
ActivityLog.countDocuments(filter)
|
|
]);
|
|
|
|
return {
|
|
data: logs,
|
|
meta: calculateMeta(totalCount, page, limit)
|
|
};
|
|
};
|
|
|
|
const getActivityActions = () => ACTIVITY_ACTIONS;
|
|
|
|
module.exports = {
|
|
createActivityLog,
|
|
getAllActivityLogs,
|
|
getActivityActions,
|
|
sanitizeValue,
|
|
buildDescription
|
|
};
|