Initial commit: teaching institution management API.

Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
2026-08-09 04:18:08 +02:00
commit f04c797be6
107 changed files with 9190 additions and 0 deletions
@@ -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
};