docs: remove detailed implementation plans for authentication and backend architecture
Deleted extensive implementation plan documents (`AUTH_IMPLEMENTATION_PLAN.md` and `BACKEND_IMPLEMENTATION_PLAN.md`) to streamline documentation efforts and prevent duplication with updated overall project guides.
This commit is contained in:
parent
2035821e89
commit
7b6da2767e
@ -1,890 +0,0 @@
|
|||||||
# 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
|
|
@ -1,223 +0,0 @@
|
|||||||
# 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
|
|
@ -1,347 +0,0 @@
|
|||||||
# Diagrammes de Flux Métier
|
|
||||||
|
|
||||||
Ce document présente les diagrammes de séquence pour les principaux flux métier de l'application de création de groupes.
|
|
||||||
|
|
||||||
## Table des Matières
|
|
||||||
|
|
||||||
1. [Flux d'Authentification](#1-flux-dauthentification)
|
|
||||||
2. [Flux de Création et Gestion de Projet](#2-flux-de-création-et-gestion-de-projet)
|
|
||||||
3. [Flux de Gestion des Personnes](#3-flux-de-gestion-des-personnes)
|
|
||||||
4. [Flux de Création de Groupe](#4-flux-de-création-de-groupe)
|
|
||||||
- [4.1 Création Manuelle](#41-création-manuelle)
|
|
||||||
- [4.2 Création Automatique](#42-création-automatique)
|
|
||||||
5. [Flux de Collaboration en Temps Réel](#5-flux-de-collaboration-en-temps-réel)
|
|
||||||
|
|
||||||
## 1. Flux d'Authentification
|
|
||||||
|
|
||||||
Le flux d'authentification utilise OAuth 2.0 avec GitHub comme fournisseur d'identité.
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Flux de Création et Gestion de Projet
|
|
||||||
|
|
||||||
Ce flux illustre le processus de création et de gestion d'un projet par un utilisateur.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User as Utilisateur
|
|
||||||
participant Frontend as Frontend (Next.js)
|
|
||||||
participant API as API (NestJS)
|
|
||||||
participant DB as Base de données
|
|
||||||
|
|
||||||
User->>Frontend: Accède au dashboard
|
|
||||||
Frontend->>API: GET /api/projects
|
|
||||||
API->>DB: Requête les projets de l'utilisateur
|
|
||||||
DB->>API: Retourne les projets
|
|
||||||
API->>Frontend: Retourne la liste des projets
|
|
||||||
Frontend->>User: Affiche les projets existants
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Créer un nouveau projet"
|
|
||||||
Frontend->>User: Affiche le formulaire de création
|
|
||||||
User->>Frontend: Remplit le formulaire (nom, description)
|
|
||||||
Frontend->>API: POST /api/projects
|
|
||||||
API->>DB: Insère le nouveau projet
|
|
||||||
DB->>API: Confirme la création
|
|
||||||
API->>Frontend: Retourne les détails du projet créé
|
|
||||||
Frontend->>User: Affiche la page du projet
|
|
||||||
|
|
||||||
User->>Frontend: Modifie les détails du projet
|
|
||||||
Frontend->>API: PATCH /api/projects/{id}
|
|
||||||
API->>DB: Met à jour le projet
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend: Retourne les détails mis à jour
|
|
||||||
Frontend->>User: Affiche les détails mis à jour
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Supprimer le projet"
|
|
||||||
Frontend->>User: Demande confirmation
|
|
||||||
User->>Frontend: Confirme la suppression
|
|
||||||
Frontend->>API: DELETE /api/projects/{id}
|
|
||||||
API->>DB: Supprime le projet et ses données associées
|
|
||||||
DB->>API: Confirme la suppression
|
|
||||||
API->>Frontend: Retourne confirmation
|
|
||||||
Frontend->>User: Redirige vers le dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Flux de Gestion des Personnes
|
|
||||||
|
|
||||||
Ce flux illustre le processus d'ajout et de gestion des personnes dans un projet.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User as Utilisateur
|
|
||||||
participant Frontend as Frontend (Next.js)
|
|
||||||
participant API as API (NestJS)
|
|
||||||
participant DB as Base de données
|
|
||||||
participant WS as WebSocket
|
|
||||||
|
|
||||||
User->>Frontend: Accède à un projet
|
|
||||||
Frontend->>API: GET /api/projects/{id}
|
|
||||||
API->>DB: Requête les détails du projet
|
|
||||||
DB->>API: Retourne les détails du projet
|
|
||||||
API->>Frontend: Retourne les détails du projet
|
|
||||||
Frontend->>User: Affiche la page du projet
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Gérer les personnes"
|
|
||||||
Frontend->>API: GET /api/projects/{id}/persons
|
|
||||||
API->>DB: Requête les personnes du projet
|
|
||||||
DB->>API: Retourne les personnes
|
|
||||||
API->>Frontend: Retourne la liste des personnes
|
|
||||||
Frontend->>User: Affiche la liste des personnes
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Ajouter une personne"
|
|
||||||
Frontend->>User: Affiche le formulaire d'ajout
|
|
||||||
User->>Frontend: Remplit les attributs (prénom, nom, genre, niveau technique, etc.)
|
|
||||||
Frontend->>API: POST /api/projects/{id}/persons
|
|
||||||
API->>DB: Insère la nouvelle personne
|
|
||||||
DB->>API: Confirme l'ajout
|
|
||||||
API->>Frontend: Retourne les détails de la personne
|
|
||||||
API->>WS: Émet événement "personAdded"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Met à jour la liste des personnes
|
|
||||||
|
|
||||||
User->>Frontend: Modifie les attributs d'une personne
|
|
||||||
Frontend->>API: PATCH /api/persons/{id}
|
|
||||||
API->>DB: Met à jour la personne
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend: Retourne les détails mis à jour
|
|
||||||
API->>WS: Émet événement "personUpdated"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche les détails mis à jour
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Supprimer une personne"
|
|
||||||
Frontend->>User: Demande confirmation
|
|
||||||
User->>Frontend: Confirme la suppression
|
|
||||||
Frontend->>API: DELETE /api/persons/{id}
|
|
||||||
API->>DB: Supprime la personne
|
|
||||||
DB->>API: Confirme la suppression
|
|
||||||
API->>Frontend: Retourne confirmation
|
|
||||||
API->>WS: Émet événement "personDeleted"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Met à jour la liste des personnes
|
|
||||||
|
|
||||||
User->>Frontend: Ajoute un tag à une personne
|
|
||||||
Frontend->>API: POST /api/persons/{id}/tags
|
|
||||||
API->>DB: Associe le tag à la personne
|
|
||||||
DB->>API: Confirme l'association
|
|
||||||
API->>Frontend: Retourne la personne mise à jour
|
|
||||||
API->>WS: Émet événement "personTagged"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche la personne avec le tag
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Flux de Création de Groupe
|
|
||||||
|
|
||||||
### 4.1 Création Manuelle
|
|
||||||
|
|
||||||
Ce flux illustre le processus de création manuelle de groupes.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User as Utilisateur
|
|
||||||
participant Frontend as Frontend (Next.js)
|
|
||||||
participant API as API (NestJS)
|
|
||||||
participant DB as Base de données
|
|
||||||
participant WS as WebSocket
|
|
||||||
|
|
||||||
User->>Frontend: Accède à un projet
|
|
||||||
Frontend->>API: GET /api/projects/{id}
|
|
||||||
API->>DB: Requête les détails du projet
|
|
||||||
DB->>API: Retourne les détails du projet
|
|
||||||
API->>Frontend: Retourne les détails du projet
|
|
||||||
Frontend->>User: Affiche la page du projet
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Créer des groupes"
|
|
||||||
Frontend->>API: GET /api/projects/{id}/persons
|
|
||||||
API->>DB: Requête les personnes du projet
|
|
||||||
DB->>API: Retourne les personnes
|
|
||||||
API->>Frontend: Retourne la liste des personnes
|
|
||||||
Frontend->>User: Affiche l'interface de création de groupes
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Création manuelle"
|
|
||||||
Frontend->>User: Affiche l'interface de glisser-déposer
|
|
||||||
User->>Frontend: Crée un nouveau groupe
|
|
||||||
Frontend->>API: POST /api/projects/{id}/groups
|
|
||||||
API->>DB: Insère le nouveau groupe
|
|
||||||
DB->>API: Confirme la création
|
|
||||||
API->>Frontend: Retourne les détails du groupe
|
|
||||||
API->>WS: Émet événement "groupCreated"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche le groupe créé
|
|
||||||
|
|
||||||
User->>Frontend: Glisse-dépose des personnes dans le groupe
|
|
||||||
Frontend->>API: POST /api/groups/{id}/persons
|
|
||||||
API->>DB: Associe les personnes au groupe
|
|
||||||
DB->>API: Confirme l'association
|
|
||||||
API->>Frontend: Retourne le groupe mis à jour
|
|
||||||
API->>WS: Émet événement "personMoved"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche le groupe avec les personnes
|
|
||||||
|
|
||||||
User->>Frontend: Renomme le groupe
|
|
||||||
Frontend->>API: PATCH /api/groups/{id}
|
|
||||||
API->>DB: Met à jour le nom du groupe
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend: Retourne les détails mis à jour
|
|
||||||
API->>WS: Émet événement "groupUpdated"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche le groupe renommé
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Enregistrer les groupes"
|
|
||||||
Frontend->>API: PUT /api/projects/{id}/groups/save
|
|
||||||
API->>DB: Enregistre l'état final des groupes
|
|
||||||
DB->>API: Confirme l'enregistrement
|
|
||||||
API->>Frontend: Retourne confirmation
|
|
||||||
Frontend->>User: Affiche message de confirmation
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Création Automatique
|
|
||||||
|
|
||||||
Ce flux illustre le processus de création automatique de groupes équilibrés.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User as Utilisateur
|
|
||||||
participant Frontend as Frontend (Next.js)
|
|
||||||
participant API as API (NestJS)
|
|
||||||
participant DB as Base de données
|
|
||||||
participant Algorithm as Algorithme de Groupes
|
|
||||||
participant WS as WebSocket
|
|
||||||
|
|
||||||
User->>Frontend: Accède à un projet
|
|
||||||
Frontend->>API: GET /api/projects/{id}
|
|
||||||
API->>DB: Requête les détails du projet
|
|
||||||
DB->>API: Retourne les détails du projet
|
|
||||||
API->>Frontend: Retourne les détails du projet
|
|
||||||
Frontend->>User: Affiche la page du projet
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Créer des groupes"
|
|
||||||
Frontend->>API: GET /api/projects/{id}/persons
|
|
||||||
API->>DB: Requête les personnes du projet
|
|
||||||
DB->>API: Retourne les personnes
|
|
||||||
API->>Frontend: Retourne la liste des personnes
|
|
||||||
Frontend->>User: Affiche l'interface de création de groupes
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Création automatique"
|
|
||||||
Frontend->>User: Affiche les options de génération
|
|
||||||
User->>Frontend: Définit le nombre de groupes souhaités
|
|
||||||
User->>Frontend: Sélectionne un preset (équilibré par niveau, etc.)
|
|
||||||
Frontend->>API: POST /api/projects/{id}/groups/generate
|
|
||||||
API->>Algorithm: Transmet les personnes et les paramètres
|
|
||||||
Algorithm->>Algorithm: Exécute l'algorithme de répartition
|
|
||||||
Algorithm->>API: Retourne les groupes générés
|
|
||||||
API->>DB: Insère les groupes générés
|
|
||||||
DB->>API: Confirme la création
|
|
||||||
API->>Frontend: Retourne les groupes générés
|
|
||||||
API->>WS: Émet événement "groupsGenerated"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche les groupes générés
|
|
||||||
|
|
||||||
User->>Frontend: Ajuste manuellement certains groupes
|
|
||||||
Frontend->>API: PATCH /api/groups/{id}/persons
|
|
||||||
API->>DB: Met à jour les associations
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend: Retourne les groupes mis à jour
|
|
||||||
API->>WS: Émet événement "groupsUpdated"
|
|
||||||
WS->>Frontend: Notifie les clients connectés
|
|
||||||
Frontend->>User: Affiche les groupes ajustés
|
|
||||||
|
|
||||||
User->>Frontend: Clic sur "Enregistrer les groupes"
|
|
||||||
Frontend->>API: PUT /api/projects/{id}/groups/save
|
|
||||||
API->>DB: Enregistre l'état final des groupes
|
|
||||||
DB->>API: Confirme l'enregistrement
|
|
||||||
API->>Frontend: Retourne confirmation
|
|
||||||
Frontend->>User: Affiche message de confirmation
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Flux de Collaboration en Temps Réel
|
|
||||||
|
|
||||||
Ce flux illustre le processus de collaboration en temps réel entre plusieurs utilisateurs travaillant sur le même projet.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User1 as Utilisateur 1
|
|
||||||
participant Frontend1 as Frontend 1
|
|
||||||
participant User2 as Utilisateur 2
|
|
||||||
participant Frontend2 as Frontend 2
|
|
||||||
participant API as API (NestJS)
|
|
||||||
participant WS as WebSocket Gateway
|
|
||||||
participant DB as Base de données
|
|
||||||
|
|
||||||
User1->>Frontend1: Se connecte au projet
|
|
||||||
Frontend1->>API: GET /api/projects/{id}
|
|
||||||
API->>DB: Requête les détails du projet
|
|
||||||
DB->>API: Retourne les détails du projet
|
|
||||||
API->>Frontend1: Retourne les détails du projet
|
|
||||||
Frontend1->>User1: Affiche la page du projet
|
|
||||||
|
|
||||||
Frontend1->>WS: Connexion WebSocket
|
|
||||||
Frontend1->>WS: Rejoint la salle "project:{id}"
|
|
||||||
WS->>Frontend1: Confirme la connexion
|
|
||||||
|
|
||||||
User2->>Frontend2: Se connecte au même projet
|
|
||||||
Frontend2->>API: GET /api/projects/{id}
|
|
||||||
API->>DB: Requête les détails du projet
|
|
||||||
DB->>API: Retourne les détails du projet
|
|
||||||
API->>Frontend2: Retourne les détails du projet
|
|
||||||
Frontend2->>User2: Affiche la page du projet
|
|
||||||
|
|
||||||
Frontend2->>WS: Connexion WebSocket
|
|
||||||
Frontend2->>WS: Rejoint la salle "project:{id}"
|
|
||||||
WS->>Frontend2: Confirme la connexion
|
|
||||||
WS->>Frontend1: Notifie qu'un autre utilisateur a rejoint
|
|
||||||
Frontend1->>User1: Affiche notification "Utilisateur 2 a rejoint"
|
|
||||||
|
|
||||||
User1->>Frontend1: Crée un nouveau groupe
|
|
||||||
Frontend1->>API: POST /api/projects/{id}/groups
|
|
||||||
API->>DB: Insère le nouveau groupe
|
|
||||||
DB->>API: Confirme la création
|
|
||||||
API->>Frontend1: Retourne les détails du groupe
|
|
||||||
API->>WS: Émet événement "groupCreated"
|
|
||||||
WS->>Frontend2: Transmet l'événement "groupCreated"
|
|
||||||
Frontend2->>User2: Met à jour l'interface avec le nouveau groupe
|
|
||||||
|
|
||||||
User2->>Frontend2: Déplace une personne dans le groupe
|
|
||||||
Frontend2->>API: PATCH /api/groups/{id}/persons
|
|
||||||
API->>DB: Met à jour les associations
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend2: Retourne le groupe mis à jour
|
|
||||||
API->>WS: Émet événement "personMoved"
|
|
||||||
WS->>Frontend1: Transmet l'événement "personMoved"
|
|
||||||
Frontend1->>User1: Met à jour l'interface avec le mouvement
|
|
||||||
|
|
||||||
User1->>Frontend1: Renomme le groupe
|
|
||||||
Frontend1->>API: PATCH /api/groups/{id}
|
|
||||||
API->>DB: Met à jour le nom du groupe
|
|
||||||
DB->>API: Confirme la mise à jour
|
|
||||||
API->>Frontend1: Retourne les détails mis à jour
|
|
||||||
API->>WS: Émet événement "groupUpdated"
|
|
||||||
WS->>Frontend2: Transmet l'événement "groupUpdated"
|
|
||||||
Frontend2->>User2: Met à jour l'interface avec le nouveau nom
|
|
||||||
|
|
||||||
User2->>Frontend2: Se déconnecte du projet
|
|
||||||
Frontend2->>WS: Quitte la salle "project:{id}"
|
|
||||||
WS->>Frontend1: Notifie qu'un utilisateur a quitté
|
|
||||||
Frontend1->>User1: Affiche notification "Utilisateur 2 a quitté"
|
|
||||||
```
|
|
@ -1,488 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,761 +0,0 @@
|
|||||||
# 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
83
SUMMARY.md
@ -1,83 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,801 +0,0 @@
|
|||||||
# 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
|
|
763
cdc.md
763
cdc.md
@ -1,763 +0,0 @@
|
|||||||
# Cahier des Charges - Application de Création de Groupes
|
|
||||||
|
|
||||||
## 1. Introduction
|
|
||||||
|
|
||||||
### 1.1 Contexte du Projet
|
|
||||||
Ce document constitue le cahier des charges pour le développement d'une application web dédiée à la création et à la gestion de groupes. Cette application permettra 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.2 Objectifs du Projet
|
|
||||||
- Développer une application permettant la création de groupes selon différents critères
|
|
||||||
- Maintenir un historique des groupes créés pour éviter les duplications
|
|
||||||
- Offrir une interface intuitive et responsive
|
|
||||||
- Assurer la sécurité des données utilisateurs
|
|
||||||
- Respecter les normes RGPD
|
|
||||||
|
|
||||||
## 2. Architecture Technique
|
|
||||||
|
|
||||||
### 2.1 Stack Technologique
|
|
||||||
L'application sera développée en utilisant les technologies suivantes:
|
|
||||||
|
|
||||||
#### Frontend:
|
|
||||||
- **NextJS**: Framework React pour le rendu côté serveur et la génération de sites statiques
|
|
||||||
- Utilisation des App Router et Server Components pour optimiser les performances
|
|
||||||
- Implémentation du SSR (Server-Side Rendering) pour améliorer le SEO et le temps de chargement initial
|
|
||||||
- Utilisation des API Routes pour les endpoints spécifiques au frontend
|
|
||||||
|
|
||||||
- **SWR**: Bibliothèque React Hooks pour la récupération de données
|
|
||||||
- Mise en cache intelligente et revalidation automatique des données
|
|
||||||
- Stratégies de récupération optimisées (stale-while-revalidate)
|
|
||||||
- Gestion des états de chargement, d'erreur et de données
|
|
||||||
- Revalidation automatique lors du focus de la fenêtre et de la reconnexion réseau
|
|
||||||
- Déduplication des requêtes multiples vers le même endpoint
|
|
||||||
|
|
||||||
- **ShadcnUI**: Bibliothèque de composants UI pour un design cohérent
|
|
||||||
- Composants accessibles et personnalisables
|
|
||||||
- Thèmes adaptables pour le mode clair/sombre
|
|
||||||
- Intégration avec Tailwind CSS pour la stylisation
|
|
||||||
|
|
||||||
- **React Hook Form**: Gestion des formulaires
|
|
||||||
- Validation des données côté client
|
|
||||||
- Gestion efficace des erreurs de formulaire
|
|
||||||
- Intégration avec Zod pour la validation de schéma
|
|
||||||
|
|
||||||
- **Motion**: Bibliothèque pour les animations et le dynamisme de l'interface
|
|
||||||
- Animations fluides et performantes
|
|
||||||
- Transitions entre les pages
|
|
||||||
- Effets visuels pour améliorer l'expérience utilisateur
|
|
||||||
|
|
||||||
#### Backend:
|
|
||||||
- **NestJS**: Framework Node.js pour construire des applications serveur efficaces et scalables
|
|
||||||
- Architecture modulaire basée sur les décorateurs
|
|
||||||
- Injection de dépendances pour une meilleure testabilité
|
|
||||||
- Support intégré pour TypeScript
|
|
||||||
- Utilisation des Guards, Interceptors et Pipes pour la gestion des requêtes
|
|
||||||
|
|
||||||
- **PostgreSQL**: Système de gestion de base de données relationnelle
|
|
||||||
- Modélisation des données avec relations complexes
|
|
||||||
- Utilisation de DrizzleORM comme ORM pour interagir avec la base de données
|
|
||||||
- Migrations SQL déclaratives et type-safe
|
|
||||||
- Approche code-first pour la définition du schéma
|
|
||||||
- Optimisation des formats de données PostgreSQL (JSONB pour les données flexibles, UUID pour les identifiants, ENUM pour les valeurs fixes)
|
|
||||||
- Stratégie d'indexation avancée pour améliorer les performances des requêtes
|
|
||||||
|
|
||||||
- **SocketIO**: Bibliothèque pour la communication en temps réel
|
|
||||||
- Mise à jour instantanée des groupes
|
|
||||||
- Notifications en temps réel
|
|
||||||
- Collaboration simultanée entre utilisateurs
|
|
||||||
|
|
||||||
- **@node-rs/argon2**: Bibliothèque pour le hachage sécurisé des mots de passe
|
|
||||||
- Implémentation en Rust pour des performances optimales
|
|
||||||
- Protection contre les attaques par force brute
|
|
||||||
- Configuration adaptée aux recommandations de sécurité actuelles
|
|
||||||
|
|
||||||
- **jose**: Bibliothèque pour la gestion des JWT (JSON Web Tokens)
|
|
||||||
- Authentification stateless
|
|
||||||
- Signature et vérification des tokens
|
|
||||||
- Gestion des expirations et du rafraîchissement des tokens
|
|
||||||
|
|
||||||
#### Authentification:
|
|
||||||
- **OAuth2.0 + OIDC**: Via compte GitHub pour l'authentification sécurisée
|
|
||||||
- Flux d'autorisation code avec PKCE
|
|
||||||
- Récupération des informations de profil via l'API GitHub
|
|
||||||
- Gestion des scopes pour limiter les accès
|
|
||||||
- Implémentation côté backend pour sécuriser le processus d'authentification
|
|
||||||
|
|
||||||
### 2.2 Architecture Applicative
|
|
||||||
L'application suivra une architecture monorepo avec séparation claire entre le frontend et le backend.
|
|
||||||
|
|
||||||
#### 2.2.1 Structure du Monorepo
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├── apps/
|
|
||||||
│ ├── web/ # Application frontend NextJS
|
|
||||||
│ │ ├── public/ # Fichiers statiques
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── app/ # App Router de NextJS
|
|
||||||
│ │ │ ├── components/ # Composants React réutilisables
|
|
||||||
│ │ │ ├── hooks/ # Custom hooks React
|
|
||||||
│ │ │ ├── lib/ # Utilitaires et configurations
|
|
||||||
│ │ │ └── styles/ # Styles globaux
|
|
||||||
│ │ └── ...
|
|
||||||
│ │
|
|
||||||
│ └── api/ # Application backend NestJS
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── modules/ # Modules NestJS
|
|
||||||
│ │ ├── common/ # Utilitaires partagés
|
|
||||||
│ │ ├── config/ # Configuration de l'application
|
|
||||||
│ │ └── main.ts # Point d'entrée de l'application
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
├── packages/ # Packages partagés
|
|
||||||
│ ├── database/ # Configuration DrizzleORM et modèles
|
|
||||||
│ ├── eslint-config/ # Configuration ESLint partagée
|
|
||||||
│ ├── tsconfig/ # Configuration TypeScript partagée
|
|
||||||
│ └── ui/ # Bibliothèque de composants UI partagés
|
|
||||||
│
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2.2 Gestion du Workspace avec PNPM
|
|
||||||
Le projet utilise PNPM pour la gestion du workspace et des packages. PNPM offre plusieurs avantages par rapport à d'autres gestionnaires de packages:
|
|
||||||
|
|
||||||
- **Efficacité de stockage**: Utilise un stockage partagé pour éviter la duplication des packages
|
|
||||||
- **Gestion de monorepo**: Facilite la gestion des dépendances entre les packages du monorepo
|
|
||||||
- **Performance**: Installation et mise à jour des dépendances plus rapides
|
|
||||||
- **Déterminisme**: Garantit que les mêmes dépendances sont installées de manière cohérente
|
|
||||||
|
|
||||||
Exemples d'utilisation de PNPM dans le monorepo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Exécuter une commande dans un package spécifique
|
|
||||||
pnpm --filter <package-name> <command>
|
|
||||||
|
|
||||||
# Exemple : démarrer le frontend uniquement
|
|
||||||
pnpm --filter web dev
|
|
||||||
|
|
||||||
# Installer une dépendance dans un package spécifique
|
|
||||||
pnpm --filter <package-name> add <dependency>
|
|
||||||
|
|
||||||
# Installer une dépendance de développement dans un package spécifique
|
|
||||||
pnpm --filter <package-name> add -D <dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2.3 Communication entre les Services
|
|
||||||
- API REST pour les opérations CRUD standard
|
|
||||||
- WebSockets via SocketIO pour les communications en temps réel
|
|
||||||
- Authentification via JWT pour sécuriser les échanges
|
|
||||||
|
|
||||||
#### 2.2.4 Architecture et Flux d'Interactions
|
|
||||||
Le diagramme ci-dessous illustre les interactions entre les différents composants du système:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph Client["Client (Navigateur)"]
|
|
||||||
FE["Frontend (NextJS)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Server["Serveur"]
|
|
||||||
BE["Backend (NestJS)"]
|
|
||||||
WS["WebSocket (SocketIO)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Storage["Stockage"]
|
|
||||||
DB[(PostgreSQL)]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph External["Services Externes"]
|
|
||||||
GH["API GitHub"]
|
|
||||||
end
|
|
||||||
|
|
||||||
FE <--> BE
|
|
||||||
FE <--> WS
|
|
||||||
BE <--> DB
|
|
||||||
BE <--> GH
|
|
||||||
BE <--> WS
|
|
||||||
|
|
||||||
classDef client fill:#9c27b0,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef server fill:#3f51b5,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef storage fill:#4caf50,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef external fill:#ff9800,stroke:#ffffff,stroke-width:2px
|
|
||||||
|
|
||||||
class Client client
|
|
||||||
class Server server
|
|
||||||
class Storage storage
|
|
||||||
class External external
|
|
||||||
```
|
|
||||||
|
|
||||||
Ce diagramme montre les principaux flux d'interactions:
|
|
||||||
|
|
||||||
1. **Frontend ↔ Backend**: Communication via API REST pour les opérations CRUD standard
|
|
||||||
- Requêtes HTTP pour la création, lecture, mise à jour et suppression de données
|
|
||||||
- Authentification via JWT pour sécuriser les échanges
|
|
||||||
- Validation des données côté client et serveur
|
|
||||||
|
|
||||||
2. **Frontend ↔ WebSocket**: Communication en temps réel
|
|
||||||
- Notifications instantanées
|
|
||||||
- Mises à jour en direct des groupes
|
|
||||||
- Collaboration entre utilisateurs
|
|
||||||
|
|
||||||
3. **Backend ↔ Base de données**: Persistance des données
|
|
||||||
- Requêtes SQL optimisées via DrizzleORM
|
|
||||||
- Transactions pour garantir l'intégrité des données
|
|
||||||
- Utilisation d'index pour des performances optimales
|
|
||||||
|
|
||||||
4. **Backend ↔ API GitHub**: Récupération des données utilisateur
|
|
||||||
- Récupération des avatars utilisateurs
|
|
||||||
- Authentification des utilisateurs
|
|
||||||
- Récupération des informations de profil
|
|
||||||
|
|
||||||
5. **Backend ↔ WebSocket**: Gestion des événements
|
|
||||||
- Diffusion des mises à jour aux clients connectés
|
|
||||||
- Gestion des salles pour les projets collaboratifs
|
|
||||||
- Notification des changements en temps réel
|
|
||||||
|
|
||||||
Cette architecture permet une séparation claire des responsabilités tout en offrant une expérience utilisateur fluide et réactive.
|
|
||||||
|
|
||||||
#### 2.2.5 Déploiement
|
|
||||||
- Conteneurisation avec Docker pour assurer la cohérence entre les environnements
|
|
||||||
- CI/CD via GitHub Actions pour l'intégration et le déploiement continus
|
|
||||||
- Infrastructure scalable pour gérer les pics de charge
|
|
||||||
|
|
||||||
### 2.3 Modèle de Données
|
|
||||||
|
|
||||||
#### 2.3.1 Entités Principales
|
|
||||||
1. **User**
|
|
||||||
- id: UUIDv7 (clé primaire, type `uuid` optimisé pour l'indexation)
|
|
||||||
- name: String (type `varchar(100)`)
|
|
||||||
- avatar: String (URL depuis l'API Github, type `text`)
|
|
||||||
- githubId: String (pour l'authentification OAuth, type `varchar(50)` avec index)
|
|
||||||
- gdprTimestamp: DateTime (timestamp d'acceptation RGPD, type `timestamptz`)
|
|
||||||
- createdAt: DateTime (type `timestamptz` avec index)
|
|
||||||
- updatedAt: DateTime (type `timestamptz`)
|
|
||||||
- metadata: JSON (données flexibles, type `jsonb`)
|
|
||||||
|
|
||||||
2. **Project**
|
|
||||||
- id: UUIDv7 (clé primaire, type `uuid` optimisé pour l'indexation)
|
|
||||||
- name: String (type `varchar(100)` avec index)
|
|
||||||
- description: String (type `text`)
|
|
||||||
- ownerId: UUID (clé étrangère vers User, type `uuid` avec index)
|
|
||||||
- settings: JSON (configurations personnalisées, type `jsonb`)
|
|
||||||
- createdAt: DateTime (type `timestamptz` avec index)
|
|
||||||
- updatedAt: DateTime (type `timestamptz`)
|
|
||||||
|
|
||||||
3. **Person**
|
|
||||||
- id: UUIDv7 (clé primaire, type `uuid` optimisé pour l'indexation)
|
|
||||||
- firstName: String (type `varchar(50)` avec index partiel)
|
|
||||||
- lastName: String (type `varchar(50)` avec index partiel)
|
|
||||||
- gender: Enum (MALE, FEMALE, NON_BINARY, type `enum` natif PostgreSQL)
|
|
||||||
- technicalLevel: Integer (type `smallint` pour économie d'espace)
|
|
||||||
- hasTechnicalTraining: Boolean (type `boolean`)
|
|
||||||
- frenchSpeakingLevel: Integer (type `smallint` pour économie d'espace)
|
|
||||||
- oralEaseLevel: Enum (SHY, RESERVED, COMFORTABLE, type `enum` natif PostgreSQL)
|
|
||||||
- age: Integer (type `smallint` pour économie d'espace)
|
|
||||||
- projectId: UUID (clé étrangère vers Project, type `uuid` avec index)
|
|
||||||
- attributes: JSON (attributs additionnels flexibles, type `jsonb`)
|
|
||||||
- tags: Relation vers PersonTag (table de jointure avec index)
|
|
||||||
- createdAt: DateTime (type `timestamptz`)
|
|
||||||
- updatedAt: DateTime (type `timestamptz`)
|
|
||||||
|
|
||||||
4. **Group**
|
|
||||||
- id: UUIDv7 (clé primaire, type `uuid` optimisé pour l'indexation)
|
|
||||||
- name: String (type `varchar(100)` avec index)
|
|
||||||
- projectId: UUID (clé étrangère vers Project, type `uuid` avec index)
|
|
||||||
- metadata: JSON (données additionnelles, type `jsonb`)
|
|
||||||
- members: Relation vers Person (table de jointure avec index)
|
|
||||||
- createdAt: DateTime (type `timestamptz`)
|
|
||||||
- updatedAt: DateTime (type `timestamptz`)
|
|
||||||
|
|
||||||
5. **Tag**
|
|
||||||
- id: UUIDv7 (clé primaire, type `uuid` optimisé pour l'indexation)
|
|
||||||
- name: String (type `varchar(50)` avec index)
|
|
||||||
- color: String (code couleur, type `varchar(7)`)
|
|
||||||
- type: Enum (PROJECT, PERSON, type `enum` natif PostgreSQL)
|
|
||||||
- persons: Relation vers Person (table de jointure avec index)
|
|
||||||
- projects: Relation vers Project (table de jointure avec index)
|
|
||||||
- createdAt: DateTime (type `timestamptz`)
|
|
||||||
- updatedAt: DateTime (type `timestamptz`)
|
|
||||||
|
|
||||||
#### 2.3.2 Relations
|
|
||||||
- Un **User** peut avoir plusieurs **Projects**
|
|
||||||
- Un **Project** appartient à un seul **User**
|
|
||||||
- Un **Project** contient plusieurs **Persons**
|
|
||||||
- Un **Project** peut avoir plusieurs **Groups**
|
|
||||||
- Un **Project** peut être associé à plusieurs **Tags** de type PROJECT
|
|
||||||
- Une **Person** appartient à un seul **Project**
|
|
||||||
- Une **Person** peut être associée à plusieurs **Tags** de type PERSON
|
|
||||||
- Une **Person** peut être membre d'un seul **Group** à la fois
|
|
||||||
- Un **Group** appartient à un seul **Project**
|
|
||||||
- Un **Group** peut contenir plusieurs **Persons**
|
|
||||||
- Les **Tags** sont globaux et gérés par les administrateurs
|
|
||||||
|
|
||||||
#### 2.3.3 Schéma de Base de Données
|
|
||||||
Le schéma sera implémenté via DrizzleORM, permettant une définition type-safe des tables et relations avec une approche code-first. Les migrations SQL seront générées automatiquement à partir des changements de schéma, offrant un contrôle précis sur l'évolution de la base de données.
|
|
||||||
|
|
||||||
##### 2.3.3.1 Modèle Conceptuel de Données (MCD)
|
|
||||||
Le diagramme ci-dessous représente le modèle conceptuel de données de l'application, montrant les entités et leurs relations:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
USER {
|
|
||||||
uuid id PK
|
|
||||||
string name
|
|
||||||
string avatar
|
|
||||||
string githubId
|
|
||||||
datetime gdprTimestamp
|
|
||||||
datetime createdAt
|
|
||||||
datetime updatedAt
|
|
||||||
json metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
PROJECT {
|
|
||||||
uuid id PK
|
|
||||||
string name
|
|
||||||
string description
|
|
||||||
uuid ownerId FK
|
|
||||||
json settings
|
|
||||||
datetime createdAt
|
|
||||||
datetime updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
PERSON {
|
|
||||||
uuid id PK
|
|
||||||
string firstName
|
|
||||||
string lastName
|
|
||||||
enum gender
|
|
||||||
int technicalLevel
|
|
||||||
boolean hasTechnicalTraining
|
|
||||||
int frenchSpeakingLevel
|
|
||||||
enum oralEaseLevel
|
|
||||||
int age
|
|
||||||
uuid projectId FK
|
|
||||||
json attributes
|
|
||||||
datetime createdAt
|
|
||||||
datetime updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
GROUP {
|
|
||||||
uuid id PK
|
|
||||||
string name
|
|
||||||
uuid projectId FK
|
|
||||||
json metadata
|
|
||||||
datetime createdAt
|
|
||||||
datetime updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
TAG {
|
|
||||||
uuid id PK
|
|
||||||
string name
|
|
||||||
string color
|
|
||||||
enum type
|
|
||||||
datetime createdAt
|
|
||||||
datetime updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
USER ||--o{ PROJECT : "possède"
|
|
||||||
PROJECT ||--o{ PERSON : "contient"
|
|
||||||
PROJECT ||--o{ GROUP : "contient"
|
|
||||||
PROJECT }o--o{ TAG : "est associé à"
|
|
||||||
PERSON }o--o{ TAG : "est associée à"
|
|
||||||
PERSON }o--|| GROUP : "est membre de"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2.3.3.2 Modèle Logique de Données (MLD)
|
|
||||||
Le diagramme ci-dessous représente le modèle logique de données, montrant les tables, leurs champs et les relations entre elles:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
users {
|
|
||||||
uuid id PK
|
|
||||||
varchar(100) name
|
|
||||||
text avatar
|
|
||||||
varchar(50) githubId
|
|
||||||
timestamptz gdprTimestamp
|
|
||||||
timestamptz createdAt
|
|
||||||
timestamptz updatedAt
|
|
||||||
jsonb metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
projects {
|
|
||||||
uuid id PK
|
|
||||||
varchar(100) name
|
|
||||||
text description
|
|
||||||
uuid ownerId FK
|
|
||||||
jsonb settings
|
|
||||||
timestamptz createdAt
|
|
||||||
timestamptz updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
persons {
|
|
||||||
uuid id PK
|
|
||||||
varchar(50) firstName
|
|
||||||
varchar(50) lastName
|
|
||||||
enum gender
|
|
||||||
smallint technicalLevel
|
|
||||||
boolean hasTechnicalTraining
|
|
||||||
smallint frenchSpeakingLevel
|
|
||||||
enum oralEaseLevel
|
|
||||||
smallint age
|
|
||||||
uuid projectId FK
|
|
||||||
jsonb attributes
|
|
||||||
timestamptz createdAt
|
|
||||||
timestamptz updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
groups {
|
|
||||||
uuid id PK
|
|
||||||
varchar(100) name
|
|
||||||
uuid projectId FK
|
|
||||||
jsonb metadata
|
|
||||||
timestamptz createdAt
|
|
||||||
timestamptz updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
tags {
|
|
||||||
uuid id PK
|
|
||||||
varchar(50) name
|
|
||||||
varchar(7) color
|
|
||||||
enum type
|
|
||||||
timestamptz createdAt
|
|
||||||
timestamptz updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
person_to_group {
|
|
||||||
uuid id PK
|
|
||||||
uuid personId FK
|
|
||||||
uuid groupId FK
|
|
||||||
timestamptz createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
person_to_tag {
|
|
||||||
uuid id PK
|
|
||||||
uuid personId FK
|
|
||||||
uuid tagId FK
|
|
||||||
timestamptz createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
project_to_tag {
|
|
||||||
uuid id PK
|
|
||||||
uuid projectId FK
|
|
||||||
uuid tagId FK
|
|
||||||
timestamptz createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
users ||--o{ projects : "ownerId"
|
|
||||||
projects ||--o{ persons : "projectId"
|
|
||||||
projects ||--o{ groups : "projectId"
|
|
||||||
projects ||--o{ project_to_tag : "projectId"
|
|
||||||
persons ||--o{ person_to_group : "personId"
|
|
||||||
groups ||--o{ person_to_group : "groupId"
|
|
||||||
persons ||--o{ person_to_tag : "personId"
|
|
||||||
tags ||--o{ person_to_tag : "tagId"
|
|
||||||
tags ||--o{ project_to_tag : "tagId"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2.3.3.3 Stratégie d'Indexation
|
|
||||||
Pour optimiser les performances des requêtes, les stratégies d'indexation suivantes seront mises en place:
|
|
||||||
- Index primaires sur toutes les clés primaires (UUIDv7)
|
|
||||||
- Index secondaires sur les clés étrangères pour accélérer les jointures
|
|
||||||
- Index composites sur les champs fréquemment utilisés ensemble dans les requêtes
|
|
||||||
- Index partiels pour les requêtes filtrées fréquentes
|
|
||||||
- Index de texte pour les recherches sur les champs textuels (noms, descriptions)
|
|
||||||
|
|
||||||
DrizzleORM facilite la définition de ces index directement dans le schéma avec une syntaxe déclarative:
|
|
||||||
```typescript
|
|
||||||
// Exemple de définition d'index avec DrizzleORM
|
|
||||||
import { pgTable, uuid, varchar, timestamp, index } from 'drizzle-orm/pg-core';
|
|
||||||
|
|
||||||
export const users = pgTable('users', {
|
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
|
||||||
name: varchar('name', { length: 255 }),
|
|
||||||
githubId: varchar('githubId', { length: 50 }),
|
|
||||||
gdprTimestamp: timestamp('gdprTimestamp', { withTimezone: true }),
|
|
||||||
}, (table) => {
|
|
||||||
return {
|
|
||||||
githubIdIdx: index('githubId_idx').on(table.githubId),
|
|
||||||
nameIdx: index('name_idx').on(table.name),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2.3.3.4 Optimisation des Formats de Données
|
|
||||||
Les types de données PostgreSQL seront optimisés pour chaque cas d'usage:
|
|
||||||
- Utilisation de `UUID` pour les identifiants (UUIDv7 pour l'ordre chronologique)
|
|
||||||
- Type `JSONB` pour les données flexibles et semi-structurées
|
|
||||||
- Types `ENUM` PostgreSQL natifs pour les valeurs fixes (genres, niveaux d'aisance)
|
|
||||||
- Type `TEXT` avec contraintes pour les chaînes de caractères variables
|
|
||||||
- Types `TIMESTAMP WITH TIME ZONE` pour les dates avec gestion des fuseaux horaires
|
|
||||||
- Utilisation de `NUMERIC` pour les valeurs nécessitant une précision exacte
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
##### 2.3.3.5 Modèle Simplifié pour Utilisateurs Non-Techniques
|
|
||||||
Le diagramme ci-dessous présente une version simplifiée du modèle de données, conçue pour être facilement compréhensible par des personnes non-techniques:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
User[Utilisateur] -->|Crée et gère| Project[Projet]
|
|
||||||
Project -->|Contient| Person[Personnes]
|
|
||||||
Project -->|Organise en| Group[Groupes]
|
|
||||||
Project -->|Associé à| Tag[Tags/Étiquettes]
|
|
||||||
Person -->|Appartient à| Group
|
|
||||||
Person -->|Associée à| Tag
|
|
||||||
Admin[Administrateur] -->|Gère| Tag
|
|
||||||
|
|
||||||
classDef user fill:#9c27b0,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef project fill:#3f51b5,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef person fill:#4caf50,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef group fill:#f44336,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef tag fill:#ff9800,stroke:#ffffff,stroke-width:2px
|
|
||||||
classDef admin fill:#00bcd4,stroke:#ffffff,stroke-width:2px
|
|
||||||
|
|
||||||
class User user
|
|
||||||
class Project project
|
|
||||||
class Person person
|
|
||||||
class Group group
|
|
||||||
class Tag tag
|
|
||||||
class Admin admin
|
|
||||||
```
|
|
||||||
|
|
||||||
Ce diagramme illustre les concepts clés de l'application:
|
|
||||||
- Un **Utilisateur** crée et gère des projets
|
|
||||||
- Chaque **Projet** contient des personnes et des groupes et peut être associé à des tags
|
|
||||||
- Les **Personnes** sont organisées en groupes et peuvent être associées à des tags
|
|
||||||
- Les **Groupes** sont composés de personnes
|
|
||||||
- Les **Tags** permettent de catégoriser les personnes et les projets selon différents critères
|
|
||||||
- L'**Administrateur** gère les tags globaux utilisés dans toute l'application
|
|
||||||
|
|
||||||
Cette représentation simplifiée permet aux parties prenantes non-techniques de comprendre facilement la structure générale de l'application sans avoir à se plonger dans les détails techniques du modèle de données.
|
|
||||||
|
|
||||||
## 3. Spécifications Fonctionnelles
|
|
||||||
|
|
||||||
### 3.1 Interface Utilisateur
|
|
||||||
|
|
||||||
#### 3.1.1 Principes Généraux
|
|
||||||
- Approche "mobile first" pour l'ensemble du site
|
|
||||||
- Interface de type dashboard pour le frontend
|
|
||||||
- Design responsive s'adaptant à tous les appareils (mobile, tablette, desktop)
|
|
||||||
- Accessibilité conforme aux normes WCAG 2.1 niveau AA
|
|
||||||
- Thème clair/sombre avec détection automatique des préférences système
|
|
||||||
|
|
||||||
#### 3.1.2 Structure Globale
|
|
||||||
- **Header**:
|
|
||||||
- Logo et nom de l'application
|
|
||||||
- Navigation principale
|
|
||||||
- Fonctionnalités de gestion de compte et de connexion
|
|
||||||
- Recherche de projets enregistrés
|
|
||||||
- Indicateur de notifications
|
|
||||||
|
|
||||||
- **Footer**:
|
|
||||||
- Liens vers les pages légales obligatoires (CGU, Politique de confidentialité, Mentions légales)
|
|
||||||
- Liens vers la documentation
|
|
||||||
- Informations de contact
|
|
||||||
- Sélecteur de langue
|
|
||||||
|
|
||||||
- **Page d'accueil**:
|
|
||||||
- Présentation des fonctionnalités aux utilisateurs anonymes
|
|
||||||
- Bac à sable interactif pour tester l'application sans inscription
|
|
||||||
- Témoignages et cas d'utilisation
|
|
||||||
- Appel à l'action pour la création de compte
|
|
||||||
|
|
||||||
- **Dashboard**:
|
|
||||||
- Vue d'ensemble des projets de l'utilisateur
|
|
||||||
- Statistiques et métriques sur l'utilisation
|
|
||||||
- Accès rapide aux fonctionnalités principales
|
|
||||||
- Notifications et alertes
|
|
||||||
|
|
||||||
#### 3.1.3 Composants UI Spécifiques
|
|
||||||
- Utilisation de ShadcnUI pour les composants de base (boutons, champs de formulaire, modales, etc.)
|
|
||||||
- Animations et transitions avec Motion pour améliorer l'expérience utilisateur
|
|
||||||
- Formulaires optimisés avec React Hook Form pour une validation instantanée
|
|
||||||
- Visualisations interactives pour les groupes créés
|
|
||||||
|
|
||||||
### 3.2 Gestion des Utilisateurs
|
|
||||||
|
|
||||||
#### 3.2.1 Inscription et Authentification
|
|
||||||
- Création de compte utilisateur obligatoire pour utiliser pleinement les fonctionnalités
|
|
||||||
- Authentification via OAuth2.0 avec GitHub:
|
|
||||||
- Flux d'authentification sécurisé avec redirection vers GitHub
|
|
||||||
- Récupération des informations de base du profil (nom, avatar) depuis l'API GitHub
|
|
||||||
- Possibilité d'étendre à d'autres fournisseurs d'identité dans le futur
|
|
||||||
- Gestion des sessions utilisateur avec JWT (JSON Web Tokens):
|
|
||||||
- Token d'accès avec durée de validité limitée (15 minutes)
|
|
||||||
- Token de rafraîchissement pour prolonger la session (validité de 7 jours)
|
|
||||||
- Révocation des tokens en cas de déconnexion ou de suspicion de compromission
|
|
||||||
- Gestion des autorisations basée sur les rôles (RBAC):
|
|
||||||
- Rôle administrateur pour la gestion globale:
|
|
||||||
- Gestion des tags globaux (création, modification, suppression)
|
|
||||||
- Attribution des types de tags (PROJECT, PERSON)
|
|
||||||
- Surveillance de l'utilisation des tags
|
|
||||||
- Gestion des utilisateurs et de leurs droits
|
|
||||||
- Rôle utilisateur standard pour la création et gestion de projets personnels
|
|
||||||
- Rôle invité pour l'accès en lecture seule à des projets partagés
|
|
||||||
|
|
||||||
#### 3.2.2 Profil Utilisateur
|
|
||||||
- Gestion des informations personnelles:
|
|
||||||
- Modification du nom d'affichage
|
|
||||||
- Affichage de l'avatar récupéré depuis l'API GitHub
|
|
||||||
- Gestion des préférences (notifications, thème, langue)
|
|
||||||
- Gestion du consentement RGPD (timestamp)
|
|
||||||
- Tableau de bord personnel:
|
|
||||||
- Vue d'ensemble des projets créés
|
|
||||||
- Statistiques d'utilisation
|
|
||||||
- Activité récente
|
|
||||||
- Gestion des notifications:
|
|
||||||
- Alertes système
|
|
||||||
- Rappels pour les projets en cours
|
|
||||||
- Notifications de partage
|
|
||||||
|
|
||||||
#### 3.2.3 Gestion des Données Utilisateur
|
|
||||||
- Accès à l'historique des projets de groupe enregistrés
|
|
||||||
- Export des données personnelles au format JSON ou CSV
|
|
||||||
- Suppression de compte avec option de conservation ou suppression des projets
|
|
||||||
- Conformité RGPD avec droit à l'oubli et portabilité des données
|
|
||||||
|
|
||||||
### 3.3 Système d'Administration
|
|
||||||
|
|
||||||
#### 3.3.1 Interface d'Administration
|
|
||||||
- Tableau de bord administrateur dédié:
|
|
||||||
- Vue d'ensemble de l'utilisation de l'application
|
|
||||||
- Statistiques sur les utilisateurs, projets, et tags
|
|
||||||
- Alertes et notifications système
|
|
||||||
- Accès sécurisé réservé aux utilisateurs avec le rôle administrateur
|
|
||||||
- Interface distincte de l'application principale
|
|
||||||
|
|
||||||
#### 3.3.2 Gestion des Tags Globaux
|
|
||||||
- Interface de création et gestion des tags:
|
|
||||||
- Création de nouveaux tags avec nom et couleur
|
|
||||||
- Définition du type de tag (PROJECT ou PERSON)
|
|
||||||
- Modification des tags existants
|
|
||||||
- Suppression des tags non utilisés
|
|
||||||
- Visualisation de l'utilisation des tags:
|
|
||||||
- Nombre de projets et personnes associés à chaque tag
|
|
||||||
- Statistiques d'utilisation par utilisateur
|
|
||||||
- Possibilité de fusionner des tags similaires
|
|
||||||
- Exportation de la liste des tags au format CSV
|
|
||||||
|
|
||||||
#### 3.3.3 Gestion des Utilisateurs
|
|
||||||
- Liste complète des utilisateurs avec filtres et recherche
|
|
||||||
- Modification des rôles utilisateur (administrateur, utilisateur standard, invité)
|
|
||||||
- Surveillance de l'activité des utilisateurs
|
|
||||||
- Possibilité de désactiver temporairement un compte utilisateur
|
|
||||||
- Vérification du statut RGPD des utilisateurs
|
|
||||||
|
|
||||||
### 3.4 Création et Gestion de Groupes
|
|
||||||
#### 3.4.1 Création de Projet de Groupe
|
|
||||||
- Possibilité de créer une liste de personnes qui seront placées dans les groupes
|
|
||||||
- Attribution de "tags" aux personnes
|
|
||||||
- Définition d'échelles de niveau personnalisées
|
|
||||||
- Nom de projet unique à l'échelle de l'utilisateur
|
|
||||||
|
|
||||||
#### 3.4.2 Attributs des Personnes
|
|
||||||
Chaque personne dans le système sera caractérisée par les attributs suivants :
|
|
||||||
- Prénom
|
|
||||||
- Nom
|
|
||||||
- Genre (Masculin, féminin, non binaire)
|
|
||||||
- Niveau d'aisance technique
|
|
||||||
- Expérience préalable en formation technique
|
|
||||||
- Capacité d'expression en français
|
|
||||||
- Niveau d'aisance à l'oral (timide, réservé, à l'aise)
|
|
||||||
- Âge
|
|
||||||
|
|
||||||
#### 3.4.3 Interface de Création Manuelle
|
|
||||||
- Affichage de la liste des personnes sur le côté (format desktop minimum)
|
|
||||||
- Possibilité de réaliser manuellement les groupes
|
|
||||||
- Option de renommer chaque groupe manuellement
|
|
||||||
|
|
||||||
#### 3.4.4 Assistant à la Création de Groupe
|
|
||||||
- Fonctionnalité de création aléatoire de groupes
|
|
||||||
- L'utilisateur définit le nombre de groupes souhaités
|
|
||||||
- Attribution obligatoire d'un nom à chaque groupe
|
|
||||||
- Sélection de presets pour la génération de groupes équilibrés:
|
|
||||||
- Groupes équilibrés pour la progression du niveau
|
|
||||||
- Groupes équilibrés par niveau de compétence
|
|
||||||
|
|
||||||
### 3.5 Communication en Temps Réel
|
|
||||||
- Utilisation de SocketIO pour les mises à jour en temps réel
|
|
||||||
- Notification des modifications de groupes aux utilisateurs concernés
|
|
||||||
- Collaboration possible entre utilisateurs sur un même projet de groupe
|
|
||||||
|
|
||||||
## 4. Exigences Techniques
|
|
||||||
|
|
||||||
### 4.1 Développement
|
|
||||||
- Respect des principes SOLID
|
|
||||||
- Application des conventions de nommage standard
|
|
||||||
- Tests unitaires et tests e2e de l'API
|
|
||||||
- Documentation technique complète
|
|
||||||
|
|
||||||
### 4.2 Sécurité et Conformité
|
|
||||||
|
|
||||||
#### 4.2.1 Protection des Données
|
|
||||||
- Chiffrement des données sensibles en base de données
|
|
||||||
- Hachage sécurisé des mots de passe avec @node-rs/argon2:
|
|
||||||
- Utilisation de sel unique pour chaque utilisateur
|
|
||||||
- Paramètres de hachage conformes aux recommandations OWASP
|
|
||||||
- Implémentation en Rust pour des performances optimales et une résistance aux attaques par force brute
|
|
||||||
- Gestion des tokens JWT avec la bibliothèque jose:
|
|
||||||
- Signatures avec algorithme RS256
|
|
||||||
- Rotation des clés de signature
|
|
||||||
- Validation complète des tokens (signature, expiration, émetteur)
|
|
||||||
- Mise en place de mécanismes de défense contre les attaques courantes:
|
|
||||||
- Protection CSRF (Cross-Site Request Forgery)
|
|
||||||
- Protection XSS (Cross-Site Scripting)
|
|
||||||
- Protection contre les injections SQL
|
|
||||||
- Rate limiting pour prévenir les attaques par force brute
|
|
||||||
|
|
||||||
#### 4.2.2 Conformité RGPD
|
|
||||||
- Conformité aux exigences RGPD et aux lois françaises:
|
|
||||||
- Minimisation des données collectées
|
|
||||||
- Finalité claire de la collecte de données
|
|
||||||
- Durée de conservation limitée et justifiée
|
|
||||||
- Mise en œuvre des droits des utilisateurs:
|
|
||||||
- Droit d'accès aux données personnelles
|
|
||||||
- Droit de rectification
|
|
||||||
- Droit à l'effacement (droit à l'oubli)
|
|
||||||
- Droit à la portabilité des données
|
|
||||||
- Droit d'opposition au traitement
|
|
||||||
- Renouvellement du consentement utilisateur tous les 13 mois pour les conditions générales d'utilisation et les cookies
|
|
||||||
- Registre des activités de traitement
|
|
||||||
- Procédure de notification en cas de violation de données
|
|
||||||
|
|
||||||
#### 4.2.3 Audit et Traçabilité
|
|
||||||
- Journalisation des actions sensibles:
|
|
||||||
- Connexions et déconnexions
|
|
||||||
- Modifications de données importantes
|
|
||||||
- Accès aux données personnelles
|
|
||||||
- Conservation des logs pendant une durée conforme aux exigences légales
|
|
||||||
- Système d'alerte en cas d'activité suspecte
|
|
||||||
- Audits de sécurité réguliers
|
|
||||||
|
|
||||||
### 4.3 Performance et Monitoring
|
|
||||||
|
|
||||||
#### 4.3.1 Objectifs de Performance
|
|
||||||
- Temps de chargement initial < 2 secondes (95ème percentile)
|
|
||||||
- Temps de réponse API < 300ms (95ème percentile)
|
|
||||||
- Temps d'exécution des requêtes SQL complexes < 100ms (95ème percentile)
|
|
||||||
- Disponibilité > 99.9%
|
|
||||||
- Support de 1000 utilisateurs simultanés minimum
|
|
||||||
- Utilisation efficiente des index pour les requêtes fréquentes
|
|
||||||
|
|
||||||
#### 4.3.2 Optimisation de la Base de Données
|
|
||||||
- Analyse régulière des plans d'exécution des requêtes avec `EXPLAIN ANALYZE`
|
|
||||||
- Mise en place d'un processus de maintenance automatisé:
|
|
||||||
- VACUUM régulier pour récupérer l'espace disque
|
|
||||||
- ANALYZE pour mettre à jour les statistiques du planificateur
|
|
||||||
- REINDEX pour maintenir l'efficacité des index
|
|
||||||
- Partitionnement des tables volumineuses pour améliorer les performances
|
|
||||||
- Utilisation de la mise en cache des requêtes fréquentes
|
|
||||||
- Optimisation des requêtes N+1 via l'utilisation appropriée des jointures et des relations
|
|
||||||
|
|
||||||
#### 4.3.3 Monitoring et Observabilité
|
|
||||||
- Mise en place d'outils de monitoring:
|
|
||||||
- Métriques d'application (temps de réponse, taux d'erreur)
|
|
||||||
- Métriques système (CPU, mémoire, disque)
|
|
||||||
- Métriques utilisateur (nombre de sessions, actions effectuées)
|
|
||||||
- Métriques de base de données:
|
|
||||||
- Temps d'exécution des requêtes
|
|
||||||
- Utilisation des index
|
|
||||||
- Taux de cache hit/miss
|
|
||||||
- Nombre de connexions actives
|
|
||||||
- Taille des tables et des index
|
|
||||||
- Alerting automatique en cas de dégradation des performances
|
|
||||||
- Tableau de bord de supervision pour l'équipe technique
|
|
||||||
- Analyse des logs centralisée
|
|
||||||
- Traçage des requêtes lentes avec pg_stat_statements
|
|
Loading…
x
Reference in New Issue
Block a user