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,45 @@
|
||||
// /components/notifications/notificationController.js
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const notificationService = require('./notificationService');
|
||||
const { successResponse, listResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.create = catchAsync(async (req, res, next) => {
|
||||
const notification = await notificationService.createNotification(req.body);
|
||||
return successResponse(res, 201, 'Notification created successfully', notification);
|
||||
});
|
||||
|
||||
exports.getOne = catchAsync(async (req, res, next) => {
|
||||
const notification = await notificationService.getNotificationById(req.params.id);
|
||||
return successResponse(res, 200, 'Notification retrieved successfully', notification);
|
||||
});
|
||||
|
||||
exports.getAll = catchAsync(async (req, res, next) => {
|
||||
const { data, meta } = await notificationService.getAllNotifications(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.update = catchAsync(async (req, res, next) => {
|
||||
const notification = await notificationService.updateNotification(req.params.id, req.body);
|
||||
return successResponse(res, 200, 'Notification updated successfully', notification);
|
||||
});
|
||||
|
||||
exports.delete = catchAsync(async (req, res, next) => {
|
||||
await notificationService.deleteNotification(req.params.id);
|
||||
return successResponse(res, 200, 'Notification deleted successfully');
|
||||
});
|
||||
|
||||
exports.search = catchAsync(async (req, res, next) => {
|
||||
const { data, meta } = await notificationService.searchNotifications(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.retry = catchAsync(async (req, res, next) => {
|
||||
const result = await notificationService.retryNotification(req.params.id);
|
||||
return successResponse(res, 200, 'Notification retried successfully', result);
|
||||
});
|
||||
|
||||
exports.getMyNotifications = catchAsync(async (req, res, next) => {
|
||||
const { data, meta } = await notificationService.getMyNotifications(req.user._id, req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
// /components/notifications/notificationModel.js
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const notificationSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
channel: {
|
||||
type: String,
|
||||
enum: ['email', 'sms', 'baleBot'],
|
||||
required: true
|
||||
},
|
||||
subject: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'sent', 'delivered', 'failed', 'retrying'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
retryCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
maxRetries: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
lastError: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
sentAt: {
|
||||
type: Date
|
||||
},
|
||||
deliveredAt: {
|
||||
type: Date
|
||||
},
|
||||
relatedEvent: {
|
||||
type: String,
|
||||
trim: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Notification', notificationSchema);
|
||||
@@ -0,0 +1,26 @@
|
||||
// /components/notifications/notificationRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const notificationController = require('./notificationController');
|
||||
const { validateCreateNotification, validateUpdateNotification } = require('./notificationValidator');
|
||||
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-notifications', notificationController.getMyNotifications);
|
||||
|
||||
// Admin Scope
|
||||
router.post('/admin/create', perm.requires(PERMISSIONS.NOTIFICATIONS_CREATE), validateCreateNotification, notificationController.create);
|
||||
router.get('/admin/get-all', perm.requires(PERMISSIONS.NOTIFICATIONS_READ), notificationController.getAll);
|
||||
router.get('/admin/search', perm.requires(PERMISSIONS.NOTIFICATIONS_SEARCH), notificationController.search);
|
||||
router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.NOTIFICATIONS_READ), notificationController.getOne);
|
||||
router.put('/admin/update/:id', perm.requires(PERMISSIONS.NOTIFICATIONS_UPDATE), validateUpdateNotification, notificationController.update);
|
||||
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.NOTIFICATIONS_DELETE), notificationController.delete);
|
||||
router.post('/admin/:id/retry', perm.requires(PERMISSIONS.NOTIFICATIONS_RETRY), notificationController.retry);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,138 @@
|
||||
// /components/notifications/notificationService.js
|
||||
|
||||
const Notification = require('./notificationModel');
|
||||
const User = require('../users/userModel');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const { sendEmail } = require('../../utils/senders/emailSender');
|
||||
const { sendSMS } = require('../../utils/senders/smsSender');
|
||||
const { sendBaleMessage } = require('../../utils/senders/baleBotSender');
|
||||
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
|
||||
|
||||
const createNotification = async (data) => {
|
||||
const user = await User.findById(data.user);
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
|
||||
const notification = await Notification.create(data);
|
||||
|
||||
// Attempt sending immediately
|
||||
try {
|
||||
if (data.channel === 'email' && user.email) {
|
||||
await sendEmail({ to: user.email, subject: data.subject, body: data.body });
|
||||
} else if (data.channel === 'baleBot') {
|
||||
await sendBaleMessage({ chatId: user.phoneNumber, body: data.body });
|
||||
} else {
|
||||
await sendSMS({ phoneNumber: user.phoneNumber, body: data.body });
|
||||
}
|
||||
notification.status = 'sent';
|
||||
notification.sentAt = new Date();
|
||||
await notification.save();
|
||||
} catch (err) {
|
||||
notification.status = 'failed';
|
||||
notification.lastError = err.message;
|
||||
notification.retryCount = 1;
|
||||
await notification.save();
|
||||
}
|
||||
|
||||
return notification;
|
||||
};
|
||||
|
||||
const getNotificationById = async (id) => {
|
||||
const notification = await Notification.findById(id).populate('user', 'name surname username email phoneNumber');
|
||||
if (!notification) {
|
||||
throw new AppError('NOTIFICATION_NOT_FOUND');
|
||||
}
|
||||
return notification;
|
||||
};
|
||||
|
||||
const getAllNotifications = async (queryParams) => {
|
||||
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
|
||||
const filter = buildFilterQuery(queryParams, ['subject', 'body']);
|
||||
|
||||
const [notifications, totalCount] = await Promise.all([
|
||||
Notification.find(filter).populate('user', 'name surname username').sort(sort).skip(skip).limit(limit),
|
||||
Notification.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const meta = calculateMeta(totalCount, page, limit);
|
||||
return { data: notifications, meta };
|
||||
};
|
||||
|
||||
const updateNotification = async (id, updateData) => {
|
||||
const notification = await Notification.findById(id);
|
||||
if (!notification) {
|
||||
throw new AppError('NOTIFICATION_NOT_FOUND');
|
||||
}
|
||||
Object.assign(notification, updateData);
|
||||
await notification.save();
|
||||
return notification;
|
||||
};
|
||||
|
||||
const deleteNotification = async (id) => {
|
||||
const notification = await Notification.findById(id);
|
||||
if (!notification) {
|
||||
throw new AppError('NOTIFICATION_NOT_FOUND');
|
||||
}
|
||||
await Notification.findByIdAndDelete(id);
|
||||
return null;
|
||||
};
|
||||
|
||||
const searchNotifications = async (queryParams) => {
|
||||
return getAllNotifications(queryParams);
|
||||
};
|
||||
|
||||
const retryNotification = async (id) => {
|
||||
const notification = await Notification.findById(id).populate('user');
|
||||
if (!notification) {
|
||||
throw new AppError('NOTIFICATION_NOT_FOUND');
|
||||
}
|
||||
|
||||
const user = notification.user;
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
|
||||
notification.status = 'retrying';
|
||||
notification.retryCount += 1;
|
||||
await notification.save();
|
||||
|
||||
try {
|
||||
if (notification.channel === 'email' && user.email) {
|
||||
await sendEmail({ to: user.email, subject: notification.subject, body: notification.body });
|
||||
} else if (notification.channel === 'baleBot') {
|
||||
await sendBaleMessage({ chatId: user.phoneNumber, body: notification.body });
|
||||
} else {
|
||||
await sendSMS({ phoneNumber: user.phoneNumber, body: notification.body });
|
||||
}
|
||||
notification.status = 'sent';
|
||||
notification.sentAt = new Date();
|
||||
await notification.save();
|
||||
return { success: true, notification };
|
||||
} catch (err) {
|
||||
notification.status = 'failed';
|
||||
notification.lastError = err.message;
|
||||
await notification.save();
|
||||
throw new Error(`Retry attempt ${notification.retryCount} failed: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getMyNotifications = async (userId, queryParams) => {
|
||||
const filter = { user: userId, ...buildFilterQuery(queryParams) };
|
||||
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
|
||||
|
||||
const [notifications, totalCount] = await Promise.all([
|
||||
Notification.find(filter).sort(sort).skip(skip).limit(limit),
|
||||
Notification.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const meta = calculateMeta(totalCount, page, limit);
|
||||
return { data: notifications, meta };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createNotification,
|
||||
getNotificationById,
|
||||
getAllNotifications,
|
||||
updateNotification,
|
||||
deleteNotification,
|
||||
searchNotifications,
|
||||
retryNotification,
|
||||
getMyNotifications
|
||||
};
|
||||
@@ -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