Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m53s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Has been cancelled
- Replace all ../../domain/ imports with @domain/ across 67 files - Configure NestJS to use tsconfig.build.json with rootDir - Add tsc-alias to resolve path aliases after build - This fixes 'Cannot find module' TypeScript compilation errors Fixed files: - 30 files in application layer - 37 files in infrastructure layer
244 lines
7.2 KiB
TypeScript
244 lines
7.2 KiB
TypeScript
/**
|
|
* 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<string, Set<string>> = 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(),
|
|
};
|
|
}
|
|
}
|