1- import { Request , Response } from "express" ;
1+ import { Response } from "express" ;
2+ import { AuthenticatedRequest } from "../middleware/auth" ;
23import { notificationService } from "../services/notificationService" ;
4+ import { NotificationType } from "../models/Notification" ;
35import logger from "../utils/logger" ;
46
57export class NotificationController {
6- public async getNotifications ( req : Request , res : Response ) : Promise < void > {
8+ public async getNotifications ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
79 try {
8- const { userId } = req . params ;
9- const { category, isRead, priority, limit, skip } = req . query ;
10+ const userId = req . user . id ;
11+ // Cast query params to plain strings to prevent NoSQL operator injection
12+ const category = typeof req . query . category === 'string' ? req . query . category : undefined ;
13+ const type = typeof req . query . type === 'string' ? req . query . type : undefined ;
14+ const isRead = req . query . isRead ;
15+ const priority = typeof req . query . priority === 'string' ? req . query . priority : undefined ;
16+ const limit = req . query . limit ? parseInt ( req . query . limit as string ) : 20 ;
17+ const skip = req . query . skip ? parseInt ( req . query . skip as string ) : 0 ;
1018
1119 const result = await notificationService . getNotifications ( {
1220 userId,
1321 category : category as any ,
22+ type : type as any ,
1423 isRead :
1524 isRead === "true" ? true : isRead === "false" ? false : undefined ,
1625 priority : priority as any ,
17- limit : limit ? parseInt ( limit as string ) : 20 ,
18- skip : skip ? parseInt ( skip as string ) : 0 ,
26+ limit : isNaN ( limit ) ? 20 : limit ,
27+ skip : isNaN ( skip ) ? 0 : skip ,
1928 } ) ;
2029
2130 res . status ( 200 ) . json ( {
@@ -30,10 +39,10 @@ export class NotificationController {
3039 }
3140 }
3241
33- public async markAsRead ( req : Request , res : Response ) : Promise < void > {
42+ public async markAsRead ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
3443 try {
3544 const { notificationId } = req . params ;
36- const { userId } = req . body ; // In a real app, get from auth middleware
45+ const userId = req . user . id ;
3746
3847 const success = await notificationService . markAsRead (
3948 notificationId ,
@@ -48,9 +57,9 @@ export class NotificationController {
4857 }
4958 }
5059
51- public async markAllAsRead ( req : Request , res : Response ) : Promise < void > {
60+ public async markAllAsRead ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
5261 try {
53- const { userId } = req . body ;
62+ const userId = req . user . id ;
5463 const count = await notificationService . markAllAsRead ( userId ) ;
5564 res . status ( 200 ) . json ( { success : true , count } ) ;
5665 } catch ( error ) {
@@ -61,9 +70,9 @@ export class NotificationController {
6170 }
6271 }
6372
64- public async getPreferences ( req : Request , res : Response ) : Promise < void > {
73+ public async getPreferences ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
6574 try {
66- const { userId } = req . params ;
75+ const userId = req . user . id ;
6776 const preferences = await notificationService . getUserPreferences ( userId ) ;
6877 res . status ( 200 ) . json ( { success : true , data : preferences } ) ;
6978 } catch ( error ) {
@@ -74,10 +83,20 @@ export class NotificationController {
7483 }
7584 }
7685
77- public async updatePreferences ( req : Request , res : Response ) : Promise < void > {
86+ public async updatePreferences ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
7887 try {
79- const { userId } = req . params ;
80- const preferences = req . body ;
88+ const userId = req . user . id ;
89+ // Sanitize input to only whitelisted preference fields
90+ const allowedFields = new Set ( [
91+ 'emailNotifications' , 'pushNotifications' , 'inAppNotifications' ,
92+ 'digestFrequency' , 'quietHoursStart' , 'quietHoursEnd' ,
93+ ] ) ;
94+ const preferences : Record < string , unknown > = { } ;
95+ for ( const [ key , value ] of Object . entries ( req . body ) ) {
96+ if ( allowedFields . has ( key ) ) {
97+ preferences [ key ] = value ;
98+ }
99+ }
81100
82101 await notificationService . setNotificationPreferences ( userId , preferences ) ;
83102 res . status ( 200 ) . json ( { success : true , message : "Preferences updated" } ) ;
@@ -89,10 +108,10 @@ export class NotificationController {
89108 }
90109 }
91110
92- public async deleteNotification ( req : Request , res : Response ) : Promise < void > {
111+ public async deleteNotification ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
93112 try {
94113 const { notificationId } = req . params ;
95- const { userId } = req . query ;
114+ const userId = req . user . id ;
96115
97116 const success = await notificationService . deleteNotification (
98117 notificationId ,
@@ -106,6 +125,136 @@ export class NotificationController {
106125 . json ( { success : false , message : "Internal server error" } ) ;
107126 }
108127 }
128+
129+ /// Get unread notification count for a user
130+ public async getUnreadCount ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
131+ try {
132+ const userId = req . user . id ;
133+ const unreadCount = await notificationService . getUnreadCount ( userId ) ;
134+ res . status ( 200 ) . json ( { success : true , data : { unreadCount } } ) ;
135+ } catch ( error ) {
136+ logger . error ( "Error in getUnreadCount controller:" , error ) ;
137+ res
138+ . status ( 500 )
139+ . json ( { success : false , message : "Internal server error" } ) ;
140+ }
141+ }
142+
143+ /// Push a real-time notification via WebSocket
144+ public async pushNotification ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
145+ try {
146+ const { userId, type, title, message, category, priority, actionUrl, metadata } = req . body ;
147+
148+ // Enforce string types to prevent type confusion (e.g., array/object injection)
149+ if ( ! userId || ! type || ! title || ! message ) {
150+ res . status ( 400 ) . json ( {
151+ success : false ,
152+ message : "Missing required fields: userId, type, title, and message are required" ,
153+ } ) ;
154+ return ;
155+ }
156+ if ( typeof userId !== 'string' || typeof type !== 'string' || typeof title !== 'string' || typeof message !== 'string' ) {
157+ res . status ( 400 ) . json ( {
158+ success : false ,
159+ message : "userId, type, title and message must be strings" ,
160+ } ) ;
161+ return ;
162+ }
163+
164+ // Validate and sanitize actionUrl and metadata before passing to service
165+ const safeActionUrl = typeof actionUrl === 'string' ? actionUrl : undefined ;
166+ const safeMetadata = metadata && typeof metadata === 'object' && ! Array . isArray ( metadata )
167+ ? metadata as Record < string , unknown >
168+ : undefined ;
169+
170+ const notification = await notificationService . createAndPushNotification (
171+ userId ,
172+ type as NotificationType ,
173+ title ,
174+ message ,
175+ category || "system" ,
176+ {
177+ priority : priority || "medium" ,
178+ actionUrl : safeActionUrl ,
179+ metadata : safeMetadata ,
180+ deliveryMethods : [ "websocket" ] ,
181+ } ,
182+ ) ;
183+
184+ res . status ( 200 ) . json ( { success : true , data : notification } ) ;
185+ } catch ( error ) {
186+ logger . error ( "Error in pushNotification controller:" , error ) ;
187+ res
188+ . status ( 500 )
189+ . json ( { success : false , message : "Internal server error" } ) ;
190+ }
191+ }
192+
193+ /// Admin announcement: push to all connected users or targeted roles
194+ public async sendAnnouncement ( req : AuthenticatedRequest , res : Response ) : Promise < void > {
195+ try {
196+ const { title, message, targetRoles, priority, actionUrl } = req . body ;
197+
198+ if ( ! title || ! message ) {
199+ res . status ( 400 ) . json ( {
200+ success : false ,
201+ message : "Title and message are required for announcements" ,
202+ } ) ;
203+ return ;
204+ }
205+ if ( typeof title !== 'string' || typeof message !== 'string' ) {
206+ res . status ( 400 ) . json ( {
207+ success : false ,
208+ message : "title and message must be strings" ,
209+ } ) ;
210+ return ;
211+ }
212+
213+ // Validate that actionUrl is a string if provided
214+ const safeActionUrl = typeof actionUrl === 'string' ? actionUrl : undefined ;
215+
216+ const result = await notificationService . sendAnnouncement (
217+ title ,
218+ message ,
219+ targetRoles || [ ] ,
220+ {
221+ priority : priority || "high" ,
222+ actionUrl : safeActionUrl ,
223+ } ,
224+ ) ;
225+
226+ res . status ( 200 ) . json ( { success : true , data : result } ) ;
227+ } catch ( error ) {
228+ logger . error ( "Error in sendAnnouncement controller:" , error ) ;
229+ res
230+ . status ( 500 )
231+ . json ( { success : false , message : "Internal server error" } ) ;
232+ }
233+ }
234+
235+ /// Deliver missed notifications to a user on reconnect
236+ public async deliverMissedNotifications (
237+ req : AuthenticatedRequest ,
238+ res : Response ,
239+ ) : Promise < void > {
240+ try {
241+ const userId = req . user . id ;
242+
243+ const delivered = await notificationService . deliverMissedNotifications (
244+ userId ,
245+ ) ;
246+
247+ res . status ( 200 ) . json ( { success : true , data : { delivered } } ) ;
248+ } catch ( error ) {
249+ logger . error (
250+ "Error in deliverMissedNotifications controller:" ,
251+ error ,
252+ ) ;
253+ res
254+ . status ( 500 )
255+ . json ( { success : false , message : "Internal server error" } ) ;
256+ }
257+ }
109258}
110259
111260export const notificationController = new NotificationController ( ) ;
0 commit comments