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
+41
View File
@@ -0,0 +1,41 @@
// /components/classes/classController.js
'use strict';
const catchAsync = require('../../utils/catchAsync');
const classService = require('./classService');
const { successResponse, listResponse } = require('../../utils/apiResponse');
exports.getAll = catchAsync(async (req, res) => {
const { data, meta } = await classService.getAll(req.query);
return listResponse(res, 200, data, meta);
});
exports.getOne = catchAsync(async (req, res) => {
const cls = await classService.getOne(req.params.id);
return successResponse(res, 200, 'Class retrieved successfully', cls);
});
exports.create = catchAsync(async (req, res) => {
const cls = await classService.create(req.body);
return successResponse(res, 201, 'Class created successfully', cls);
});
exports.update = catchAsync(async (req, res) => {
const cls = await classService.update(req.params.id, req.body);
return successResponse(res, 200, 'Class updated successfully', cls);
});
exports.delete = catchAsync(async (req, res) => {
await classService.remove(req.params.id);
return successResponse(res, 200, 'Class deleted successfully');
});
exports.registerUsers = catchAsync(async (req, res) => {
const cls = await classService.registerUsers(req.params.id, req.body.userIds || []);
return successResponse(res, 200, 'Users registered in class successfully', cls);
});
exports.getMyClasses = catchAsync(async (req, res) => {
const { data, meta } = await classService.getMyClasses(req.user._id, req.query);
return listResponse(res, 200, data, meta);
});
+47
View File
@@ -0,0 +1,47 @@
// /components/classes/classModel.js
'use strict';
const mongoose = require('mongoose');
const classSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
course: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Course',
required: true
},
professor: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Professor'
},
students: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
capacity: {
type: Number,
default: 30
},
tuitionFee: {
type: Number,
default: 0
},
startDate: {
type: Date
},
endDate: {
type: Date
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Class', classSchema);
+25
View File
@@ -0,0 +1,25 @@
// /components/classes/classRoutes.js
'use strict';
const express = require('express');
const classController = require('./classController');
const authMiddleware = require('../../middlewares/authMiddleware');
const perm = require('../../middlewares/permissionMiddleware');
const { PERMISSIONS } = require('../../constants/permissions');
const router = express.Router();
router.use(authMiddleware);
// User Scope
router.get('/user/my-classes', perm.requires(PERMISSIONS.CLASSES_READ), classController.getMyClasses);
// Admin Scope
router.get('/admin/get-all', perm.requires(PERMISSIONS.CLASSES_READ), classController.getAll);
router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.CLASSES_READ), classController.getOne);
router.post('/admin/create', perm.requires(PERMISSIONS.CLASSES_CREATE), classController.create);
router.put('/admin/update/:id', perm.requires(PERMISSIONS.CLASSES_UPDATE), classController.update);
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.delete);
router.post('/admin/:id/register-users', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.registerUsers);
module.exports = router;
+108
View File
@@ -0,0 +1,108 @@
// /components/classes/classService.js
'use strict';
const Class = require('./classModel');
const User = require('../users/userModel');
const AppError = require('../../utils/AppError');
const { calculateMeta } = require('../../utils/pagination');
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
const logger = require('../../utils/logger');
const getAll = async (query) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200);
const skip = (page - 1) * limit;
const filter = {};
if (query.courseId) filter.course = query.courseId;
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
const [items, total] = await Promise.all([
Class.find(filter)
.populate({ path: 'course', select: 'title type price' })
.populate({ path: 'professor', select: 'name surname' })
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
return { data: items, meta: calculateMeta(total, page, limit) };
};
const getOne = async (id) => {
const cls = await Class.findById(id)
.populate({ path: 'course', select: 'title type price' })
.populate({ path: 'professor', select: 'name surname phoneNumber' })
.populate({ path: 'students', select: 'name surname phoneNumber' })
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND');
return cls;
};
const create = async (body) => {
const cls = await Class.create(body);
return getOne(cls._id);
};
const update = async (id, body) => {
const cls = await Class.findByIdAndUpdate(id, body, { new: true, runValidators: true })
.populate({ path: 'course', select: 'title' })
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND');
return cls;
};
const remove = async (id) => {
const cls = await Class.findByIdAndDelete(id);
if (!cls) throw new AppError('CLASS_NOT_FOUND');
};
const registerUsers = async (classId, userIds) => {
const cls = await Class.findById(classId).populate({ path: 'course', select: 'title' });
if (!cls) throw new AppError('CLASS_NOT_FOUND');
const toAdd = (userIds || []).filter(
(id) => !cls.students.map((s) => s.toString()).includes(id.toString())
);
if (toAdd.length === 0) {
return getOne(classId);
}
cls.students.push(...toAdd);
await cls.save();
const classLabel = cls.name || cls.course?.title || 'کلاس';
const users = await User.find({ _id: { $in: toAdd } }).select('phoneNumber').lean();
await Promise.all(
users.map(async (user) => {
if (!user.phoneNumber) return;
try {
await sendClassRegisteredSms(user.phoneNumber, classLabel, user._id);
} catch (err) {
logger.error(`[registerUsers] SMS failed for ${user.phoneNumber}: ${err.message}`);
}
})
);
return getOne(classId);
};
const getMyClasses = async (userId, query = {}) => {
const page = parseInt(query.page) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200);
const skip = (page - 1) * limit;
const filter = { students: userId };
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
const [items, total] = await Promise.all([
Class.find(filter)
.populate({ path: 'course', select: 'title type price description' })
.populate({ path: 'professor', select: 'name surname' })
.skip(skip).limit(limit).sort({ startDate: -1, createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
return { data: items, meta: calculateMeta(total, page, limit) };
};
module.exports = { getAll, getOne, create, update, remove, registerUsers, getMyClasses };
+6
View File
@@ -0,0 +1,6 @@
// Stub validator — pass-through middleware (no validation yet)
const passThrough = (req, res, next) => next();
module.exports = new Proxy({}, {
get: () => passThrough
});
+6
View File
@@ -0,0 +1,6 @@
// Stub validator — pass-through middleware (no validation yet)
const passThrough = (req, res, next) => next();
module.exports = new Proxy({}, {
get: () => passThrough
});