Added detailed documentation files, including project overview, current status, specifications, implementation guide, and README structure. Organized content to improve navigation and streamline project understanding.
24 KiB
24 KiB
Plan d'Implémentation des WebSockets
Ce document détaille le plan d'implémentation du système de communication en temps réel avec 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 la communication en temps réel entre les clients et le serveur. Cette approche permettra :
- La collaboration en temps réel entre les utilisateurs travaillant sur le même projet
- Les notifications instantanées pour les actions importantes
- La mise à jour automatique de l'interface utilisateur lorsque des changements sont effectués par d'autres utilisateurs
2. Architecture WebSocket
flowchart TB
subgraph Client["Clients"]
C1["Client 1"]
C2["Client 2"]
C3["Client 3"]
end
subgraph Server["Serveur NestJS"]
GW["WebSocket Gateway"]
AS["Authentication Service"]
PS["Project Service"]
GS["Group Service"]
NS["Notification Service"]
end
C1 <--> GW
C2 <--> GW
C3 <--> GW
GW <--> AS
GW <--> PS
GW <--> GS
GW <--> NS
3. Configuration de Socket.IO avec NestJS
3.1 Installation des Dépendances
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
pnpm add -D @types/socket.io
3.2 Module WebSockets
// src/modules/websockets/websockets.module.ts
import { Module } from '@nestjs/common';
import { ProjectsGateway } from './gateways/projects.gateway';
import { GroupsGateway } from './gateways/groups.gateway';
import { NotificationsGateway } from './gateways/notifications.gateway';
import { WebSocketService } from './services/websocket.service';
import { AuthModule } from '../auth/auth.module';
import { ProjectsModule } from '../projects/projects.module';
import { GroupsModule } from '../groups/groups.module';
@Module({
imports: [AuthModule, ProjectsModule, GroupsModule],
providers: [
ProjectsGateway,
GroupsGateway,
NotificationsGateway,
WebSocketService,
],
exports: [WebSocketService],
})
export class WebSocketsModule {}
4. Service WebSocket
Le service WebSocket sera responsable de la gestion des connexions et des salles.
// src/modules/websockets/services/websocket.service.ts
import { Injectable } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
@Injectable()
export class WebSocketService {
private server: Server;
setServer(server: Server) {
this.server = server;
}
getServer(): Server {
return this.server;
}
joinRoom(socket: Socket, room: string) {
socket.join(room);
}
leaveRoom(socket: Socket, room: string) {
socket.leave(room);
}
emitToRoom(room: string, event: string, data: any) {
this.server.to(room).emit(event, data);
}
emitToAll(event: string, data: any) {
this.server.emit(event, data);
}
emitToUser(userId: string, event: string, data: any) {
this.emitToRoom(`user:${userId}`, event, data);
}
emitToProject(projectId: string, event: string, data: any) {
this.emitToRoom(`project:${projectId}`, event, data);
}
emitToGroup(groupId: string, event: string, data: any) {
this.emitToRoom(`group:${groupId}`, event, data);
}
}
5. Gateways WebSocket
5.1 Gateway de Base
// src/modules/websockets/gateways/base.gateway.ts
import {
WebSocketGateway,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketServer,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { WebSocketService } from '../services/websocket.service';
import { AuthService } from '../../auth/services/auth.service';
@WebSocketGateway({
cors: {
origin: process.env.CORS_ORIGIN,
credentials: true,
},
})
export class BaseGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
protected readonly logger = new Logger(this.constructor.name);
constructor(
protected readonly webSocketService: WebSocketService,
protected readonly authService: AuthService,
) {}
afterInit(server: Server) {
this.webSocketService.setServer(server);
this.logger.log('WebSocket Gateway initialized');
}
async handleConnection(client: Socket) {
try {
const token = client.handshake.auth.token;
if (!token) {
client.disconnect();
return;
}
const user = await this.authService.validateToken(token);
if (!user) {
client.disconnect();
return;
}
client.data.user = user;
// Rejoindre la salle personnelle de l'utilisateur
this.webSocketService.joinRoom(client, `user:${user.id}`);
this.logger.log(`Client connected: ${client.id}, User: ${user.id}`);
} catch (error) {
this.logger.error(`Connection error: ${error.message}`);
client.disconnect();
}
}
handleDisconnect(client: Socket) {
this.logger.log(`Client disconnected: ${client.id}`);
}
getUserFromSocket(client: Socket) {
return client.data.user;
}
}
5.2 Gateway Projets
// src/modules/websockets/gateways/projects.gateway.ts
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { BaseGateway } from './base.gateway';
import { WebSocketService } from '../services/websocket.service';
import { AuthService } from '../../auth/services/auth.service';
import { ProjectsService } from '../../projects/services/projects.service';
export class ProjectsGateway extends BaseGateway {
constructor(
protected readonly webSocketService: WebSocketService,
protected readonly authService: AuthService,
private readonly projectsService: ProjectsService,
) {
super(webSocketService, authService);
}
@SubscribeMessage('project:join')
async handleJoinProject(
@ConnectedSocket() client: Socket,
@MessageBody() data: { projectId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { projectId } = data;
// Vérifier si l'utilisateur a accès au projet
const hasAccess = await this.projectsService.hasAccess(projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Rejoindre la salle du projet
this.webSocketService.joinRoom(client, `project:${projectId}`);
this.logger.log(`User ${user.id} joined project ${projectId}`);
return { success: true };
} catch (error) {
this.logger.error(`Error joining project: ${error.message}`);
return { error: 'Failed to join project' };
}
}
@SubscribeMessage('project:leave')
handleLeaveProject(
@ConnectedSocket() client: Socket,
@MessageBody() data: { projectId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { projectId } = data;
// Quitter la salle du projet
this.webSocketService.leaveRoom(client, `project:${projectId}`);
this.logger.log(`User ${user.id} left project ${projectId}`);
return { success: true };
} catch (error) {
this.logger.error(`Error leaving project: ${error.message}`);
return { error: 'Failed to leave project' };
}
}
@SubscribeMessage('project:update')
async handleUpdateProject(
@ConnectedSocket() client: Socket,
@MessageBody() data: { projectId: string, changes: any },
) {
try {
const user = this.getUserFromSocket(client);
const { projectId, changes } = data;
// Vérifier si l'utilisateur a accès au projet
const hasAccess = await this.projectsService.hasAccess(projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Émettre l'événement de mise à jour à tous les clients dans la salle du projet
this.webSocketService.emitToProject(projectId, 'project:updated', {
projectId,
changes,
updatedBy: user.id,
});
return { success: true };
} catch (error) {
this.logger.error(`Error updating project: ${error.message}`);
return { error: 'Failed to update project' };
}
}
}
5.3 Gateway Groupes
// src/modules/websockets/gateways/groups.gateway.ts
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { BaseGateway } from './base.gateway';
import { WebSocketService } from '../services/websocket.service';
import { AuthService } from '../../auth/services/auth.service';
import { GroupsService } from '../../groups/services/groups.service';
import { ProjectsService } from '../../projects/services/projects.service';
export class GroupsGateway extends BaseGateway {
constructor(
protected readonly webSocketService: WebSocketService,
protected readonly authService: AuthService,
private readonly groupsService: GroupsService,
private readonly projectsService: ProjectsService,
) {
super(webSocketService, authService);
}
@SubscribeMessage('group:join')
async handleJoinGroup(
@ConnectedSocket() client: Socket,
@MessageBody() data: { groupId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { groupId } = data;
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
const group = await this.groupsService.findById(groupId);
if (!group) {
return { error: 'Group not found' };
}
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Rejoindre la salle du groupe
this.webSocketService.joinRoom(client, `group:${groupId}`);
this.logger.log(`User ${user.id} joined group ${groupId}`);
return { success: true };
} catch (error) {
this.logger.error(`Error joining group: ${error.message}`);
return { error: 'Failed to join group' };
}
}
@SubscribeMessage('group:leave')
handleLeaveGroup(
@ConnectedSocket() client: Socket,
@MessageBody() data: { groupId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { groupId } = data;
// Quitter la salle du groupe
this.webSocketService.leaveRoom(client, `group:${groupId}`);
this.logger.log(`User ${user.id} left group ${groupId}`);
return { success: true };
} catch (error) {
this.logger.error(`Error leaving group: ${error.message}`);
return { error: 'Failed to leave group' };
}
}
@SubscribeMessage('group:update')
async handleUpdateGroup(
@ConnectedSocket() client: Socket,
@MessageBody() data: { groupId: string, changes: any },
) {
try {
const user = this.getUserFromSocket(client);
const { groupId, changes } = data;
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
const group = await this.groupsService.findById(groupId);
if (!group) {
return { error: 'Group not found' };
}
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Émettre l'événement de mise à jour à tous les clients dans la salle du groupe
this.webSocketService.emitToGroup(groupId, 'group:updated', {
groupId,
changes,
updatedBy: user.id,
});
// Émettre également l'événement au niveau du projet
this.webSocketService.emitToProject(group.projectId, 'group:updated', {
groupId,
changes,
updatedBy: user.id,
});
return { success: true };
} catch (error) {
this.logger.error(`Error updating group: ${error.message}`);
return { error: 'Failed to update group' };
}
}
@SubscribeMessage('group:addPerson')
async handleAddPersonToGroup(
@ConnectedSocket() client: Socket,
@MessageBody() data: { groupId: string, personId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { groupId, personId } = data;
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
const group = await this.groupsService.findById(groupId);
if (!group) {
return { error: 'Group not found' };
}
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Émettre l'événement d'ajout de personne à tous les clients dans la salle du groupe
this.webSocketService.emitToGroup(groupId, 'group:personAdded', {
groupId,
personId,
addedBy: user.id,
});
// Émettre également l'événement au niveau du projet
this.webSocketService.emitToProject(group.projectId, 'group:personAdded', {
groupId,
personId,
addedBy: user.id,
});
return { success: true };
} catch (error) {
this.logger.error(`Error adding person to group: ${error.message}`);
return { error: 'Failed to add person to group' };
}
}
@SubscribeMessage('group:removePerson')
async handleRemovePersonFromGroup(
@ConnectedSocket() client: Socket,
@MessageBody() data: { groupId: string, personId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { groupId, personId } = data;
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
const group = await this.groupsService.findById(groupId);
if (!group) {
return { error: 'Group not found' };
}
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
if (!hasAccess) {
return { error: 'Access denied' };
}
// Émettre l'événement de suppression de personne à tous les clients dans la salle du groupe
this.webSocketService.emitToGroup(groupId, 'group:personRemoved', {
groupId,
personId,
removedBy: user.id,
});
// Émettre également l'événement au niveau du projet
this.webSocketService.emitToProject(group.projectId, 'group:personRemoved', {
groupId,
personId,
removedBy: user.id,
});
return { success: true };
} catch (error) {
this.logger.error(`Error removing person from group: ${error.message}`);
return { error: 'Failed to remove person from group' };
}
}
}
5.4 Gateway Notifications
// src/modules/websockets/gateways/notifications.gateway.ts
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { BaseGateway } from './base.gateway';
import { WebSocketService } from '../services/websocket.service';
import { AuthService } from '../../auth/services/auth.service';
export class NotificationsGateway extends BaseGateway {
constructor(
protected readonly webSocketService: WebSocketService,
protected readonly authService: AuthService,
) {
super(webSocketService, authService);
}
@SubscribeMessage('notification:read')
handleReadNotification(
@ConnectedSocket() client: Socket,
@MessageBody() data: { notificationId: string },
) {
try {
const user = this.getUserFromSocket(client);
const { notificationId } = data;
// Logique pour marquer la notification comme lue
this.logger.log(`User ${user.id} read notification ${notificationId}`);
return { success: true };
} catch (error) {
this.logger.error(`Error reading notification: ${error.message}`);
return { error: 'Failed to read notification' };
}
}
// Méthode pour envoyer une notification à un utilisateur spécifique
sendNotificationToUser(userId: string, notification: any) {
this.webSocketService.emitToUser(userId, 'notification:new', notification);
}
// Méthode pour envoyer une notification à tous les utilisateurs d'un projet
sendNotificationToProject(projectId: string, notification: any) {
this.webSocketService.emitToProject(projectId, 'notification:new', notification);
}
}
6. Intégration avec les Services Existants
6.1 Service Projets
// src/modules/projects/services/projects.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { WebSocketService } from '../../websockets/services/websocket.service';
@Injectable()
export class ProjectsService {
constructor(
// Autres injections...
private readonly webSocketService: WebSocketService,
) {}
// Méthodes existantes...
async update(id: string, data: any, userId: string) {
// Logique de mise à jour du projet
// Notification en temps réel
this.webSocketService.emitToProject(id, 'project:updated', {
projectId: id,
changes: data,
updatedBy: userId,
});
return updatedProject;
}
async addCollaborator(projectId: string, collaboratorId: string, role: string, userId: string) {
// Logique d'ajout de collaborateur
// Notification en temps réel
this.webSocketService.emitToProject(projectId, 'project:collaboratorAdded', {
projectId,
collaboratorId,
role,
addedBy: userId,
});
// Notification personnelle au collaborateur
this.webSocketService.emitToUser(collaboratorId, 'notification:new', {
type: 'PROJECT_INVITATION',
projectId,
invitedBy: userId,
role,
});
return result;
}
}
6.2 Service Groupes
// src/modules/groups/services/groups.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { WebSocketService } from '../../websockets/services/websocket.service';
@Injectable()
export class GroupsService {
constructor(
// Autres injections...
private readonly webSocketService: WebSocketService,
) {}
// Méthodes existantes...
async create(data: any, userId: string) {
// Logique de création de groupe
// Notification en temps réel
this.webSocketService.emitToProject(data.projectId, 'group:created', {
groupId: createdGroup.id,
group: createdGroup,
createdBy: userId,
});
return createdGroup;
}
async addPerson(groupId: string, personId: string, userId: string) {
// Logique d'ajout de personne au groupe
const group = await this.findById(groupId);
// Notification en temps réel
this.webSocketService.emitToGroup(groupId, 'group:personAdded', {
groupId,
personId,
addedBy: userId,
});
this.webSocketService.emitToProject(group.projectId, 'group:personAdded', {
groupId,
personId,
addedBy: userId,
});
return result;
}
}
7. Intégration avec le Frontend
7.1 Configuration du Client Socket.IO
// frontend/lib/socket.ts
import { io, Socket } from 'socket.io-client';
import { useEffect, useState } from 'react';
let socket: Socket | null = null;
export const initializeSocket = (token: string) => {
if (socket) {
socket.disconnect();
}
socket = io(process.env.NEXT_PUBLIC_API_URL, {
auth: { token },
withCredentials: true,
});
return socket;
};
export const getSocket = () => {
return socket;
};
export const disconnectSocket = () => {
if (socket) {
socket.disconnect();
socket = null;
}
};
export const useSocket = (token: string | null) => {
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
if (!token) {
disconnectSocket();
setIsConnected(false);
return;
}
const socketInstance = initializeSocket(token);
socketInstance.on('connect', () => {
setIsConnected(true);
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
});
return () => {
socketInstance.off('connect');
socketInstance.off('disconnect');
};
}, [token]);
return { socket, isConnected };
};
7.2 Hook pour les Projets
// frontend/hooks/useProjectSocket.ts
import { useEffect, useState } from 'react';
import { getSocket } from '../lib/socket';
export const useProjectSocket = (projectId: string | null) => {
const [isJoined, setIsJoined] = useState(false);
const socket = getSocket();
useEffect(() => {
if (!socket || !projectId) {
setIsJoined(false);
return;
}
// Rejoindre la salle du projet
socket.emit('project:join', { projectId }, (response) => {
if (response.success) {
setIsJoined(true);
} else {
console.error('Failed to join project:', response.error);
}
});
// Quitter la salle du projet lors du démontage
return () => {
socket.emit('project:leave', { projectId });
setIsJoined(false);
};
}, [socket, projectId]);
const updateProject = (changes: any) => {
if (!socket || !projectId || !isJoined) {
return Promise.reject('Not connected to project');
}
return new Promise((resolve, reject) => {
socket.emit('project:update', { projectId, changes }, (response) => {
if (response.success) {
resolve(response);
} else {
reject(response.error);
}
});
});
};
return { isJoined, updateProject };
};
7.3 Hook pour les Groupes
// frontend/hooks/useGroupSocket.ts
import { useEffect, useState } from 'react';
import { getSocket } from '../lib/socket';
export const useGroupSocket = (groupId: string | null) => {
const [isJoined, setIsJoined] = useState(false);
const socket = getSocket();
useEffect(() => {
if (!socket || !groupId) {
setIsJoined(false);
return;
}
// Rejoindre la salle du groupe
socket.emit('group:join', { groupId }, (response) => {
if (response.success) {
setIsJoined(true);
} else {
console.error('Failed to join group:', response.error);
}
});
// Quitter la salle du groupe lors du démontage
return () => {
socket.emit('group:leave', { groupId });
setIsJoined(false);
};
}, [socket, groupId]);
const updateGroup = (changes: any) => {
if (!socket || !groupId || !isJoined) {
return Promise.reject('Not connected to group');
}
return new Promise((resolve, reject) => {
socket.emit('group:update', { groupId, changes }, (response) => {
if (response.success) {
resolve(response);
} else {
reject(response.error);
}
});
});
};
const addPersonToGroup = (personId: string) => {
if (!socket || !groupId || !isJoined) {
return Promise.reject('Not connected to group');
}
return new Promise((resolve, reject) => {
socket.emit('group:addPerson', { groupId, personId }, (response) => {
if (response.success) {
resolve(response);
} else {
reject(response.error);
}
});
});
};
const removePersonFromGroup = (personId: string) => {
if (!socket || !groupId || !isJoined) {
return Promise.reject('Not connected to group');
}
return new Promise((resolve, reject) => {
socket.emit('group:removePerson', { groupId, personId }, (response) => {
if (response.success) {
resolve(response);
} else {
reject(response.error);
}
});
});
};
return { isJoined, updateGroup, addPersonToGroup, removePersonFromGroup };
};
7.4 Hook pour les Notifications
// frontend/hooks/useNotifications.ts
import { useEffect, useState } from 'react';
import { getSocket } from '../lib/socket';
export const useNotifications = () => {
const [notifications, setNotifications] = useState([]);
const socket = getSocket();
useEffect(() => {
if (!socket) {
return;
}
// Écouter les nouvelles notifications
socket.on('notification:new', (notification) => {
setNotifications((prev) => [notification, ...prev]);
});
return () => {
socket.off('notification:new');
};
}, [socket]);
const markAsRead = (notificationId: string) => {
if (!socket) {