Documented comprehensive implementation plans for the authentication system and database schema, including architecture, module structure, API integration, security measures, and GDPR compliance details.
801 lines
25 KiB
Markdown
801 lines
25 KiB
Markdown
# Plan d'Implémentation des WebSockets
|
|
|
|
Ce document détaille le plan d'implémentation du système de communication en temps réel via WebSockets pour l'application de création de groupes, basé sur les spécifications du cahier des charges.
|
|
|
|
## 1. Vue d'Ensemble
|
|
|
|
L'application utilisera Socket.IO pour établir une communication bidirectionnelle en temps réel entre le client et le serveur. Cette fonctionnalité permettra :
|
|
|
|
- La mise à jour instantanée des groupes
|
|
- Les notifications en temps réel
|
|
- La collaboration simultanée entre utilisateurs
|
|
|
|
## 2. Architecture WebSocket
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Client as Client (Next.js)
|
|
participant Gateway as WebSocket Gateway (NestJS)
|
|
participant Service as Services NestJS
|
|
participant DB as Base de données
|
|
|
|
Client->>Gateway: Connexion WebSocket
|
|
Gateway->>Gateway: Authentification (JWT)
|
|
Gateway->>Client: Connexion établie
|
|
|
|
Client->>Gateway: Rejoindre une salle (projet)
|
|
Gateway->>Client: Confirmation d'adhésion à la salle
|
|
|
|
Note over Client,Gateway: Communication bidirectionnelle
|
|
|
|
Client->>Gateway: Événement (ex: modification groupe)
|
|
Gateway->>Service: Traitement de l'événement
|
|
Service->>DB: Mise à jour des données
|
|
Service->>Gateway: Résultat de l'opération
|
|
Gateway->>Client: Diffusion aux clients concernés
|
|
```
|
|
|
|
## 3. Structure des Modules
|
|
|
|
### 3.1 Module WebSockets
|
|
|
|
```typescript
|
|
// src/modules/websockets/websockets.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { JwtModule } from '@nestjs/jwt';
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
import { ProjectsModule } from '../projects/projects.module';
|
|
import { GroupsModule } from '../groups/groups.module';
|
|
import { UsersModule } from '../users/users.module';
|
|
import { ProjectsGateway } from './gateways/projects.gateway';
|
|
import { GroupsGateway } from './gateways/groups.gateway';
|
|
import { NotificationsGateway } from './gateways/notifications.gateway';
|
|
import { WebSocketService } from './services/websocket.service';
|
|
|
|
@Module({
|
|
imports: [
|
|
JwtModule.registerAsync({
|
|
imports: [ConfigModule],
|
|
inject: [ConfigService],
|
|
useFactory: (configService: ConfigService) => ({
|
|
secret: configService.get<string>('JWT_ACCESS_SECRET'),
|
|
}),
|
|
}),
|
|
ProjectsModule,
|
|
GroupsModule,
|
|
UsersModule,
|
|
],
|
|
providers: [
|
|
ProjectsGateway,
|
|
GroupsGateway,
|
|
NotificationsGateway,
|
|
WebSocketService,
|
|
],
|
|
exports: [WebSocketService],
|
|
})
|
|
export class WebSocketsModule {}
|
|
```
|
|
|
|
### 3.2 Gateways WebSocket
|
|
|
|
#### 3.2.1 Gateway de Base
|
|
|
|
```typescript
|
|
// src/modules/websockets/gateways/base.gateway.ts
|
|
import {
|
|
OnGatewayConnection,
|
|
OnGatewayDisconnect,
|
|
OnGatewayInit,
|
|
WebSocketServer,
|
|
} from '@nestjs/websockets';
|
|
import { Logger, UseGuards } from '@nestjs/common';
|
|
import { Server, Socket } from 'socket.io';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { WsJwtGuard } from '../guards/ws-jwt.guard';
|
|
import { WebSocketService } from '../services/websocket.service';
|
|
|
|
export abstract class BaseGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
|
@WebSocketServer() server: Server;
|
|
protected logger = new Logger(this.constructor.name);
|
|
|
|
constructor(
|
|
protected readonly jwtService: JwtService,
|
|
protected readonly webSocketService: WebSocketService,
|
|
) {}
|
|
|
|
afterInit(server: Server) {
|
|
this.webSocketService.setServer(server);
|
|
this.logger.log('WebSocket Gateway initialized');
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
async handleConnection(client: Socket) {
|
|
try {
|
|
const token = this.extractTokenFromHeader(client);
|
|
if (!token) {
|
|
this.disconnect(client);
|
|
return;
|
|
}
|
|
|
|
const payload = this.jwtService.verify(token);
|
|
const userId = payload.sub;
|
|
|
|
// Associer l'ID utilisateur au socket
|
|
client.data.userId = userId;
|
|
|
|
// Ajouter le client à la liste des clients connectés
|
|
this.webSocketService.addClient(userId, client.id);
|
|
|
|
this.logger.log(`Client connected: ${client.id}, User: ${userId}`);
|
|
} catch (error) {
|
|
this.logger.error(`Connection error: ${error.message}`);
|
|
this.disconnect(client);
|
|
}
|
|
}
|
|
|
|
handleDisconnect(client: Socket) {
|
|
const userId = client.data.userId;
|
|
if (userId) {
|
|
this.webSocketService.removeClient(userId, client.id);
|
|
}
|
|
this.logger.log(`Client disconnected: ${client.id}`);
|
|
}
|
|
|
|
private extractTokenFromHeader(client: Socket): string | undefined {
|
|
const auth = client.handshake.auth.token || client.handshake.headers.authorization;
|
|
if (!auth) return undefined;
|
|
|
|
const parts = auth.split(' ');
|
|
if (parts.length === 2 && parts[0] === 'Bearer') {
|
|
return parts[1];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private disconnect(client: Socket) {
|
|
client.disconnect();
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.2.2 Gateway des Projets
|
|
|
|
```typescript
|
|
// src/modules/websockets/gateways/projects.gateway.ts
|
|
import {
|
|
WebSocketGateway,
|
|
SubscribeMessage,
|
|
MessageBody,
|
|
ConnectedSocket,
|
|
} from '@nestjs/websockets';
|
|
import { UseGuards } from '@nestjs/common';
|
|
import { Socket } from 'socket.io';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { ProjectsService } from '../../projects/services/projects.service';
|
|
import { BaseGateway } from './base.gateway';
|
|
import { WsJwtGuard } from '../guards/ws-jwt.guard';
|
|
import { WebSocketService } from '../services/websocket.service';
|
|
import { JoinProjectDto } from '../dto/join-project.dto';
|
|
import { ProjectUpdatedEvent } from '../events/project-updated.event';
|
|
|
|
@WebSocketGateway({
|
|
cors: {
|
|
origin: '*',
|
|
},
|
|
namespace: 'projects',
|
|
})
|
|
export class ProjectsGateway extends BaseGateway {
|
|
constructor(
|
|
protected readonly jwtService: JwtService,
|
|
protected readonly webSocketService: WebSocketService,
|
|
private readonly projectsService: ProjectsService,
|
|
) {
|
|
super(jwtService, webSocketService);
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('joinProject')
|
|
async handleJoinProject(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() data: JoinProjectDto,
|
|
) {
|
|
try {
|
|
const { projectId } = data;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Rejoindre la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
await client.join(roomName);
|
|
|
|
// Enregistrer l'association utilisateur-projet
|
|
this.webSocketService.addUserToProject(userId, projectId, client.id);
|
|
|
|
this.logger.log(`User ${userId} joined project ${projectId}`);
|
|
|
|
return { success: true, message: `Joined project ${projectId}` };
|
|
} catch (error) {
|
|
this.logger.error(`Error joining project: ${error.message}`);
|
|
return { error: 'Failed to join project' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('leaveProject')
|
|
async handleLeaveProject(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() data: JoinProjectDto,
|
|
) {
|
|
try {
|
|
const { projectId } = data;
|
|
const userId = client.data.userId;
|
|
|
|
// Quitter la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
await client.leave(roomName);
|
|
|
|
// Supprimer l'association utilisateur-projet
|
|
this.webSocketService.removeUserFromProject(userId, projectId, client.id);
|
|
|
|
this.logger.log(`User ${userId} left project ${projectId}`);
|
|
|
|
return { success: true, message: `Left project ${projectId}` };
|
|
} catch (error) {
|
|
this.logger.error(`Error leaving project: ${error.message}`);
|
|
return { error: 'Failed to leave project' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('projectUpdated')
|
|
async handleProjectUpdated(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: ProjectUpdatedEvent,
|
|
) {
|
|
try {
|
|
const { projectId, data } = event;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Diffuser la mise à jour à tous les clients dans la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit('projectUpdated', {
|
|
projectId,
|
|
data,
|
|
updatedBy: userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Project ${projectId} updated by user ${userId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error updating project: ${error.message}`);
|
|
return { error: 'Failed to update project' };
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.2.3 Gateway des Groupes
|
|
|
|
```typescript
|
|
// src/modules/websockets/gateways/groups.gateway.ts
|
|
import {
|
|
WebSocketGateway,
|
|
SubscribeMessage,
|
|
MessageBody,
|
|
ConnectedSocket,
|
|
} from '@nestjs/websockets';
|
|
import { UseGuards } from '@nestjs/common';
|
|
import { Socket } from 'socket.io';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { GroupsService } from '../../groups/services/groups.service';
|
|
import { ProjectsService } from '../../projects/services/projects.service';
|
|
import { BaseGateway } from './base.gateway';
|
|
import { WsJwtGuard } from '../guards/ws-jwt.guard';
|
|
import { WebSocketService } from '../services/websocket.service';
|
|
import { GroupCreatedEvent } from '../events/group-created.event';
|
|
import { GroupUpdatedEvent } from '../events/group-updated.event';
|
|
import { GroupDeletedEvent } from '../events/group-deleted.event';
|
|
import { PersonMovedEvent } from '../events/person-moved.event';
|
|
|
|
@WebSocketGateway({
|
|
cors: {
|
|
origin: '*',
|
|
},
|
|
namespace: 'groups',
|
|
})
|
|
export class GroupsGateway extends BaseGateway {
|
|
constructor(
|
|
protected readonly jwtService: JwtService,
|
|
protected readonly webSocketService: WebSocketService,
|
|
private readonly groupsService: GroupsService,
|
|
private readonly projectsService: ProjectsService,
|
|
) {
|
|
super(jwtService, webSocketService);
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('groupCreated')
|
|
async handleGroupCreated(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: GroupCreatedEvent,
|
|
) {
|
|
try {
|
|
const { projectId, group } = event;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Diffuser la création du groupe à tous les clients dans la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit('groupCreated', {
|
|
projectId,
|
|
group,
|
|
createdBy: userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Group created in project ${projectId} by user ${userId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error creating group: ${error.message}`);
|
|
return { error: 'Failed to create group' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('groupUpdated')
|
|
async handleGroupUpdated(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: GroupUpdatedEvent,
|
|
) {
|
|
try {
|
|
const { projectId, groupId, data } = event;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Diffuser la mise à jour du groupe à tous les clients dans la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit('groupUpdated', {
|
|
projectId,
|
|
groupId,
|
|
data,
|
|
updatedBy: userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Group ${groupId} updated in project ${projectId} by user ${userId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error updating group: ${error.message}`);
|
|
return { error: 'Failed to update group' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('groupDeleted')
|
|
async handleGroupDeleted(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: GroupDeletedEvent,
|
|
) {
|
|
try {
|
|
const { projectId, groupId } = event;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Diffuser la suppression du groupe à tous les clients dans la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit('groupDeleted', {
|
|
projectId,
|
|
groupId,
|
|
deletedBy: userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Group ${groupId} deleted from project ${projectId} by user ${userId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error deleting group: ${error.message}`);
|
|
return { error: 'Failed to delete group' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('personMoved')
|
|
async handlePersonMoved(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: PersonMovedEvent,
|
|
) {
|
|
try {
|
|
const { projectId, personId, fromGroupId, toGroupId } = event;
|
|
const userId = client.data.userId;
|
|
|
|
// Vérifier si l'utilisateur a accès au projet
|
|
const hasAccess = await this.projectsService.checkUserAccess(projectId, userId);
|
|
if (!hasAccess) {
|
|
return { error: 'Access denied to this project' };
|
|
}
|
|
|
|
// Diffuser le déplacement de la personne à tous les clients dans la salle du projet
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit('personMoved', {
|
|
projectId,
|
|
personId,
|
|
fromGroupId,
|
|
toGroupId,
|
|
movedBy: userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Person ${personId} moved from group ${fromGroupId} to group ${toGroupId} in project ${projectId} by user ${userId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error moving person: ${error.message}`);
|
|
return { error: 'Failed to move person' };
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.2.4 Gateway des Notifications
|
|
|
|
```typescript
|
|
// src/modules/websockets/gateways/notifications.gateway.ts
|
|
import {
|
|
WebSocketGateway,
|
|
SubscribeMessage,
|
|
MessageBody,
|
|
ConnectedSocket,
|
|
} from '@nestjs/websockets';
|
|
import { UseGuards } from '@nestjs/common';
|
|
import { Socket } from 'socket.io';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { BaseGateway } from './base.gateway';
|
|
import { WsJwtGuard } from '../guards/ws-jwt.guard';
|
|
import { WebSocketService } from '../services/websocket.service';
|
|
import { NotificationEvent } from '../events/notification.event';
|
|
|
|
@WebSocketGateway({
|
|
cors: {
|
|
origin: '*',
|
|
},
|
|
namespace: 'notifications',
|
|
})
|
|
export class NotificationsGateway extends BaseGateway {
|
|
constructor(
|
|
protected readonly jwtService: JwtService,
|
|
protected readonly webSocketService: WebSocketService,
|
|
) {
|
|
super(jwtService, webSocketService);
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('subscribeToNotifications')
|
|
async handleSubscribeToNotifications(@ConnectedSocket() client: Socket) {
|
|
try {
|
|
const userId = client.data.userId;
|
|
|
|
// Rejoindre la salle des notifications personnelles
|
|
const roomName = `user:${userId}:notifications`;
|
|
await client.join(roomName);
|
|
|
|
this.logger.log(`User ${userId} subscribed to notifications`);
|
|
|
|
return { success: true, message: 'Subscribed to notifications' };
|
|
} catch (error) {
|
|
this.logger.error(`Error subscribing to notifications: ${error.message}`);
|
|
return { error: 'Failed to subscribe to notifications' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('unsubscribeFromNotifications')
|
|
async handleUnsubscribeFromNotifications(@ConnectedSocket() client: Socket) {
|
|
try {
|
|
const userId = client.data.userId;
|
|
|
|
// Quitter la salle des notifications personnelles
|
|
const roomName = `user:${userId}:notifications`;
|
|
await client.leave(roomName);
|
|
|
|
this.logger.log(`User ${userId} unsubscribed from notifications`);
|
|
|
|
return { success: true, message: 'Unsubscribed from notifications' };
|
|
} catch (error) {
|
|
this.logger.error(`Error unsubscribing from notifications: ${error.message}`);
|
|
return { error: 'Failed to unsubscribe from notifications' };
|
|
}
|
|
}
|
|
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage('sendNotification')
|
|
async handleSendNotification(
|
|
@ConnectedSocket() client: Socket,
|
|
@MessageBody() event: NotificationEvent,
|
|
) {
|
|
try {
|
|
const { recipientId, type, data } = event;
|
|
const senderId = client.data.userId;
|
|
|
|
// Diffuser la notification à l'utilisateur spécifique
|
|
const roomName = `user:${recipientId}:notifications`;
|
|
this.server.to(roomName).emit('notification', {
|
|
type,
|
|
data,
|
|
senderId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
this.logger.log(`Notification sent from user ${senderId} to user ${recipientId}`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
this.logger.error(`Error sending notification: ${error.message}`);
|
|
return { error: 'Failed to send notification' };
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.3 Service WebSocket
|
|
|
|
```typescript
|
|
// src/modules/websockets/services/websocket.service.ts
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Server } from 'socket.io';
|
|
|
|
@Injectable()
|
|
export class WebSocketService {
|
|
private server: Server;
|
|
private readonly logger = new Logger(WebSocketService.name);
|
|
|
|
// Map des clients connectés par utilisateur
|
|
private readonly connectedClients = new Map<string, Set<string>>();
|
|
|
|
// Map des projets par utilisateur
|
|
private readonly userProjects = new Map<string, Set<string>>();
|
|
|
|
// Map des utilisateurs par projet
|
|
private readonly projectUsers = new Map<string, Set<string>>();
|
|
|
|
// Map des sockets par projet
|
|
private readonly projectSockets = new Map<string, Set<string>>();
|
|
|
|
setServer(server: Server) {
|
|
this.server = server;
|
|
}
|
|
|
|
getServer(): Server {
|
|
return this.server;
|
|
}
|
|
|
|
// Gestion des clients connectés
|
|
addClient(userId: string, socketId: string) {
|
|
if (!this.connectedClients.has(userId)) {
|
|
this.connectedClients.set(userId, new Set());
|
|
}
|
|
this.connectedClients.get(userId).add(socketId);
|
|
this.logger.debug(`Client ${socketId} added for user ${userId}`);
|
|
}
|
|
|
|
removeClient(userId: string, socketId: string) {
|
|
if (this.connectedClients.has(userId)) {
|
|
this.connectedClients.get(userId).delete(socketId);
|
|
if (this.connectedClients.get(userId).size === 0) {
|
|
this.connectedClients.delete(userId);
|
|
}
|
|
}
|
|
|
|
// Nettoyer les associations projet-utilisateur
|
|
this.cleanupUserProjects(userId, socketId);
|
|
|
|
this.logger.debug(`Client ${socketId} removed for user ${userId}`);
|
|
}
|
|
|
|
isUserConnected(userId: string): boolean {
|
|
return this.connectedClients.has(userId) && this.connectedClients.get(userId).size > 0;
|
|
}
|
|
|
|
getUserSocketIds(userId: string): string[] {
|
|
if (!this.connectedClients.has(userId)) {
|
|
return [];
|
|
}
|
|
return Array.from(this.connectedClients.get(userId));
|
|
}
|
|
|
|
// Gestion des associations utilisateur-projet
|
|
addUserToProject(userId: string, projectId: string, socketId: string) {
|
|
// Ajouter le projet à l'utilisateur
|
|
if (!this.userProjects.has(userId)) {
|
|
this.userProjects.set(userId, new Set());
|
|
}
|
|
this.userProjects.get(userId).add(projectId);
|
|
|
|
// Ajouter l'utilisateur au projet
|
|
if (!this.projectUsers.has(projectId)) {
|
|
this.projectUsers.set(projectId, new Set());
|
|
}
|
|
this.projectUsers.get(projectId).add(userId);
|
|
|
|
// Ajouter le socket au projet
|
|
if (!this.projectSockets.has(projectId)) {
|
|
this.projectSockets.set(projectId, new Set());
|
|
}
|
|
this.projectSockets.get(projectId).add(socketId);
|
|
|
|
this.logger.debug(`User ${userId} added to project ${projectId} with socket ${socketId}`);
|
|
}
|
|
|
|
removeUserFromProject(userId: string, projectId: string, socketId: string) {
|
|
// Supprimer le socket du projet
|
|
if (this.projectSockets.has(projectId)) {
|
|
this.projectSockets.get(projectId).delete(socketId);
|
|
if (this.projectSockets.get(projectId).size === 0) {
|
|
this.projectSockets.delete(projectId);
|
|
}
|
|
}
|
|
|
|
// Vérifier si l'utilisateur a d'autres sockets connectés au projet
|
|
const userSocketIds = this.getUserSocketIds(userId);
|
|
const hasOtherSocketsInProject = userSocketIds.some(sid =>
|
|
sid !== socketId && this.projectSockets.has(projectId) && this.projectSockets.get(projectId).has(sid)
|
|
);
|
|
|
|
// Si l'utilisateur n'a plus de sockets connectés au projet, supprimer l'association
|
|
if (!hasOtherSocketsInProject) {
|
|
// Supprimer le projet de l'utilisateur
|
|
if (this.userProjects.has(userId)) {
|
|
this.userProjects.get(userId).delete(projectId);
|
|
if (this.userProjects.get(userId).size === 0) {
|
|
this.userProjects.delete(userId);
|
|
}
|
|
}
|
|
|
|
// Supprimer l'utilisateur du projet
|
|
if (this.projectUsers.has(projectId)) {
|
|
this.projectUsers.get(projectId).delete(userId);
|
|
if (this.projectUsers.get(projectId).size === 0) {
|
|
this.projectUsers.delete(projectId);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.logger.debug(`User ${userId} removed from project ${projectId} with socket ${socketId}`);
|
|
}
|
|
|
|
getUserProjects(userId: string): string[] {
|
|
if (!this.userProjects.has(userId)) {
|
|
return [];
|
|
}
|
|
return Array.from(this.userProjects.get(userId));
|
|
}
|
|
|
|
getProjectUsers(projectId: string): string[] {
|
|
if (!this.projectUsers.has(projectId)) {
|
|
return [];
|
|
}
|
|
return Array.from(this.projectUsers.get(projectId));
|
|
}
|
|
|
|
// Nettoyage des associations lors de la déconnexion
|
|
private cleanupUserProjects(userId: string, socketId: string) {
|
|
const projectIds = this.getUserProjects(userId);
|
|
for (const projectId of projectIds) {
|
|
this.removeUserFromProject(userId, projectId, socketId);
|
|
}
|
|
}
|
|
|
|
// Méthodes pour envoyer des messages
|
|
sendToUser(userId: string, event: string, data: any) {
|
|
const socketIds = this.getUserSocketIds(userId);
|
|
for (const socketId of socketIds) {
|
|
this.server.to(socketId).emit(event, data);
|
|
}
|
|
this.logger.debug(`Event ${event} sent to user ${userId}`);
|
|
}
|
|
|
|
sendToProject(projectId: string, event: string, data: any) {
|
|
const roomName = `project:${projectId}`;
|
|
this.server.to(roomName).emit(event, data);
|
|
this.logger.debug(`Event ${event} sent to project ${projectId}`);
|
|
}
|
|
|
|
broadcastToAll(event: string, data: any) {
|
|
this.server.emit(event, data);
|
|
this.logger.debug(`Event ${event} broadcasted to all connected clients`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.4 Guard WebSocket JWT
|
|
|
|
```typescript
|
|
// src/modules/websockets/guards/ws-jwt.guard.ts
|
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { WsException } from '@nestjs/websockets';
|
|
import { Socket } from 'socket.io';
|
|
|
|
@Injectable()
|
|
export class WsJwtGuard implements CanActivate {
|
|
constructor(private readonly jwtService: JwtService) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
try {
|
|
const client: Socket = context.switchToWs().getClient();
|
|
const token = this.extractTokenFromHeader(client);
|
|
|
|
if (!token) {
|
|
throw new WsException('Unauthorized');
|
|
}
|
|
|
|
const payload = this.jwtService.verify(token);
|
|
client.data.userId = payload.sub;
|
|
|
|
return true;
|
|
} catch (error) {
|
|
throw new WsException('Unauthorized');
|
|
}
|
|
}
|
|
|
|
private extractTokenFromHeader(client: Socket): string | undefined {
|
|
const auth = client.handshake.auth.token || client.handshake.headers.authorization;
|
|
if (!auth) return undefined;
|
|
|
|
const parts = auth.split(' ');
|
|
if (parts.length === 2 && parts[0] === 'Bearer') {
|
|
return parts[1];
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.5 DTOs et Événements
|
|
|
|
#### 3.5.1 DTO JoinProject
|
|
|
|
```typescript
|
|
// src/modules/websockets/dto/join-project.dto.ts
|
|
import { IsUUID, IsNotEmpty } from 'class-validator';
|
|
|
|
export class JoinProjectDto {
|
|
@IsUUID()
|
|
@IsNotEmpty()
|
|
projectId: string;
|
|
}
|
|
```
|
|
|
|
#### 3.5.2 Événement ProjectUpdated
|
|
|
|
```typescript |