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:
parent
9976cfeb7a
commit
ef934a8599
890
AUTH_IMPLEMENTATION_PLAN.md
Normal file
890
AUTH_IMPLEMENTATION_PLAN.md
Normal 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
|
223
BACKEND_IMPLEMENTATION_PLAN.md
Normal file
223
BACKEND_IMPLEMENTATION_PLAN.md
Normal 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
488
DATABASE_SCHEMA_PLAN.md
Normal 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
761
IMPLEMENTATION_GUIDE.md
Normal 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
83
SUMMARY.md
Normal 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.
|
801
WEBSOCKET_IMPLEMENTATION_PLAN.md
Normal file
801
WEBSOCKET_IMPLEMENTATION_PLAN.md
Normal 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
|
Loading…
x
Reference in New Issue
Block a user