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,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);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,6 @@
|
||||
// Stub validator — pass-through middleware (no validation yet)
|
||||
const passThrough = (req, res, next) => next();
|
||||
|
||||
module.exports = new Proxy({}, {
|
||||
get: () => passThrough
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
// Stub validator — pass-through middleware (no validation yet)
|
||||
const passThrough = (req, res, next) => next();
|
||||
|
||||
module.exports = new Proxy({}, {
|
||||
get: () => passThrough
|
||||
});
|
||||
Reference in New Issue
Block a user