Files
gameno-api/components/activityLogs/activityLogService.js
T
kavehhn bf5e66fa95 feat: add append-only course/user data import and expand user profile
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.
2026-08-15 01:31:44 +03:30

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 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
};