/** * Notifications WebSocket Gateway * * Handles real-time notification delivery via WebSocket */ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, ConnectedSocket, MessageBody, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger, UseGuards } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { NotificationService } from '../services/notification.service'; import { Notification } from '@domain/entities/notification.entity'; /** * WebSocket authentication guard */ @UseGuards() @WebSocketGateway({ cors: { origin: process.env.FRONTEND_URL || ['http://localhost:3000', 'http://localhost:3001'], credentials: true, }, namespace: '/notifications', }) export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private readonly logger = new Logger(NotificationsGateway.name); private userSockets: Map> = new Map(); // userId -> Set of socket IDs constructor( private readonly jwtService: JwtService, private readonly notificationService: NotificationService ) {} /** * Handle client connection */ async handleConnection(client: Socket) { try { // Extract JWT token from handshake const token = this.extractToken(client); if (!token) { this.logger.warn(`Client ${client.id} connection rejected: No token provided`); client.disconnect(); return; } // Verify JWT token const payload = await this.jwtService.verifyAsync(token); const userId = payload.sub; // Store socket connection for user if (!this.userSockets.has(userId)) { this.userSockets.set(userId, new Set()); } this.userSockets.get(userId)!.add(client.id); // Store user ID in socket data for later use client.data.userId = userId; client.data.organizationId = payload.organizationId; // Join user-specific room client.join(`user:${userId}`); this.logger.log(`Client ${client.id} connected for user ${userId}`); // Send unread count on connection const unreadCount = await this.notificationService.getUnreadCount(userId); client.emit('unread_count', { count: unreadCount }); // Send recent notifications on connection const recentNotifications = await this.notificationService.getRecentNotifications(userId, 10); client.emit('recent_notifications', { notifications: recentNotifications.map(n => this.mapNotificationToDto(n)), }); } catch (error: any) { this.logger.error( `Error during client connection: ${error?.message || 'Unknown error'}`, error?.stack ); client.disconnect(); } } /** * Handle client disconnection */ handleDisconnect(client: Socket) { const userId = client.data.userId; if (userId && this.userSockets.has(userId)) { this.userSockets.get(userId)!.delete(client.id); if (this.userSockets.get(userId)!.size === 0) { this.userSockets.delete(userId); } } this.logger.log(`Client ${client.id} disconnected`); } /** * Handle mark notification as read */ @SubscribeMessage('mark_as_read') async handleMarkAsRead( @ConnectedSocket() client: Socket, @MessageBody() data: { notificationId: string } ) { try { const userId = client.data.userId; await this.notificationService.markAsRead(data.notificationId); // Send updated unread count const unreadCount = await this.notificationService.getUnreadCount(userId); this.emitToUser(userId, 'unread_count', { count: unreadCount }); return { success: true }; } catch (error: any) { this.logger.error(`Error marking notification as read: ${error?.message}`); return { success: false, error: error?.message }; } } /** * Handle mark all notifications as read */ @SubscribeMessage('mark_all_as_read') async handleMarkAllAsRead(@ConnectedSocket() client: Socket) { try { const userId = client.data.userId; await this.notificationService.markAllAsRead(userId); // Send updated unread count (should be 0) this.emitToUser(userId, 'unread_count', { count: 0 }); return { success: true }; } catch (error: any) { this.logger.error(`Error marking all notifications as read: ${error?.message}`); return { success: false, error: error?.message }; } } /** * Handle get unread count */ @SubscribeMessage('get_unread_count') async handleGetUnreadCount(@ConnectedSocket() client: Socket) { try { const userId = client.data.userId; const unreadCount = await this.notificationService.getUnreadCount(userId); return { count: unreadCount }; } catch (error: any) { this.logger.error(`Error getting unread count: ${error?.message}`); return { count: 0 }; } } /** * Send notification to a specific user */ async sendNotificationToUser(userId: string, notification: Notification) { const notificationDto = this.mapNotificationToDto(notification); // Emit to all connected sockets for this user this.emitToUser(userId, 'new_notification', { notification: notificationDto }); // Update unread count const unreadCount = await this.notificationService.getUnreadCount(userId); this.emitToUser(userId, 'unread_count', { count: unreadCount }); this.logger.log(`Notification sent to user ${userId}: ${notification.title}`); } /** * Broadcast notification to organization */ async broadcastToOrganization(organizationId: string, notification: Notification) { const notificationDto = this.mapNotificationToDto(notification); this.server.to(`org:${organizationId}`).emit('new_notification', { notification: notificationDto, }); this.logger.log(`Notification broadcasted to organization ${organizationId}`); } /** * Helper: Emit event to all sockets of a user */ private emitToUser(userId: string, event: string, data: any) { this.server.to(`user:${userId}`).emit(event, data); } /** * Helper: Extract JWT token from socket handshake */ private extractToken(client: Socket): string | null { // Check Authorization header const authHeader = client.handshake.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } // Check query parameter const token = client.handshake.query.token; if (typeof token === 'string') { return token; } // Check auth object (socket.io-client way) const auth = client.handshake.auth; if (auth && typeof auth.token === 'string') { return auth.token; } return null; } /** * Helper: Map notification entity to DTO */ private mapNotificationToDto(notification: Notification) { return { id: notification.id, type: notification.type, priority: notification.priority, title: notification.title, message: notification.message, metadata: notification.metadata, read: notification.read, readAt: notification.readAt?.toISOString(), actionUrl: notification.actionUrl, createdAt: notification.createdAt.toISOString(), }; } }