Files
gameno-api/components/activityLogs/activityLogService.js
T
kavehhn 192c595c68 feat: add password change, class unenroll, and optional notify flags
Let admins skip SMS on user, class, and invoice actions, and let users change their own password with the current one.
2026-08-15 23:48:18 +03:30

150 lines
3.8 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',
'currentPassword',
'newPassword',
'confirmPassword',
'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 || queryParams.search) {
const searchRegex = new RegExp(String(queryParams.q || queryParams.search).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), '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 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
};