Initial commit: teaching institution management API.
Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
// /components/activityLogs/activityLogController.js
|
||||
'use strict';
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const activityLogService = require('./activityLogService');
|
||||
const { successResponse, listResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.getAll = catchAsync(async (req, res) => {
|
||||
const { data, meta } = await activityLogService.getAllActivityLogs(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.getActions = catchAsync(async (req, res) => {
|
||||
const actions = activityLogService.getActivityActions();
|
||||
return successResponse(res, 200, 'Activity actions retrieved successfully', actions);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
// /components/activityLogs/activityLogModel.js
|
||||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const ACTIVITY_ACTIONS = [
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'login',
|
||||
'logout',
|
||||
'enroll',
|
||||
'attendance',
|
||||
'upload',
|
||||
'retry',
|
||||
'other'
|
||||
];
|
||||
|
||||
const activityLogSchema = new mongoose.Schema(
|
||||
{
|
||||
actor: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
default: null,
|
||||
index: true
|
||||
},
|
||||
actorUsername: {
|
||||
type: String,
|
||||
default: null,
|
||||
index: true
|
||||
},
|
||||
actorName: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
enum: ACTIVITY_ACTIONS,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
resource: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
resourceId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
statusCode: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ip: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
userAgent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
metadata: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
activityLogSchema.index({ createdAt: -1 });
|
||||
activityLogSchema.index({ action: 1, createdAt: -1 });
|
||||
|
||||
module.exports = mongoose.model('ActivityLog', activityLogSchema);
|
||||
module.exports.ACTIVITY_ACTIONS = ACTIVITY_ACTIONS;
|
||||
@@ -0,0 +1,26 @@
|
||||
// /components/activityLogs/activityLogRoutes.js
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const authMiddleware = require('../../middlewares/authMiddleware');
|
||||
const perm = require('../../middlewares/permissionMiddleware');
|
||||
const { PERMISSIONS } = require('../../constants/permissions');
|
||||
const activityLogController = require('./activityLogController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/admin/get-all',
|
||||
authMiddleware,
|
||||
perm.requires(PERMISSIONS.LOGS_READ),
|
||||
activityLogController.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/admin/actions',
|
||||
authMiddleware,
|
||||
perm.requires(PERMISSIONS.LOGS_READ),
|
||||
activityLogController.getActions
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,146 @@
|
||||
// /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
|
||||
};
|
||||
Reference in New Issue
Block a user