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,45 @@
// /components/certificates/certificateController.js
const catchAsync = require('../../utils/catchAsync');
const certificateService = require('./certificateService');
const { successResponse, listResponse } = require('../../utils/apiResponse');
exports.create = catchAsync(async (req, res, next) => {
const certificate = await certificateService.createCertificate(req.body);
return successResponse(res, 201, 'Certificate created successfully', certificate);
});
exports.getOne = catchAsync(async (req, res, next) => {
const certificate = await certificateService.getCertificateById(req.params.id);
return successResponse(res, 200, 'Certificate retrieved successfully', certificate);
});
exports.getAll = catchAsync(async (req, res, next) => {
const { data, meta } = await certificateService.getAllCertificates(req.query);
return listResponse(res, 200, data, meta);
});
exports.update = catchAsync(async (req, res, next) => {
const certificate = await certificateService.updateCertificate(req.params.id, req.body);
return successResponse(res, 200, 'Certificate updated successfully', certificate);
});
exports.delete = catchAsync(async (req, res, next) => {
await certificateService.deleteCertificate(req.params.id);
return successResponse(res, 200, 'Certificate deleted successfully');
});
exports.search = catchAsync(async (req, res, next) => {
const { data, meta } = await certificateService.searchCertificates(req.query);
return listResponse(res, 200, data, meta);
});
exports.upload = catchAsync(async (req, res, next) => {
const certificate = await certificateService.uploadUserCertificate(req.user._id, req.body);
return successResponse(res, 201, 'Certificate uploaded and committed successfully', certificate);
});
exports.getMyCertificates = catchAsync(async (req, res, next) => {
const { data, meta } = await certificateService.getMyCertificates(req.user._id, req.query);
return listResponse(res, 200, data, meta);
});
@@ -0,0 +1,47 @@
// /components/certificates/certificateModel.js
const mongoose = require('mongoose');
const certificateSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
course: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Course',
index: true
},
title: {
type: String,
required: true,
trim: true
},
issuer: {
type: String,
trim: true
},
fileKey: {
type: String,
required: true,
trim: true
},
originalName: {
type: String,
trim: true
},
issuedAt: {
type: Date,
default: Date.now
},
isOfficial: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
module.exports = mongoose.model('Certificate', certificateSchema);
@@ -0,0 +1,30 @@
// /components/certificates/certificateRoutes.js
const express = require('express');
const certificateController = require('./certificateController');
const {
validateCreateCertificate,
validateUpdateCertificate,
validateUploadCertificate
} = require('./certificateValidator');
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-certificates', perm.requires(PERMISSIONS.CERTIFICATES_READ), certificateController.getMyCertificates);
router.post('/user/upload', perm.requires(PERMISSIONS.FILES_UPLOAD), validateUploadCertificate, certificateController.upload);
// Admin Scope
router.post('/admin/create', perm.requires(PERMISSIONS.CERTIFICATES_CREATE), validateCreateCertificate, certificateController.create);
router.get('/admin/get-all', perm.requires(PERMISSIONS.CERTIFICATES_READ), certificateController.getAll);
router.get('/admin/search', perm.requires(PERMISSIONS.CERTIFICATES_SEARCH), certificateController.search);
router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.CERTIFICATES_READ), certificateController.getOne);
router.put('/admin/update/:id', perm.requires(PERMISSIONS.CERTIFICATES_UPDATE), validateUpdateCertificate, certificateController.update);
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.CERTIFICATES_DELETE), certificateController.delete);
module.exports = router;
@@ -0,0 +1,181 @@
// /components/certificates/certificateService.js
const path = require('path');
const Certificate = require('./certificateModel');
const User = require('../users/userModel');
const AppError = require('../../utils/AppError');
const { commitTempFile, generatePresignedUrl, deleteFromBucket } = require('../../utils/s3Client');
const eventEmitter = require('../../events/eventEmitter');
const EVENT_NAMES = require('../../constants/eventNames');
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
const createCertificate = async (data) => {
const user = await User.findById(data.user);
if (!user) throw new AppError('USER_NOT_FOUND');
// Move file from Temp bucket to Permanent Storage bucket
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const certificate = await Certificate.create({
user: data.user,
course: data.course || null,
title: data.title,
issuer: data.issuer || '',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
isOfficial: data.isOfficial || false
});
user.certificates.push(certificate._id);
await user.save();
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_ISSUED, { certificateId: certificate._id, userId: user._id });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getCertificateById = async (id) => {
const certificate = await Certificate.findById(id)
.populate('user', 'name surname username nationalIdCode')
.populate('course', 'title type');
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getAllCertificates = async (queryParams) => {
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const filter = buildFilterQuery(queryParams, ['title', 'issuer']);
const [certificates, totalCount] = await Promise.all([
Certificate.find(filter)
.populate('user', 'name surname username')
.populate('course', 'title')
.sort(sort)
.skip(skip)
.limit(limit),
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
};
const updateCertificate = async (id, updateData) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
if (updateData.tempFileName) {
const targetKey = `certificates/cert-${Date.now()}-${path.basename(updateData.tempFileName)}`;
const { fileKey } = await commitTempFile(updateData.tempFileName, targetKey);
// Delete old file from storage bucket
await deleteFromBucket(certificate.fileKey);
certificate.fileKey = fileKey;
}
if (updateData.title) certificate.title = updateData.title;
if (updateData.issuer !== undefined) certificate.issuer = updateData.issuer;
if (updateData.isOfficial !== undefined) certificate.isOfficial = updateData.isOfficial;
await certificate.save();
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const deleteCertificate = async (id) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
await deleteFromBucket(certificate.fileKey);
await User.findByIdAndUpdate(certificate.user, { $pull: { certificates: certificate._id } });
await Certificate.findByIdAndDelete(id);
return null;
};
const searchCertificates = async (queryParams) => {
return getAllCertificates(queryParams);
};
const uploadUserCertificate = async (userId, data) => {
const user = await User.findById(userId);
if (!user) throw new AppError('USER_NOT_FOUND');
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const certificate = await Certificate.create({
user: userId,
course: data.course || null,
title: data.title,
issuer: data.issuer || 'Self-Uploaded',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
isOfficial: false
});
user.certificates.push(certificate._id);
await user.save();
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_UPLOADED, { certificateId: certificate._id, userId });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getMyCertificates = async (userId, queryParams) => {
const filter = { user: userId, ...buildFilterQuery(queryParams) };
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const [certificates, totalCount] = await Promise.all([
Certificate.find(filter)
.populate('course', 'title type')
.sort(sort)
.skip(skip)
.limit(limit),
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
};
module.exports = {
createCertificate,
getCertificateById,
getAllCertificates,
updateCertificate,
deleteCertificate,
searchCertificates,
uploadUserCertificate,
getMyCertificates
};
@@ -0,0 +1,6 @@
// Stub validator — pass-through middleware (no validation yet)
const passThrough = (req, res, next) => next();
module.exports = new Proxy({}, {
get: () => passThrough
});