docs: add implementation plans for authentication and database schema

Documented comprehensive implementation plans for the authentication system and database schema, including architecture, module structure, API integration, security measures, and GDPR compliance details.
This commit is contained in:
Mathis H (Avnyr) 2025-05-15 13:10:00 +02:00
parent 9976cfeb7a
commit ef934a8599
6 changed files with 3246 additions and 0 deletions

890
AUTH_IMPLEMENTATION_PLAN.md Normal file
View File

@ -0,0 +1,890 @@
# Plan d'Implémentation de l'Authentification
Ce document détaille le plan d'implémentation du système d'authentification pour l'application de création de groupes, basé sur les spécifications du cahier des charges.
## 1. Vue d'Ensemble
L'application utilisera OAuth 2.0 avec GitHub comme fournisseur d'identité, combiné avec une gestion de session basée sur JWT (JSON Web Tokens). Cette approche offre plusieurs avantages :
- Délégation de l'authentification à un service tiers sécurisé (GitHub)
- Pas besoin de gérer les mots de passe des utilisateurs
- Récupération des informations de profil (nom, avatar) depuis l'API GitHub
- Authentification stateless avec JWT pour une meilleure scalabilité
## 2. Flux d'Authentification
```mermaid
sequenceDiagram
participant User as Utilisateur
participant Frontend as Frontend (Next.js)
participant Backend as Backend (NestJS)
participant GitHub as GitHub OAuth
User->>Frontend: Clic sur "Se connecter avec GitHub"
Frontend->>Backend: Redirection vers /auth/github
Backend->>GitHub: Redirection vers GitHub OAuth
GitHub->>User: Demande d'autorisation
User->>GitHub: Accepte l'autorisation
GitHub->>Backend: Redirection avec code d'autorisation
Backend->>GitHub: Échange code contre token d'accès
GitHub->>Backend: Retourne token d'accès
Backend->>GitHub: Requête informations utilisateur
GitHub->>Backend: Retourne informations utilisateur
Backend->>Backend: Crée/Met à jour l'utilisateur en BDD
Backend->>Backend: Génère JWT (access + refresh tokens)
Backend->>Frontend: Redirection avec tokens JWT
Frontend->>Frontend: Stocke les tokens
Frontend->>User: Affiche interface authentifiée
```
## 3. Structure des Modules
### 3.1 Module d'Authentification
```typescript
// src/modules/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthController } from './controllers/auth.controller';
import { AuthService } from './services/auth.service';
import { GithubStrategy } from './strategies/github.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';
import { UsersModule } from '../users/users.module';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_ACCESS_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
},
}),
}),
UsersModule,
],
controllers: [AuthController],
providers: [AuthService, GithubStrategy, JwtStrategy, JwtRefreshStrategy],
exports: [AuthService],
})
export class AuthModule {}
```
### 3.2 Stratégies d'Authentification
#### 3.2.1 Stratégie GitHub
```typescript
// src/modules/auth/strategies/github.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../services/auth.service';
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
constructor(
private configService: ConfigService,
private authService: AuthService,
) {
super({
clientID: configService.get<string>('GITHUB_CLIENT_ID'),
clientSecret: configService.get<string>('GITHUB_CLIENT_SECRET'),
callbackURL: configService.get<string>('GITHUB_CALLBACK_URL'),
scope: ['read:user'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
const { id, displayName, photos } = profile;
const user = await this.authService.validateGithubUser({
githubId: id,
name: displayName || `user_${id}`,
avatar: photos?.[0]?.value,
});
return user;
}
}
```
#### 3.2.2 Stratégie JWT
```typescript
// src/modules/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/services/users.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_ACCESS_SECRET'),
});
}
async validate(payload: JwtPayload) {
const { sub } = payload;
const user = await this.usersService.findById(sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
return user;
}
}
```
#### 3.2.3 Stratégie JWT Refresh
```typescript
// src/modules/auth/strategies/jwt-refresh.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { UsersService } from '../../users/services/users.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true,
});
}
async validate(req: Request, payload: JwtPayload) {
const refreshToken = req.headers.authorization?.replace('Bearer ', '');
if (!refreshToken) {
throw new UnauthorizedException('Refresh token not found');
}
const { sub } = payload;
const user = await this.usersService.findById(sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
return { ...user, refreshToken };
}
}
```
### 3.3 Service d'Authentification
```typescript
// src/modules/auth/services/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/services/users.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { TokensResponse } from '../interfaces/tokens-response.interface';
import { GithubUserDto } from '../dto/github-user.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async validateGithubUser(githubUserDto: GithubUserDto) {
const { githubId, name, avatar } = githubUserDto;
// Recherche de l'utilisateur par githubId
let user = await this.usersService.findByGithubId(githubId);
// Si l'utilisateur n'existe pas, on le crée
if (!user) {
user = await this.usersService.create({
githubId,
name,
avatar,
gdprTimestamp: new Date(),
});
} else {
// Mise à jour des informations de l'utilisateur
user = await this.usersService.update(user.id, {
name,
avatar,
});
}
return user;
}
async login(user: any): Promise<TokensResponse> {
const payload: JwtPayload = { sub: user.id };
const [accessToken, refreshToken] = await Promise.all([
this.generateAccessToken(payload),
this.generateRefreshToken(payload),
]);
return {
accessToken,
refreshToken,
expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
};
}
async refreshTokens(userId: string, refreshToken: string): Promise<TokensResponse> {
// Vérification du refresh token (à implémenter avec une table de tokens révoqués)
const payload: JwtPayload = { sub: userId };
const [accessToken, newRefreshToken] = await Promise.all([
this.generateAccessToken(payload),
this.generateRefreshToken(payload),
]);
return {
accessToken,
refreshToken: newRefreshToken,
expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
};
}
async logout(userId: string, refreshToken: string): Promise<void> {
// Ajouter le refresh token à la liste des tokens révoqués
// À implémenter avec une table de tokens révoqués
}
private async generateAccessToken(payload: JwtPayload): Promise<string> {
return this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
});
}
private async generateRefreshToken(payload: JwtPayload): Promise<string> {
return this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRATION', '7d'),
});
}
}
```
### 3.4 Contrôleur d'Authentification
```typescript
// src/modules/auth/controllers/auth.controller.ts
import { Controller, Get, Post, UseGuards, Req, Res, Body, HttpCode } from '@nestjs/common';
import { Response } from 'express';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../services/auth.service';
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
import { GetUser } from '../decorators/get-user.decorator';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { Public } from '../decorators/public.decorator';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Get('github')
@UseGuards(AuthGuard('github'))
githubAuth() {
// Cette route redirige vers GitHub pour l'authentification
}
@Public()
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubAuthCallback(@Req() req, @Res() res: Response) {
const { accessToken, refreshToken } = await this.authService.login(req.user);
// Redirection vers le frontend avec les tokens
const redirectUrl = `${this.configService.get<string>('FRONTEND_URL')}/auth/callback?accessToken=${accessToken}&refreshToken=${refreshToken}`;
return res.redirect(redirectUrl);
}
@Public()
@Post('refresh')
@UseGuards(JwtRefreshGuard)
@HttpCode(200)
async refreshTokens(
@GetUser('id') userId: string,
@GetUser('refreshToken') refreshToken: string,
) {
return this.authService.refreshTokens(userId, refreshToken);
}
@Post('logout')
@HttpCode(200)
async logout(
@GetUser('id') userId: string,
@Body() refreshTokenDto: RefreshTokenDto,
) {
await this.authService.logout(userId, refreshTokenDto.refreshToken);
return { message: 'Logout successful' };
}
@Get('profile')
getProfile(@GetUser() user) {
return user;
}
}
```
### 3.5 Guards et Décorateurs
#### 3.5.1 Guard JWT
```typescript
// src/modules/auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
```
#### 3.5.2 Guard JWT Refresh
```typescript
// src/modules/auth/guards/jwt-refresh.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {}
```
#### 3.5.3 Décorateur Public
```typescript
// src/modules/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
```
#### 3.5.4 Décorateur GetUser
```typescript
// src/modules/auth/decorators/get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
```
### 3.6 Interfaces et DTOs
#### 3.6.1 Interface JwtPayload
```typescript
// src/modules/auth/interfaces/jwt-payload.interface.ts
export interface JwtPayload {
sub: string;
iat?: number;
exp?: number;
}
```
#### 3.6.2 Interface TokensResponse
```typescript
// src/modules/auth/interfaces/tokens-response.interface.ts
export interface TokensResponse {
accessToken: string;
refreshToken: string;
expiresIn: string;
}
```
#### 3.6.3 DTO GithubUser
```typescript
// src/modules/auth/dto/github-user.dto.ts
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
export class GithubUserDto {
@IsString()
@IsNotEmpty()
githubId: string;
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
avatar?: string;
}
```
#### 3.6.4 DTO RefreshToken
```typescript
// src/modules/auth/dto/refresh-token.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
export class RefreshTokenDto {
@IsString()
@IsNotEmpty()
refreshToken: string;
}
```
## 4. Configuration Globale de l'Authentification
### 4.1 Configuration du Module App
```typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { DatabaseModule } from './database/database.module';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { validate } from './config/env.validation';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validate,
}),
DatabaseModule,
AuthModule,
UsersModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
```
### 4.2 Configuration CORS dans main.ts
```typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
// Configuration globale des pipes de validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// Configuration CORS
app.enableCors({
origin: configService.get<string>('CORS_ORIGIN'),
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
// Préfixe global pour les routes API
app.setGlobalPrefix(configService.get<string>('API_PREFIX', 'api'));
const port = configService.get<number>('PORT', 3000);
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();
```
## 5. Sécurité et Bonnes Pratiques
### 5.1 Gestion des Tokens
- **Access Token** : Durée de vie courte (15 minutes) pour limiter les risques en cas de vol
- **Refresh Token** : Durée de vie plus longue (7 jours) pour permettre le rafraîchissement de l'access token
- Stockage sécurisé des tokens côté client (localStorage pour l'access token, httpOnly cookie pour le refresh token dans une implémentation plus sécurisée)
- Révocation des tokens en cas de déconnexion ou de suspicion de compromission
### 5.2 Protection contre les Attaques Courantes
- **CSRF** : Utilisation de tokens anti-CSRF pour les opérations sensibles
- **XSS** : Échappement des données utilisateur, utilisation de Content Security Policy
- **Injection** : Validation des entrées avec class-validator, utilisation de paramètres préparés avec DrizzleORM
- **Rate Limiting** : Limitation du nombre de requêtes d'authentification pour prévenir les attaques par force brute
### 5.3 Conformité RGPD
- Enregistrement du timestamp d'acceptation RGPD lors de la création du compte
- Possibilité d'exporter les données personnelles
- Possibilité de supprimer le compte et toutes les données associées
- Renouvellement du consentement tous les 13 mois
## 6. Intégration avec le Frontend
### 6.1 Flux d'Authentification côté Frontend
```typescript
// Exemple de code pour le frontend (Next.js)
// app/auth/github/page.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { API_URL } from '@/lib/constants';
export default function GitHubAuthPage() {
const router = useRouter();
useEffect(() => {
// Redirection vers l'endpoint d'authentification GitHub du backend
window.location.href = `${API_URL}/auth/github`;
}, []);
return <div>Redirection vers GitHub pour authentification...</div>;
}
```
```typescript
// app/auth/callback/page.tsx
'use client';
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
export default function AuthCallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { login } = useAuth();
useEffect(() => {
const accessToken = searchParams.get('accessToken');
const refreshToken = searchParams.get('refreshToken');
if (accessToken && refreshToken) {
// Stockage des tokens
login(accessToken, refreshToken);
// Redirection vers le dashboard
router.push('/dashboard');
} else {
// Erreur d'authentification
router.push('/auth/error');
}
}, [searchParams, login, router]);
return <div>Finalisation de l'authentification...</div>;
}
```
### 6.2 Hook d'Authentification
```typescript
// hooks/useAuth.tsx
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { jwtDecode } from 'jwt-decode';
import { API_URL } from '@/lib/constants';
interface AuthContextType {
isAuthenticated: boolean;
user: any | null;
login: (accessToken: string, refreshToken: string) => void;
logout: () => Promise<void>;
refreshAccessToken: () => Promise<string | null>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [user, setUser] = useState<any | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
useEffect(() => {
// Récupération des tokens depuis le localStorage au chargement
const storedAccessToken = localStorage.getItem('accessToken');
const storedRefreshToken = localStorage.getItem('refreshToken');
if (storedAccessToken && storedRefreshToken) {
try {
// Vérification de l'expiration du token
const decoded = jwtDecode(storedAccessToken);
const currentTime = Date.now() / 1000;
if (decoded.exp && decoded.exp > currentTime) {
setAccessToken(storedAccessToken);
setRefreshToken(storedRefreshToken);
setIsAuthenticated(true);
fetchUserProfile(storedAccessToken);
} else {
// Token expiré, tentative de rafraîchissement
refreshTokens(storedRefreshToken);
}
} catch (error) {
// Token invalide, nettoyage
clearTokens();
}
}
}, []);
const fetchUserProfile = async (token: string) => {
try {
const response = await fetch(`${API_URL}/auth/profile`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
throw new Error('Failed to fetch user profile');
}
} catch (error) {
console.error('Error fetching user profile:', error);
}
};
const refreshTokens = async (token: string) => {
try {
const response = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = await response.json();
setAccessToken(newAccessToken);
setRefreshToken(newRefreshToken);
setIsAuthenticated(true);
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
fetchUserProfile(newAccessToken);
return newAccessToken;
} else {
throw new Error('Failed to refresh tokens');
}
} catch (error) {
console.error('Error refreshing tokens:', error);
clearTokens();
return null;
}
};
const login = (newAccessToken: string, newRefreshToken: string) => {
setAccessToken(newAccessToken);
setRefreshToken(newRefreshToken);
setIsAuthenticated(true);
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
fetchUserProfile(newAccessToken);
};
const logout = async () => {
if (refreshToken) {
try {
await fetch(`${API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ refreshToken }),
});
} catch (error) {
console.error('Error during logout:', error);
}
}
clearTokens();
};
const clearTokens = () => {
setAccessToken(null);
setRefreshToken(null);
setIsAuthenticated(false);
setUser(null);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
};
const refreshAccessToken = async () => {
if (refreshToken) {
return refreshTokens(refreshToken);
}
return null;
};
return (
<AuthContext.Provider
value={{
isAuthenticated,
user,
login,
logout,
refreshAccessToken,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
```
### 6.3 Client API avec Gestion des Tokens
```typescript
// lib/api-client.ts
import { useAuth } from '@/hooks/useAuth';
import { API_URL } from './constants';
export function useApiClient() {
const { isAuthenticated, refreshAccessToken } = useAuth();
const fetchWithAuth = async (endpoint: string, options: RequestInit = {}) => {
if (!isAuthenticated) {
throw new Error('User not authenticated');
}
const accessToken = localStorage.getItem('accessToken');
const headers = {
...options.headers,
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
try {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers,
});
// Si le token est expiré (401), on tente de le rafraîchir
if (response.status === 401) {
const newAccessToken = await refreshAccessToken();
if (newAccessToken) {
// Nouvelle tentative avec le token rafraîchi
return fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
Authorization: `Bearer ${newAccessToken}`,
},
});
} else {
throw new Error('Failed to refresh access token');
}
}
return response;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
};
return { fetchWithAuth };
}
```
## 7. Tests
### 7.1 Tests Unitaires
```typescript
// src/modules/auth/services/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { UsersService } from

View File

@ -0,0 +1,223 @@
# Plan d'Implémentation du Backend
Ce document détaille le plan d'implémentation du backend pour l'application de création de groupes, basé sur les spécifications du cahier des charges.
## 1. Structure des Dossiers
```
backend/
├── src/
│ ├── main.ts # Point d'entrée de l'application
│ ├── app.module.ts # Module principal
│ ├── config/ # Configuration de l'application
│ │ ├── app.config.ts # Configuration générale
│ │ ├── database.config.ts # Configuration de la base de données
│ │ ├── auth.config.ts # Configuration de l'authentification
│ │ └── env.validation.ts # Validation des variables d'environnement
│ ├── common/ # Utilitaires partagés
│ │ ├── decorators/ # Décorateurs personnalisés
│ │ ├── filters/ # Filtres d'exception
│ │ ├── guards/ # Guards d'authentification et d'autorisation
│ │ ├── interceptors/ # Intercepteurs
│ │ ├── pipes/ # Pipes de validation
│ │ └── utils/ # Fonctions utilitaires
│ ├── modules/ # Modules fonctionnels
│ │ ├── auth/ # Module d'authentification
│ │ │ ├── controllers/ # Contrôleurs d'authentification
│ │ │ ├── services/ # Services d'authentification
│ │ │ ├── guards/ # Guards spécifiques à l'authentification
│ │ │ ├── strategies/ # Stratégies d'authentification (GitHub OAuth)
│ │ │ └── auth.module.ts # Module d'authentification
│ │ ├── users/ # Module de gestion des utilisateurs
│ │ │ ├── controllers/ # Contrôleurs utilisateurs
│ │ │ ├── services/ # Services utilisateurs
│ │ │ ├── dto/ # Objets de transfert de données
│ │ │ └── users.module.ts # Module utilisateurs
│ │ ├── projects/ # Module de gestion des projets
│ │ │ ├── controllers/ # Contrôleurs projets
│ │ │ ├── services/ # Services projets
│ │ │ ├── dto/ # Objets de transfert de données
│ │ │ └── projects.module.ts # Module projets
│ │ ├── persons/ # Module de gestion des personnes
│ │ │ ├── controllers/ # Contrôleurs personnes
│ │ │ ├── services/ # Services personnes
│ │ │ ├── dto/ # Objets de transfert de données
│ │ │ └── persons.module.ts # Module personnes
│ │ ├── groups/ # Module de gestion des groupes
│ │ │ ├── controllers/ # Contrôleurs groupes
│ │ │ ├── services/ # Services groupes
│ │ │ ├── dto/ # Objets de transfert de données
│ │ │ └── groups.module.ts # Module groupes
│ │ ├── tags/ # Module de gestion des tags
│ │ │ ├── controllers/ # Contrôleurs tags
│ │ │ ├── services/ # Services tags
│ │ │ ├── dto/ # Objets de transfert de données
│ │ │ └── tags.module.ts # Module tags
│ │ └── websockets/ # Module de gestion des WebSockets
│ │ ├── gateways/ # Gateways WebSocket
│ │ ├── events/ # Définitions des événements
│ │ └── websockets.module.ts # Module WebSockets
│ └── database/ # Configuration de la base de données
│ ├── migrations/ # Migrations de base de données
│ ├── schema/ # Schéma de base de données (DrizzleORM)
│ └── database.module.ts # Module de base de données
├── test/ # Tests
│ ├── e2e/ # Tests end-to-end
│ └── unit/ # Tests unitaires
└── .env.example # Exemple de fichier d'environnement
```
## 2. Dépendances à Ajouter
```bash
# Dépendances principales
pnpm add @nestjs/config @nestjs/passport passport passport-github2 @nestjs/jwt
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
pnpm add drizzle-orm pg
pnpm add @node-rs/argon2 jose
pnpm add class-validator class-transformer
pnpm add zod zod-validation-error
pnpm add uuid
# Dépendances de développement
pnpm add -D drizzle-kit
pnpm add -D @types/passport-github2 @types/socket.io @types/pg @types/uuid
```
## 3. Configuration de l'Environnement
Créer un fichier `.env.example` avec les variables suivantes :
```
# Application
PORT=3000
NODE_ENV=development
API_PREFIX=api
# Database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/groupmaker
# Authentication
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
# JWT
JWT_ACCESS_SECRET=your_access_token_secret
JWT_REFRESH_SECRET=your_refresh_token_secret
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
# CORS
CORS_ORIGIN=http://localhost:3000
```
## 4. Étapes d'Implémentation
### 4.1 Configuration de Base
1. **Configuration de l'Application**
- Mettre à jour `main.ts` pour inclure la configuration CORS, les préfixes d'API, et les pipes de validation globaux
- Créer un module de configuration pour charger les variables d'environnement avec validation
2. **Configuration de la Base de Données**
- Configurer DrizzleORM avec PostgreSQL
- Définir le schéma de base de données selon le modèle de données spécifié
- Mettre en place les migrations de base de données
### 4.2 Authentification et Autorisation
1. **Authentification GitHub OAuth**
- Implémenter la stratégie d'authentification GitHub
- Créer les endpoints d'authentification (login, callback, refresh, logout)
- Mettre en place la gestion des JWT (génération, validation, rafraîchissement)
2. **Autorisation RBAC**
- Implémenter les guards pour la vérification des rôles
- Créer des décorateurs pour les rôles et les permissions
- Mettre en place la logique de vérification des autorisations
### 4.3 Modules Fonctionnels
1. **Module Utilisateurs**
- Implémenter les opérations CRUD pour les utilisateurs
- Gérer les profils utilisateurs et les préférences
- Implémenter la logique de consentement RGPD
2. **Module Projets**
- Implémenter les opérations CRUD pour les projets
- Gérer les relations avec les utilisateurs, les personnes et les groupes
- Implémenter la logique de partage de projets
3. **Module Personnes**
- Implémenter les opérations CRUD pour les personnes
- Gérer les attributs des personnes (niveau technique, genre, âge, etc.)
- Implémenter la logique d'association avec les tags
4. **Module Groupes**
- Implémenter les opérations CRUD pour les groupes
- Développer les algorithmes de création automatique de groupes équilibrés
- Gérer les relations avec les personnes
5. **Module Tags**
- Implémenter les opérations CRUD pour les tags
- Gérer les types de tags (PROJECT, PERSON)
- Implémenter la logique d'association avec les projets et les personnes
### 4.4 Communication en Temps Réel
1. **WebSockets avec SocketIO**
- Configurer les gateways WebSocket
- Implémenter les événements pour les mises à jour en temps réel
- Gérer les salles pour les projets collaboratifs
### 4.5 Sécurité et Conformité RGPD
1. **Sécurité**
- Implémenter le hachage des mots de passe avec @node-rs/argon2
- Mettre en place des protections contre les attaques courantes (CSRF, XSS, injections SQL)
- Configurer le rate limiting pour prévenir les attaques par force brute
2. **Conformité RGPD**
- Implémenter les fonctionnalités d'export des données personnelles
- Mettre en place la logique de suppression de compte
- Gérer les consentements utilisateurs et leur renouvellement
### 4.6 Tests et Documentation
1. **Tests**
- Écrire des tests unitaires pour les services et les contrôleurs
- Développer des tests e2e pour les API
- Mettre en place des tests d'intégration pour les modules critiques
2. **Documentation**
- Générer la documentation API avec Swagger
- Documenter les endpoints, les modèles de données et les paramètres
- Fournir des exemples d'utilisation des API
## 5. Calendrier d'Implémentation
1. **Semaine 1: Configuration et Base de Données**
- Configuration de l'environnement
- Mise en place de la base de données avec DrizzleORM
- Définition du schéma et création des migrations
2. **Semaine 2: Authentification et Utilisateurs**
- Implémentation de l'authentification GitHub OAuth
- Développement du module utilisateurs
- Mise en place de la gestion des JWT
3. **Semaine 3: Modules Principaux**
- Développement des modules projets, personnes et groupes
- Implémentation des opérations CRUD
- Mise en place des relations entre entités
4. **Semaine 4: Fonctionnalités Avancées**
- Implémentation des WebSockets pour la communication en temps réel
- Développement des algorithmes de création de groupes
- Mise en place des fonctionnalités de sécurité et de conformité RGPD
5. **Semaine 5: Tests et Finalisation**
- Écriture des tests unitaires et e2e
- Documentation de l'API
- Optimisation des performances et correction des bugs

488
DATABASE_SCHEMA_PLAN.md Normal file
View File

@ -0,0 +1,488 @@
# Plan d'Implémentation du Schéma de Base de Données
Ce document détaille le plan d'implémentation du schéma de base de données pour l'application de création de groupes, basé sur le modèle de données spécifié dans le cahier des charges.
## 1. Schéma DrizzleORM
Le schéma sera implémenté en utilisant DrizzleORM avec PostgreSQL. Voici la définition des tables et leurs relations.
### 1.1 Table `users`
```typescript
import { pgTable, uuid, varchar, text, timestamp, jsonb } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), // UUIDv7 pour l'ordre chronologique
name: varchar('name', { length: 100 }).notNull(),
avatar: text('avatar'), // URL depuis l'API Github
githubId: varchar('githubId', { length: 50 }).notNull().unique(),
gdprTimestamp: timestamp('gdprTimestamp', { withTimezone: true }),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull(),
metadata: jsonb('metadata').default({})
}, (table) => {
return {
githubIdIdx: index('githubId_idx').on(table.githubId),
createdAtIdx: index('createdAt_idx').on(table.createdAt)
};
});
```
### 1.2 Table `projects`
```typescript
import { pgTable, uuid, varchar, text, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core';
import { users } from './users';
export const projects = pgTable('projects', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 100 }).notNull(),
description: text('description'),
ownerId: uuid('ownerId').notNull().references(() => users.id, { onDelete: 'cascade' }),
settings: jsonb('settings').default({}),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
nameIdx: index('name_idx').on(table.name),
ownerIdIdx: index('ownerId_idx').on(table.ownerId),
createdAtIdx: index('createdAt_idx').on(table.createdAt)
};
});
```
### 1.3 Enum `gender`
```typescript
export const gender = pgEnum('gender', ['MALE', 'FEMALE', 'NON_BINARY']);
```
### 1.4 Enum `oralEaseLevel`
```typescript
export const oralEaseLevel = pgEnum('oralEaseLevel', ['SHY', 'RESERVED', 'COMFORTABLE']);
```
### 1.5 Table `persons`
```typescript
import { pgTable, uuid, varchar, smallint, boolean, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { gender, oralEaseLevel } from './enums';
export const persons = pgTable('persons', {
id: uuid('id').primaryKey().defaultRandom(),
firstName: varchar('firstName', { length: 50 }).notNull(),
lastName: varchar('lastName', { length: 50 }).notNull(),
gender: gender('gender').notNull(),
technicalLevel: smallint('technicalLevel').notNull(),
hasTechnicalTraining: boolean('hasTechnicalTraining').notNull().default(false),
frenchSpeakingLevel: smallint('frenchSpeakingLevel').notNull(),
oralEaseLevel: oralEaseLevel('oralEaseLevel').notNull(),
age: smallint('age'),
projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }),
attributes: jsonb('attributes').default({}),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
firstNameIdx: index('firstName_idx').on(table.firstName),
lastNameIdx: index('lastName_idx').on(table.lastName),
projectIdIdx: index('projectId_idx').on(table.projectId),
nameCompositeIdx: index('name_composite_idx').on(table.firstName, table.lastName)
};
});
```
### 1.6 Table `groups`
```typescript
import { pgTable, uuid, varchar, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core';
import { projects } from './projects';
export const groups = pgTable('groups', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 100 }).notNull(),
projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }),
metadata: jsonb('metadata').default({}),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
nameIdx: index('name_idx').on(table.name),
projectIdIdx: index('projectId_idx').on(table.projectId)
};
});
```
### 1.7 Enum `tagType`
```typescript
export const tagType = pgEnum('tagType', ['PROJECT', 'PERSON']);
```
### 1.8 Table `tags`
```typescript
import { pgTable, uuid, varchar, timestamp, foreignKey } from 'drizzle-orm/pg-core';
import { tagType } from './enums';
export const tags = pgTable('tags', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 50 }).notNull(),
color: varchar('color', { length: 7 }).notNull(),
type: tagType('type').notNull(),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
nameIdx: index('name_idx').on(table.name),
typeIdx: index('type_idx').on(table.type)
};
});
```
### 1.9 Table `personToGroup` (Relation)
```typescript
import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { groups } from './groups';
export const personToGroup = pgTable('person_to_group', {
id: uuid('id').primaryKey().defaultRandom(),
personId: uuid('personId').notNull().references(() => persons.id, { onDelete: 'cascade' }),
groupId: uuid('groupId').notNull().references(() => groups.id, { onDelete: 'cascade' }),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
personIdIdx: index('personId_idx').on(table.personId),
groupIdIdx: index('groupId_idx').on(table.groupId),
personGroupUniqueIdx: uniqueIndex('person_group_unique_idx').on(table.personId, table.groupId)
};
});
```
### 1.10 Table `personToTag` (Relation)
```typescript
import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { tags } from './tags';
export const personToTag = pgTable('person_to_tag', {
id: uuid('id').primaryKey().defaultRandom(),
personId: uuid('personId').notNull().references(() => persons.id, { onDelete: 'cascade' }),
tagId: uuid('tagId').notNull().references(() => tags.id, { onDelete: 'cascade' }),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
personIdIdx: index('personId_idx').on(table.personId),
tagIdIdx: index('tagId_idx').on(table.tagId),
personTagUniqueIdx: uniqueIndex('person_tag_unique_idx').on(table.personId, table.tagId)
};
});
```
### 1.11 Table `projectToTag` (Relation)
```typescript
import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { tags } from './tags';
export const projectToTag = pgTable('project_to_tag', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }),
tagId: uuid('tagId').notNull().references(() => tags.id, { onDelete: 'cascade' }),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
projectIdIdx: index('projectId_idx').on(table.projectId),
tagIdIdx: index('tagId_idx').on(table.tagId),
projectTagUniqueIdx: uniqueIndex('project_tag_unique_idx').on(table.projectId, table.tagId)
};
});
```
## 2. Relations et Types
### 2.1 Relations
```typescript
// Définition des relations pour les requêtes
export const relations = {
users: {
projects: one(users, {
fields: [users.id],
references: [projects.ownerId],
}),
},
projects: {
owner: many(projects, {
fields: [projects.ownerId],
references: [users.id],
}),
persons: one(projects, {
fields: [projects.id],
references: [persons.projectId],
}),
groups: one(projects, {
fields: [projects.id],
references: [groups.projectId],
}),
tags: many(projects, {
through: {
table: projectToTag,
fields: [projectToTag.projectId, projectToTag.tagId],
references: [projects.id, tags.id],
},
}),
},
persons: {
project: many(persons, {
fields: [persons.projectId],
references: [projects.id],
}),
group: many(persons, {
through: {
table: personToGroup,
fields: [personToGroup.personId, personToGroup.groupId],
references: [persons.id, groups.id],
},
}),
tags: many(persons, {
through: {
table: personToTag,
fields: [personToTag.personId, personToTag.tagId],
references: [persons.id, tags.id],
},
}),
},
groups: {
project: many(groups, {
fields: [groups.projectId],
references: [projects.id],
}),
persons: many(groups, {
through: {
table: personToGroup,
fields: [personToGroup.groupId, personToGroup.personId],
references: [groups.id, persons.id],
},
}),
},
tags: {
persons: many(tags, {
through: {
table: personToTag,
fields: [personToTag.tagId, personToTag.personId],
references: [tags.id, persons.id],
},
}),
projects: many(tags, {
through: {
table: projectToTag,
fields: [projectToTag.tagId, projectToTag.projectId],
references: [tags.id, projects.id],
},
}),
},
};
```
### 2.2 Types Inférés
```typescript
// Types inférés à partir du schéma
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
export type Person = typeof persons.$inferSelect;
export type NewPerson = typeof persons.$inferInsert;
export type Group = typeof groups.$inferSelect;
export type NewGroup = typeof groups.$inferInsert;
export type Tag = typeof tags.$inferSelect;
export type NewTag = typeof tags.$inferInsert;
export type PersonToGroup = typeof personToGroup.$inferSelect;
export type NewPersonToGroup = typeof personToGroup.$inferInsert;
export type PersonToTag = typeof personToTag.$inferSelect;
export type NewPersonToTag = typeof personToTag.$inferInsert;
export type ProjectToTag = typeof projectToTag.$inferSelect;
export type NewProjectToTag = typeof projectToTag.$inferInsert;
```
## 3. Migrations
### 3.1 Configuration de Drizzle Kit
Créer un fichier `drizzle.config.ts` à la racine du projet backend :
```typescript
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config();
export default {
schema: './src/database/schema/*.ts',
out: './src/database/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/groupmaker',
},
verbose: true,
strict: true,
} satisfies Config;
```
### 3.2 Scripts pour les Migrations
Ajouter les scripts suivants au `package.json` du backend :
```json
{
"scripts": {
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "ts-node src/database/migrate.ts",
"db:studio": "drizzle-kit studio"
}
}
```
### 3.3 Script de Migration
Créer un fichier `src/database/migrate.ts` :
```typescript
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
import * as dotenv from 'dotenv';
dotenv.config();
const main = async () => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const db = drizzle(pool);
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './src/database/migrations' });
console.log('Migrations completed successfully');
await pool.end();
};
main().catch((err) => {
console.error('Migration failed');
console.error(err);
process.exit(1);
});
```
## 4. Module de Base de Données
### 4.1 Module Database
Créer un fichier `src/database/database.module.ts` :
```typescript
import { Module, Global } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';
export const DATABASE_POOL = 'DATABASE_POOL';
export const DRIZZLE = 'DRIZZLE';
@Global()
@Module({
imports: [ConfigModule],
providers: [
{
provide: DATABASE_POOL,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const pool = new Pool({
connectionString: configService.get<string>('DATABASE_URL'),
});
// Test the connection
const client = await pool.connect();
try {
await client.query('SELECT NOW()');
console.log('Database connection established successfully');
} finally {
client.release();
}
return pool;
},
},
{
provide: DRIZZLE,
inject: [DATABASE_POOL],
useFactory: (pool: Pool) => {
return drizzle(pool, { schema });
},
},
],
exports: [DATABASE_POOL, DRIZZLE],
})
export class DatabaseModule {}
```
### 4.2 Index des Schémas
Créer un fichier `src/database/schema/index.ts` pour exporter tous les schémas :
```typescript
export * from './users';
export * from './projects';
export * from './persons';
export * from './groups';
export * from './tags';
export * from './personToGroup';
export * from './personToTag';
export * from './projectToTag';
export * from './enums';
export * from './relations';
```
## 5. Stratégie d'Indexation
Les index suivants seront créés pour optimiser les performances des requêtes :
1. **Index Primaires** : Sur toutes les clés primaires (UUIDv7)
2. **Index Secondaires** : Sur les clés étrangères pour accélérer les jointures
3. **Index Composites** : Sur les champs fréquemment utilisés ensemble dans les requêtes
4. **Index Partiels** : Pour les requêtes filtrées fréquentes
5. **Index de Texte** : Pour les recherches sur les champs textuels (noms, descriptions)
## 6. Optimisation des Formats de Données
Les types de données PostgreSQL seront optimisés pour chaque cas d'usage :
1. **UUID** : Pour les identifiants (UUIDv7 pour l'ordre chronologique)
2. **JSONB** : Pour les données flexibles et semi-structurées (metadata, settings, attributes)
3. **ENUM** : Types PostgreSQL natifs pour les valeurs fixes (gender, oralEaseLevel, tagType)
4. **VARCHAR** : Avec contraintes pour les chaînes de caractères variables
5. **TIMESTAMP WITH TIME ZONE** : Pour les dates avec gestion des fuseaux horaires
6. **SMALLINT** : Pour les valeurs numériques entières de petite taille (technicalLevel, age)
7. **BOOLEAN** : Pour les valeurs booléennes (hasTechnicalTraining)
Ces optimisations permettront d'améliorer les performances des requêtes, de réduire l'empreinte mémoire et d'assurer l'intégrité des données.

761
IMPLEMENTATION_GUIDE.md Normal file
View File

@ -0,0 +1,761 @@
# Guide d'Implémentation du Backend
Ce document présente un guide complet pour l'implémentation du backend de l'application de création de groupes, basé sur les spécifications du cahier des charges et les plans détaillés précédemment établis.
## Table des Matières
1. [Vue d'Ensemble](#1-vue-densemble)
2. [Préparation de l'Environnement](#2-préparation-de-lenvironnement)
3. [Structure du Projet](#3-structure-du-projet)
4. [Configuration de Base](#4-configuration-de-base)
5. [Base de Données](#5-base-de-données)
6. [Authentification](#6-authentification)
7. [Modules Fonctionnels](#7-modules-fonctionnels)
8. [Communication en Temps Réel](#8-communication-en-temps-réel)
9. [Sécurité et Conformité RGPD](#9-sécurité-et-conformité-rgpd)
10. [Tests et Documentation](#10-tests-et-documentation)
11. [Déploiement](#11-déploiement)
12. [Calendrier d'Implémentation](#12-calendrier-dimplémentation)
## 1. Vue d'Ensemble
L'application est une plateforme de création et de gestion de groupes qui permet aux utilisateurs de créer des groupes en prenant en compte divers paramètres et de conserver un historique des groupes précédemment créés.
### 1.1 Architecture Globale
L'application suit une architecture monorepo avec séparation claire entre le frontend et le backend :
- **Frontend** : Application Next.js avec App Router et Server Components
- **Backend** : API NestJS avec PostgreSQL et DrizzleORM
- **Communication** : API REST pour les opérations CRUD et WebSockets pour les mises à jour en temps réel
- **Authentification** : OAuth 2.0 avec GitHub et JWT pour la gestion des sessions
## 2. Préparation de l'Environnement
### 2.1 Installation des Dépendances
```bash
# Installation des dépendances principales
pnpm add @nestjs/config @nestjs/passport passport passport-github2 @nestjs/jwt
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
pnpm add drizzle-orm pg
pnpm add @node-rs/argon2 jose
pnpm add class-validator class-transformer
pnpm add zod zod-validation-error
pnpm add uuid
# Installation des dépendances de développement
pnpm add -D drizzle-kit
pnpm add -D @types/passport-github2 @types/socket.io @types/pg @types/uuid
```
### 2.2 Configuration de l'Environnement
Créer un fichier `.env.example` à la racine du projet backend :
```
# Application
PORT=3000
NODE_ENV=development
API_PREFIX=api
# Database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/groupmaker
# Authentication
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
# JWT
JWT_ACCESS_SECRET=your_access_token_secret
JWT_REFRESH_SECRET=your_refresh_token_secret
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
# CORS
CORS_ORIGIN=http://localhost:3000
FRONTEND_URL=http://localhost:3000
```
## 3. Structure du Projet
La structure du projet backend suivra l'organisation suivante :
```
backend/
├── src/
│ ├── main.ts # Point d'entrée de l'application
│ ├── app.module.ts # Module principal
│ ├── config/ # Configuration de l'application
│ │ ├── app.config.ts # Configuration générale
│ │ ├── database.config.ts # Configuration de la base de données
│ │ ├── auth.config.ts # Configuration de l'authentification
│ │ └── env.validation.ts # Validation des variables d'environnement
│ ├── common/ # Utilitaires partagés
│ │ ├── decorators/ # Décorateurs personnalisés
│ │ ├── filters/ # Filtres d'exception
│ │ ├── guards/ # Guards d'authentification et d'autorisation
│ │ ├── interceptors/ # Intercepteurs
│ │ ├── pipes/ # Pipes de validation
│ │ └── utils/ # Fonctions utilitaires
│ ├── modules/ # Modules fonctionnels
│ │ ├── auth/ # Module d'authentification
│ │ ├── users/ # Module de gestion des utilisateurs
│ │ ├── projects/ # Module de gestion des projets
│ │ ├── persons/ # Module de gestion des personnes
│ │ ├── groups/ # Module de gestion des groupes
│ │ ├── tags/ # Module de gestion des tags
│ │ └── websockets/ # Module de gestion des WebSockets
│ └── database/ # Configuration de la base de données
│ ├── migrations/ # Migrations de base de données
│ ├── schema/ # Schéma de base de données (DrizzleORM)
│ └── database.module.ts # Module de base de données
├── test/ # Tests
│ ├── e2e/ # Tests end-to-end
│ └── unit/ # Tests unitaires
└── .env.example # Exemple de fichier d'environnement
```
## 4. Configuration de Base
### 4.1 Point d'Entrée de l'Application
Mettre à jour le fichier `src/main.ts` :
```typescript
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
// Configuration globale des pipes de validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// Configuration CORS
app.enableCors({
origin: configService.get<string>('CORS_ORIGIN'),
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
// Préfixe global pour les routes API
app.setGlobalPrefix(configService.get<string>('API_PREFIX', 'api'));
const port = configService.get<number>('PORT', 3000);
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();
```
### 4.2 Module Principal
Mettre à jour le fichier `src/app.module.ts` :
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './database/database.module';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { ProjectsModule } from './modules/projects/projects.module';
import { PersonsModule } from './modules/persons/persons.module';
import { GroupsModule } from './modules/groups/groups.module';
import { TagsModule } from './modules/tags/tags.module';
import { WebSocketsModule } from './modules/websockets/websockets.module';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { validate } from './config/env.validation';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validate,
}),
DatabaseModule,
AuthModule,
UsersModule,
ProjectsModule,
PersonsModule,
GroupsModule,
TagsModule,
WebSocketsModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
```
### 4.3 Validation des Variables d'Environnement
Créer le fichier `src/config/env.validation.ts` :
```typescript
import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, IsString, validateSync } from 'class-validator';
enum Environment {
Development = 'development',
Production = 'production',
Test = 'test',
}
class EnvironmentVariables {
@IsEnum(Environment)
NODE_ENV: Environment;
@IsNumber()
PORT: number;
@IsString()
API_PREFIX: string;
@IsString()
DATABASE_URL: string;
@IsString()
GITHUB_CLIENT_ID: string;
@IsString()
GITHUB_CLIENT_SECRET: string;
@IsString()
GITHUB_CALLBACK_URL: string;
@IsString()
JWT_ACCESS_SECRET: string;
@IsString()
JWT_REFRESH_SECRET: string;
@IsString()
JWT_ACCESS_EXPIRATION: string;
@IsString()
JWT_REFRESH_EXPIRATION: string;
@IsString()
CORS_ORIGIN: string;
@IsString()
FRONTEND_URL: string;
}
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToClass(
EnvironmentVariables,
{
...config,
PORT: config.PORT ? parseInt(config.PORT as string, 10) : 3000,
},
{ enableImplicitConversion: true },
);
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
```
## 5. Base de Données
### 5.1 Configuration de DrizzleORM
Créer le fichier `drizzle.config.ts` à la racine du projet backend :
```typescript
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config();
export default {
schema: './src/database/schema/*.ts',
out: './src/database/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/groupmaker',
},
verbose: true,
strict: true,
} satisfies Config;
```
### 5.2 Module de Base de Données
Créer le fichier `src/database/database.module.ts` :
```typescript
import { Module, Global } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';
export const DATABASE_POOL = 'DATABASE_POOL';
export const DRIZZLE = 'DRIZZLE';
@Global()
@Module({
imports: [ConfigModule],
providers: [
{
provide: DATABASE_POOL,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const pool = new Pool({
connectionString: configService.get<string>('DATABASE_URL'),
});
// Test the connection
const client = await pool.connect();
try {
await client.query('SELECT NOW()');
console.log('Database connection established successfully');
} finally {
client.release();
}
return pool;
},
},
{
provide: DRIZZLE,
inject: [DATABASE_POOL],
useFactory: (pool: Pool) => {
return drizzle(pool, { schema });
},
},
],
exports: [DATABASE_POOL, DRIZZLE],
})
export class DatabaseModule {}
```
### 5.3 Script de Migration
Créer le fichier `src/database/migrate.ts` :
```typescript
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
import * as dotenv from 'dotenv';
dotenv.config();
const main = async () => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const db = drizzle(pool);
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './src/database/migrations' });
console.log('Migrations completed successfully');
await pool.end();
};
main().catch((err) => {
console.error('Migration failed');
console.error(err);
process.exit(1);
});
```
### 5.4 Schéma de Base de Données
Créer les fichiers de schéma dans le dossier `src/database/schema/` selon le plan détaillé dans le document DATABASE_SCHEMA_PLAN.md.
### 5.5 Scripts pour les Migrations
Ajouter les scripts suivants au `package.json` du backend :
```json
{
"scripts": {
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "ts-node src/database/migrate.ts",
"db:studio": "drizzle-kit studio"
}
}
```
## 6. Authentification
### 6.1 Module d'Authentification
Créer le fichier `src/modules/auth/auth.module.ts` selon le plan détaillé dans le document AUTH_IMPLEMENTATION_PLAN.md.
### 6.2 Stratégies d'Authentification
Implémenter les stratégies d'authentification (GitHub, JWT, JWT Refresh) selon le plan détaillé dans le document AUTH_IMPLEMENTATION_PLAN.md.
### 6.3 Service d'Authentification
Implémenter le service d'authentification selon le plan détaillé dans le document AUTH_IMPLEMENTATION_PLAN.md.
### 6.4 Contrôleur d'Authentification
Implémenter le contrôleur d'authentification selon le plan détaillé dans le document AUTH_IMPLEMENTATION_PLAN.md.
### 6.5 Guards et Décorateurs
Implémenter les guards et décorateurs d'authentification selon le plan détaillé dans le document AUTH_IMPLEMENTATION_PLAN.md.
## 7. Modules Fonctionnels
### 7.1 Module Utilisateurs
#### 7.1.1 Service Utilisateurs
```typescript
// src/modules/users/services/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
import { Inject } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import * as schema from '../../../database/schema';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
@Injectable()
export class UsersService {
constructor(@Inject(DRIZZLE) private readonly db: any) {}
async create(createUserDto: CreateUserDto) {
const [user] = await this.db
.insert(schema.users)
.values(createUserDto)
.returning();
return user;
}
async findAll() {
return this.db.select().from(schema.users);
}
async findById(id: string) {
const [user] = await this.db
.select()
.from(schema.users)
.where(eq(schema.users.id, id));
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async findByGithubId(githubId: string) {
const [user] = await this.db
.select()
.from(schema.users)
.where(eq(schema.users.githubId, githubId));
return user;
}
async update(id: string, updateUserDto: UpdateUserDto) {
const [user] = await this.db
.update(schema.users)
.set({
...updateUserDto,
updatedAt: new Date(),
})
.where(eq(schema.users.id, id))
.returning();
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async remove(id: string) {
const [user] = await this.db
.delete(schema.users)
.where(eq(schema.users.id, id))
.returning();
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async updateGdprConsent(id: string) {
return this.update(id, { gdprTimestamp: new Date() });
}
async exportUserData(id: string) {
const user = await this.findById(id);
const projects = await this.db
.select()
.from(schema.projects)
.where(eq(schema.projects.ownerId, id));
return {
user,
projects,
};
}
}
```
#### 7.1.2 Contrôleur Utilisateurs
```typescript
// src/modules/users/controllers/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { UsersService } from '../services/users.service';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { GetUser } from '../../auth/decorators/get-user.decorator';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { Role } from '../../auth/enums/role.enum';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
findAll() {
return this.usersService.findAll();
}
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@GetUser() user) {
return user;
}
@Get(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
findOne(@Param('id') id: string) {
return this.usersService.findById(id);
}
@Patch(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
@Post('gdpr-consent')
@UseGuards(JwtAuthGuard)
updateGdprConsent(@GetUser('id') userId: string) {
return this.usersService.updateGdprConsent(userId);
}
@Get('export-data')
@UseGuards(JwtAuthGuard)
exportUserData(@GetUser('id') userId: string) {
return this.usersService.exportUserData(userId);
}
}
```
### 7.2 Module Projets
Implémenter le module de gestion des projets avec les opérations CRUD et les relations avec les utilisateurs, les personnes et les groupes.
### 7.3 Module Personnes
Implémenter le module de gestion des personnes avec les opérations CRUD et les attributs spécifiés (niveau technique, genre, âge, etc.).
### 7.4 Module Groupes
Implémenter le module de gestion des groupes avec les opérations CRUD et les algorithmes de création automatique de groupes équilibrés.
### 7.5 Module Tags
Implémenter le module de gestion des tags avec les opérations CRUD et la gestion des types de tags (PROJECT, PERSON).
## 8. Communication en Temps Réel
### 8.1 Module WebSockets
Implémenter le module WebSockets selon le plan détaillé dans le document WEBSOCKET_IMPLEMENTATION_PLAN.md.
### 8.2 Gateways WebSocket
Implémenter les gateways WebSocket (Projets, Groupes, Notifications) selon le plan détaillé dans le document WEBSOCKET_IMPLEMENTATION_PLAN.md.
### 8.3 Service WebSocket
Implémenter le service WebSocket selon le plan détaillé dans le document WEBSOCKET_IMPLEMENTATION_PLAN.md.
## 9. Sécurité et Conformité RGPD
### 9.1 Sécurité
#### 9.1.1 Protection contre les Attaques Courantes
- Implémenter la protection CSRF pour les opérations sensibles
- Configurer les en-têtes de sécurité (Content-Security-Policy, X-XSS-Protection, etc.)
- Utiliser des paramètres préparés avec DrizzleORM pour prévenir les injections SQL
- Mettre en place le rate limiting pour prévenir les attaques par force brute
#### 9.1.2 Gestion des Tokens
- Implémenter la révocation des tokens JWT
- Configurer la rotation des clés de signature
- Mettre en place la validation complète des tokens (signature, expiration, émetteur)
### 9.2 Conformité RGPD
#### 9.2.1 Gestion du Consentement
- Implémenter l'enregistrement du timestamp d'acceptation RGPD
- Mettre en place le renouvellement du consentement tous les 13 mois
#### 9.2.2 Droits des Utilisateurs
- Implémenter l'export des données personnelles
- Mettre en place la suppression de compte avec option de conservation ou suppression des projets
## 10. Tests et Documentation
### 10.1 Tests
#### 10.1.1 Tests Unitaires
Écrire des tests unitaires pour les services et les contrôleurs en utilisant Jest.
#### 10.1.2 Tests E2E
Développer des tests end-to-end pour les API en utilisant Supertest.
### 10.2 Documentation
#### 10.2.1 Documentation API
Générer la documentation API avec Swagger en utilisant les décorateurs NestJS.
#### 10.2.2 Documentation Technique
Documenter l'architecture, les modèles de données et les flux d'interaction.
## 11. Déploiement
### 11.1 Conteneurisation
Créer un Dockerfile pour le backend :
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main"]
```
### 11.2 CI/CD
Configurer un workflow CI/CD avec GitHub Actions pour l'intégration et le déploiement continus.
## 12. Calendrier d'Implémentation
1. **Semaine 1: Configuration et Base de Données**
- Configuration de l'environnement
- Mise en place de la base de données avec DrizzleORM
- Définition du schéma et création des migrations
2. **Semaine 2: Authentification et Utilisateurs**
- Implémentation de l'authentification GitHub OAuth
- Développement du module utilisateurs
- Mise en place de la gestion des JWT
3. **Semaine 3: Modules Principaux**
- Développement des modules projets, personnes et groupes
- Implémentation des opérations CRUD
- Mise en place des relations entre entités
4. **Semaine 4: Fonctionnalités Avancées**
- Implémentation des WebSockets pour la communication en temps réel
- Développement des algorithmes de création de groupes
- Mise en place des fonctionnalités de sécurité et de conformité RGPD
5. **Semaine 5: Tests et Finalisation**
- Écriture des tests unitaires et e2e
- Documentation de l'API
- Optimisation des performances et correction des bugs

83
SUMMARY.md Normal file
View File

@ -0,0 +1,83 @@
# Résumé et Prochaines Étapes
## Résumé du Travail Effectué
Nous avons élaboré un plan de bataille complet pour l'implémentation du backend de l'application de création de groupes, basé sur les spécifications du cahier des charges. Ce travail a abouti à la création de plusieurs documents détaillés :
1. **BACKEND_IMPLEMENTATION_PLAN.md** : Plan général d'implémentation du backend, incluant la structure des dossiers, les dépendances à ajouter, la configuration de l'environnement, et les étapes d'implémentation.
2. **DATABASE_SCHEMA_PLAN.md** : Plan détaillé du schéma de base de données, incluant la définition des tables, les relations, les types, les migrations, et les stratégies d'optimisation.
3. **AUTH_IMPLEMENTATION_PLAN.md** : Plan d'implémentation du système d'authentification avec OAuth 2.0 via GitHub et JWT, incluant les stratégies, services, contrôleurs, guards et décorateurs.
4. **WEBSOCKET_IMPLEMENTATION_PLAN.md** : Plan d'implémentation du système de communication en temps réel avec Socket.IO, incluant les gateways, services, et événements.
5. **IMPLEMENTATION_GUIDE.md** : Guide complet combinant tous les plans précédents et fournissant une feuille de route claire pour l'implémentation du backend.
Ces documents fournissent une base solide pour le développement du backend, avec des instructions détaillées pour chaque composant du système.
## Prochaines Étapes
Pour mettre en œuvre ce plan, voici les prochaines étapes à suivre :
### 1. Configuration Initiale
- [ ] Installer les dépendances nécessaires avec pnpm
- [ ] Créer le fichier .env à partir du modèle .env.example
- [ ] Configurer la structure de base du projet selon le plan
### 2. Base de Données
- [ ] Implémenter les schémas de base de données avec DrizzleORM
- [ ] Configurer le module de base de données dans NestJS
- [ ] Générer et exécuter les migrations initiales
### 3. Authentification
- [ ] Implémenter le module d'authentification avec GitHub OAuth
- [ ] Configurer les stratégies JWT pour la gestion des sessions
- [ ] Mettre en place les guards et décorateurs pour la protection des routes
### 4. Modules Fonctionnels
- [ ] Implémenter le module utilisateurs
- [ ] Implémenter le module projets
- [ ] Implémenter le module personnes
- [ ] Implémenter le module groupes
- [ ] Implémenter le module tags
### 5. Communication en Temps Réel
- [ ] Configurer Socket.IO avec NestJS
- [ ] Implémenter les gateways WebSocket pour les projets, groupes et notifications
- [ ] Mettre en place le service WebSocket pour la gestion des connexions
### 6. Sécurité et Conformité RGPD
- [ ] Implémenter les mesures de sécurité (protection CSRF, validation des entrées, etc.)
- [ ] Mettre en place les fonctionnalités de conformité RGPD (consentement, export de données, etc.)
### 7. Tests et Documentation
- [ ] Écrire des tests unitaires pour les services et contrôleurs
- [ ] Développer des tests e2e pour les API
- [ ] Générer la documentation API avec Swagger
### 8. Déploiement
- [ ] Créer le Dockerfile pour la conteneurisation
- [ ] Configurer le workflow CI/CD avec GitHub Actions
## Recommandations
1. **Approche Itérative** : Suivre une approche itérative en implémentant d'abord les fonctionnalités de base, puis en ajoutant progressivement les fonctionnalités plus avancées.
2. **Tests Continus** : Écrire des tests au fur et à mesure du développement pour s'assurer que les fonctionnalités sont correctement implémentées.
3. **Documentation** : Documenter le code et les API au fur et à mesure pour faciliter la maintenance et l'évolution du projet.
4. **Revue de Code** : Effectuer des revues de code régulières pour s'assurer de la qualité du code et du respect des bonnes pratiques.
5. **Suivi du Calendrier** : Suivre le calendrier d'implémentation proposé pour s'assurer que le projet progresse selon le planning prévu.
En suivant ce plan et ces recommandations, l'implémentation du backend de l'application de création de groupes devrait être réalisée de manière efficace et conforme aux spécifications du cahier des charges.

View File

@ -0,0 +1,801 @@
# 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