Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app-backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import dotenv from 'dotenv';
import connectDB from './src/config/connectDB.js';
import app from './src/app.js';
import { initSocket } from './src/socket.js';

dotenv.config();

Expand All @@ -10,14 +11,20 @@ const PORT = process.env.PORT || 3000;
const startServer = async () => {
try {
await connectDB();
app.listen(PORT, '0.0.0.0', () => {


const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(`📘 Swagger UI: http://localhost:${PORT}/api-docs`);
console.log(`🔌 WebSocket server ready for real-time notifications`);
});

initSocket(server);

} catch (err) {
console.error('❌ Failed to start server:', err.message);
process.exit(1);
}
};

startServer();
startServer();
1 change: 0 additions & 1 deletion app-backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import setupSwagger from './config/swagger.js'; // ✅ now using ES module impor
import { auditMiddleware } from "./middleware/logger.js";
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();

app.use(helmet());
Expand Down
144 changes: 116 additions & 28 deletions app-backend/src/controllers/notification.controller.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
import Notification from '../models/Notification.js';
import User from '../models/User.js';

/**
* GET /notifications
* User-specific notifications (paginated + filters)
* User-specific notifications (paginated + filters + priority sorting)
*/
export const getNotifications = async (req, res) => {
try {
const userId = req.user._id;

let { page = 1, limit = 20, type, isRead } = req.query;
let { page = 1, limit = 20, type, category, priority, isRead, sortBy = 'priority' } = req.query;

page = Math.max(1, parseInt(page));
limit = Math.min(100, Math.max(1, parseInt(limit)));

const filter = { userId };

if (type) filter.type = type;
if (category) filter.category = category;
if (priority) filter.priority = priority;
if (isRead !== undefined) filter.isRead = isRead === 'true';

// Sort logic: CRITICAL first, then HIGH, then date
let sortOptions = {};
if (sortBy === 'priority') {
sortOptions = { priority: -1, createdAt: -1 };
} else {
sortOptions = { createdAt: -1 };
}

const notifications = await Notification.find(filter)
.sort({ createdAt: -1 })
.sort(sortOptions)
.skip((page - 1) * limit)
.limit(limit);

const total = await Notification.countDocuments(filter);

// Get unread count by priority
const unreadByPriority = await Notification.getUnreadCountByPriority(userId);

res.json({
notifications,
total,
page,
limit,
success: true,
data: notifications,
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
stats: { unreadByPriority, totalUnread: notifications.filter(n => !n.isRead).length }
});
} catch (err) {
res.status(500).json({ message: err.message });
Expand All @@ -38,42 +52,70 @@ export const getNotifications = async (req, res) => {

/**
* POST /notifications
* Secure notification creation (restricted roles only)
* Enhanced notification creation with priority support
*/
export const createNotification = async (req, res) => {
try {
const { userId, type, title, message, data } = req.body;
const { userId, type, category, priority, title, message, data, metadata, expiresAt, broadcast, broadcastRoles } = req.body;

// Required field validation
if (!userId || !type || !message) {
if (!userId || !type || !category || !message) {
return res.status(400).json({
message: 'userId, type, and message are required',
message: 'userId, type, category, and message are required',
});
}

const allowedRoles = [
'super_admin',
'admin',
'branch_admin',
'employer',
];
const allowedRoles = ['super_admin', 'admin', 'branch_admin', 'employer'];

if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
message: 'You are not allowed to create notifications',
});
}

const notification = await Notification.create({
// Handle broadcast to multiple users
if (broadcast) {
let targetUsers = [];
if (broadcastRoles && broadcastRoles.length > 0) {
targetUsers = await User.find({ role: { $in: broadcastRoles } }).distinct('_id');
} else if (userId === 'all') {
targetUsers = await User.find().distinct('_id');
}

if (targetUsers.length > 0) {
const notifications = await Notification.broadcast({
type,
category,
priority: priority || 'MEDIUM',
title: title || '',
message,
data: data || {},
metadata: metadata || {},
expiresAt: expiresAt || null,
}, targetUsers);

return res.status(201).json({
success: true,
message: `Broadcast sent to ${targetUsers.length} users`,
count: notifications.length
});
}
}

// Single notification
const notification = await Notification.createNotification({
userId,
type,
category,
priority: priority || 'MEDIUM',
title: title || '',
message,
data: data || {},
createdBy: req.user._id,
metadata: metadata || {},
expiresAt: expiresAt || null,
});

res.status(201).json(notification);
res.status(201).json({ success: true, data: notification });
} catch (err) {
res.status(400).json({ message: err.message });
}
Expand All @@ -94,7 +136,7 @@ export const getNotificationById = async (req, res) => {
return res.status(404).json({ message: 'Notification not found' });
}

res.json(notification);
res.json({ success: true, data: notification });
} catch (err) {
res.status(500).json({ message: err.message });
}
Expand All @@ -111,15 +153,15 @@ export const markAsRead = async (req, res) => {
_id: req.params.id,
userId: req.user._id,
},
{ isRead: true },
{ isRead: true, readAt: new Date() },
{ new: true }
);

if (!notification) {
return res.status(404).json({ message: 'Notification not found' });
}

res.json(notification);
res.json({ success: true, data: notification });
} catch (err) {
res.status(500).json({ message: err.message });
}
Expand All @@ -131,12 +173,15 @@ export const markAsRead = async (req, res) => {
*/
export const markAllAsRead = async (req, res) => {
try {
await Notification.updateMany(
{ userId: req.user._id },
{ isRead: true }
const result = await Notification.updateMany(
{ userId: req.user._id, isRead: false },
{ isRead: true, readAt: new Date() }
);

res.json({ success: true });
res.json({
success: true,
message: `${result.modifiedCount} notifications marked as read`
});
} catch (err) {
res.status(500).json({ message: err.message });
}
Expand All @@ -152,8 +197,51 @@ export const getUnreadCount = async (req, res) => {
isRead: false,
});

res.json({ unreadCount: count });
const byPriority = await Notification.getUnreadCountByPriority(req.user._id);

res.json({
success: true,
unreadCount: count,
byPriority
});
} catch (err) {
res.status(500).json({ message: err.message });
}
};

/**
* DELETE /notifications/expired
* Clean up expired notifications
*/
export const deleteExpiredNotifications = async (req, res) => {
try {
const result = await Notification.deleteMany({
expiresAt: { $lt: new Date() }
});

res.json({
success: true,
message: `${result.deletedCount} expired notifications deleted`
});
} catch (err) {
res.status(500).json({ message: err.message });
}
};

/**
* GET /notifications/unread/high-priority
* Get unread HIGH and CRITICAL priority notifications
*/
export const getHighPriorityUnread = async (req, res) => {
try {
const notifications = await Notification.find({
userId: req.user._id,
isRead: false,
priority: { $in: ['HIGH', 'CRITICAL'] }
}).sort({ priority: -1, createdAt: -1 });

res.json({ success: true, data: notifications });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
Loading