docs: add comprehensive project documentation files
Added detailed documentation files, including project overview, current status, specifications, implementation guide, and README structure. Organized content to improve navigation and streamline project understanding.
This commit is contained in:
554
docs/implementation/AUTH_IMPLEMENTATION_PLAN.md
Normal file
554
docs/implementation/AUTH_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# 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;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## 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 {}
|
||||
```
|
||||
|
||||
## 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>;
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Conclusion
|
||||
|
||||
Ce plan d'implémentation fournit une base solide pour mettre en place un système d'authentification sécurisé et conforme aux bonnes pratiques. L'utilisation d'OAuth 2.0 avec GitHub comme fournisseur d'identité simplifie le processus d'authentification pour les utilisateurs tout en offrant un niveau de sécurité élevé.
|
||||
|
||||
La gestion des sessions avec JWT permet une architecture stateless qui facilite la scalabilité de l'application, tandis que le mécanisme de refresh token offre une expérience utilisateur fluide sans compromettre la sécurité.
|
||||
|
||||
Les mesures de sécurité et de conformité RGPD intégrées dans ce plan garantissent que l'application respecte les normes actuelles en matière de protection des données et de sécurité des utilisateurs.
|
||||
223
docs/implementation/BACKEND_IMPLEMENTATION_PLAN.md
Normal file
223
docs/implementation/BACKEND_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Plan d'Implémentation du Backend
|
||||
|
||||
Ce document détaille le plan d'implémentation du backend pour l'application de création de groupes, basé sur les spécifications du cahier des charges.
|
||||
|
||||
## 1. Structure des Dossiers
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── main.ts # Point d'entrée de l'application
|
||||
│ ├── app.module.ts # Module principal
|
||||
│ ├── config/ # Configuration de l'application
|
||||
│ │ ├── app.config.ts # Configuration générale
|
||||
│ │ ├── database.config.ts # Configuration de la base de données
|
||||
│ │ ├── auth.config.ts # Configuration de l'authentification
|
||||
│ │ └── env.validation.ts # Validation des variables d'environnement
|
||||
│ ├── common/ # Utilitaires partagés
|
||||
│ │ ├── decorators/ # Décorateurs personnalisés
|
||||
│ │ ├── filters/ # Filtres d'exception
|
||||
│ │ ├── guards/ # Guards d'authentification et d'autorisation
|
||||
│ │ ├── interceptors/ # Intercepteurs
|
||||
│ │ ├── pipes/ # Pipes de validation
|
||||
│ │ └── utils/ # Fonctions utilitaires
|
||||
│ ├── modules/ # Modules fonctionnels
|
||||
│ │ ├── auth/ # Module d'authentification
|
||||
│ │ │ ├── controllers/ # Contrôleurs d'authentification
|
||||
│ │ │ ├── services/ # Services d'authentification
|
||||
│ │ │ ├── guards/ # Guards spécifiques à l'authentification
|
||||
│ │ │ ├── strategies/ # Stratégies d'authentification (GitHub OAuth)
|
||||
│ │ │ └── auth.module.ts # Module d'authentification
|
||||
│ │ ├── users/ # Module de gestion des utilisateurs
|
||||
│ │ │ ├── controllers/ # Contrôleurs utilisateurs
|
||||
│ │ │ ├── services/ # Services utilisateurs
|
||||
│ │ │ ├── dto/ # Objets de transfert de données
|
||||
│ │ │ └── users.module.ts # Module utilisateurs
|
||||
│ │ ├── projects/ # Module de gestion des projets
|
||||
│ │ │ ├── controllers/ # Contrôleurs projets
|
||||
│ │ │ ├── services/ # Services projets
|
||||
│ │ │ ├── dto/ # Objets de transfert de données
|
||||
│ │ │ └── projects.module.ts # Module projets
|
||||
│ │ ├── persons/ # Module de gestion des personnes
|
||||
│ │ │ ├── controllers/ # Contrôleurs personnes
|
||||
│ │ │ ├── services/ # Services personnes
|
||||
│ │ │ ├── dto/ # Objets de transfert de données
|
||||
│ │ │ └── persons.module.ts # Module personnes
|
||||
│ │ ├── groups/ # Module de gestion des groupes
|
||||
│ │ │ ├── controllers/ # Contrôleurs groupes
|
||||
│ │ │ ├── services/ # Services groupes
|
||||
│ │ │ ├── dto/ # Objets de transfert de données
|
||||
│ │ │ └── groups.module.ts # Module groupes
|
||||
│ │ ├── tags/ # Module de gestion des tags
|
||||
│ │ │ ├── controllers/ # Contrôleurs tags
|
||||
│ │ │ ├── services/ # Services tags
|
||||
│ │ │ ├── dto/ # Objets de transfert de données
|
||||
│ │ │ └── tags.module.ts # Module tags
|
||||
│ │ └── websockets/ # Module de gestion des WebSockets
|
||||
│ │ ├── gateways/ # Gateways WebSocket
|
||||
│ │ ├── events/ # Définitions des événements
|
||||
│ │ └── websockets.module.ts # Module WebSockets
|
||||
│ └── database/ # Configuration de la base de données
|
||||
│ ├── migrations/ # Migrations de base de données
|
||||
│ ├── schema/ # Schéma de base de données (DrizzleORM)
|
||||
│ └── database.module.ts # Module de base de données
|
||||
├── test/ # Tests
|
||||
│ ├── e2e/ # Tests end-to-end
|
||||
│ └── unit/ # Tests unitaires
|
||||
└── .env.example # Exemple de fichier d'environnement
|
||||
```
|
||||
|
||||
## 2. Dépendances à Ajouter
|
||||
|
||||
```bash
|
||||
# Dépendances principales
|
||||
pnpm add @nestjs/config @nestjs/passport passport passport-github2 @nestjs/jwt
|
||||
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
|
||||
pnpm add drizzle-orm pg
|
||||
pnpm add @node-rs/argon2 jose
|
||||
pnpm add class-validator class-transformer
|
||||
pnpm add zod zod-validation-error
|
||||
pnpm add uuid
|
||||
|
||||
# Dépendances de développement
|
||||
pnpm add -D drizzle-kit
|
||||
pnpm add -D @types/passport-github2 @types/socket.io @types/pg @types/uuid
|
||||
```
|
||||
|
||||
## 3. Configuration de l'Environnement
|
||||
|
||||
Créer un fichier `.env.example` avec les variables suivantes :
|
||||
|
||||
```
|
||||
# Application
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
API_PREFIX=api
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/groupmaker
|
||||
|
||||
# Authentication
|
||||
GITHUB_CLIENT_ID=your_github_client_id
|
||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||
GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
|
||||
|
||||
# JWT
|
||||
JWT_ACCESS_SECRET=your_access_token_secret
|
||||
JWT_REFRESH_SECRET=your_refresh_token_secret
|
||||
JWT_ACCESS_EXPIRATION=15m
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
|
||||
# CORS
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
```
|
||||
|
||||
## 4. Étapes d'Implémentation
|
||||
|
||||
### 4.1 Configuration de Base
|
||||
|
||||
1. **Configuration de l'Application**
|
||||
- Mettre à jour `main.ts` pour inclure la configuration CORS, les préfixes d'API, et les pipes de validation globaux
|
||||
- Créer un module de configuration pour charger les variables d'environnement avec validation
|
||||
|
||||
2. **Configuration de la Base de Données**
|
||||
- Configurer DrizzleORM avec PostgreSQL
|
||||
- Définir le schéma de base de données selon le modèle de données spécifié
|
||||
- Mettre en place les migrations de base de données
|
||||
|
||||
### 4.2 Authentification et Autorisation
|
||||
|
||||
1. **Authentification GitHub OAuth**
|
||||
- Implémenter la stratégie d'authentification GitHub
|
||||
- Créer les endpoints d'authentification (login, callback, refresh, logout)
|
||||
- Mettre en place la gestion des JWT (génération, validation, rafraîchissement)
|
||||
|
||||
2. **Autorisation RBAC**
|
||||
- Implémenter les guards pour la vérification des rôles
|
||||
- Créer des décorateurs pour les rôles et les permissions
|
||||
- Mettre en place la logique de vérification des autorisations
|
||||
|
||||
### 4.3 Modules Fonctionnels
|
||||
|
||||
1. **Module Utilisateurs**
|
||||
- Implémenter les opérations CRUD pour les utilisateurs
|
||||
- Gérer les profils utilisateurs et les préférences
|
||||
- Implémenter la logique de consentement RGPD
|
||||
|
||||
2. **Module Projets**
|
||||
- Implémenter les opérations CRUD pour les projets
|
||||
- Gérer les relations avec les utilisateurs, les personnes et les groupes
|
||||
- Implémenter la logique de partage de projets
|
||||
|
||||
3. **Module Personnes**
|
||||
- Implémenter les opérations CRUD pour les personnes
|
||||
- Gérer les attributs des personnes (niveau technique, genre, âge, etc.)
|
||||
- Implémenter la logique d'association avec les tags
|
||||
|
||||
4. **Module Groupes**
|
||||
- Implémenter les opérations CRUD pour les groupes
|
||||
- Développer les algorithmes de création automatique de groupes équilibrés
|
||||
- Gérer les relations avec les personnes
|
||||
|
||||
5. **Module Tags**
|
||||
- Implémenter les opérations CRUD pour les tags
|
||||
- Gérer les types de tags (PROJECT, PERSON)
|
||||
- Implémenter la logique d'association avec les projets et les personnes
|
||||
|
||||
### 4.4 Communication en Temps Réel
|
||||
|
||||
1. **WebSockets avec SocketIO**
|
||||
- Configurer les gateways WebSocket
|
||||
- Implémenter les événements pour les mises à jour en temps réel
|
||||
- Gérer les salles pour les projets collaboratifs
|
||||
|
||||
### 4.5 Sécurité et Conformité RGPD
|
||||
|
||||
1. **Sécurité**
|
||||
- Implémenter le hachage des mots de passe avec @node-rs/argon2
|
||||
- Mettre en place des protections contre les attaques courantes (CSRF, XSS, injections SQL)
|
||||
- Configurer le rate limiting pour prévenir les attaques par force brute
|
||||
|
||||
2. **Conformité RGPD**
|
||||
- Implémenter les fonctionnalités d'export des données personnelles
|
||||
- Mettre en place la logique de suppression de compte
|
||||
- Gérer les consentements utilisateurs et leur renouvellement
|
||||
|
||||
### 4.6 Tests et Documentation
|
||||
|
||||
1. **Tests**
|
||||
- Écrire des tests unitaires pour les services et les contrôleurs
|
||||
- Développer des tests e2e pour les API
|
||||
- Mettre en place des tests d'intégration pour les modules critiques
|
||||
|
||||
2. **Documentation**
|
||||
- Générer la documentation API avec Swagger
|
||||
- Documenter les endpoints, les modèles de données et les paramètres
|
||||
- Fournir des exemples d'utilisation des API
|
||||
|
||||
## 5. Calendrier d'Implémentation
|
||||
|
||||
1. **Semaine 1: Configuration et Base de Données**
|
||||
- Configuration de l'environnement
|
||||
- Mise en place de la base de données avec DrizzleORM
|
||||
- Définition du schéma et création des migrations
|
||||
|
||||
2. **Semaine 2: Authentification et Utilisateurs**
|
||||
- Implémentation de l'authentification GitHub OAuth
|
||||
- Développement du module utilisateurs
|
||||
- Mise en place de la gestion des JWT
|
||||
|
||||
3. **Semaine 3: Modules Principaux**
|
||||
- Développement des modules projets, personnes et groupes
|
||||
- Implémentation des opérations CRUD
|
||||
- Mise en place des relations entre entités
|
||||
|
||||
4. **Semaine 4: Fonctionnalités Avancées**
|
||||
- Implémentation des WebSockets pour la communication en temps réel
|
||||
- Développement des algorithmes de création de groupes
|
||||
- Mise en place des fonctionnalités de sécurité et de conformité RGPD
|
||||
|
||||
5. **Semaine 5: Tests et Finalisation**
|
||||
- Écriture des tests unitaires et e2e
|
||||
- Documentation de l'API
|
||||
- Optimisation des performances et correction des bugs
|
||||
405
docs/implementation/DATABASE_SCHEMA_PLAN.md
Normal file
405
docs/implementation/DATABASE_SCHEMA_PLAN.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# Plan du Schéma de Base de Données
|
||||
|
||||
Ce document détaille le plan du schéma de base de données pour l'application de création de groupes, utilisant DrizzleORM avec PostgreSQL.
|
||||
|
||||
## 1. Vue d'Ensemble
|
||||
|
||||
Le schéma de base de données est conçu pour supporter les fonctionnalités suivantes :
|
||||
- Gestion des utilisateurs et authentification
|
||||
- Création et gestion de projets
|
||||
- Gestion des personnes et de leurs attributs
|
||||
- Création et gestion de groupes
|
||||
- Système de tags pour catégoriser les personnes et les projets
|
||||
|
||||
## 2. Tables Principales
|
||||
|
||||
### 2.1 Table `users`
|
||||
|
||||
Stocke les informations des utilisateurs de l'application.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/users.ts
|
||||
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
githubId: varchar('github_id', { length: 255 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
avatar: varchar('avatar', { length: 1024 }),
|
||||
role: varchar('role', { length: 50 }).notNull().default('USER'),
|
||||
gdprTimestamp: timestamp('gdpr_timestamp').notNull(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type NewUser = typeof users.$inferInsert;
|
||||
```
|
||||
|
||||
### 2.2 Table `projects`
|
||||
|
||||
Stocke les informations des projets créés par les utilisateurs.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/projects.ts
|
||||
import { pgTable, uuid, varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
|
||||
export const projects = pgTable('projects', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
settings: jsonb('settings').notNull().default({}),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
isPublic: boolean('is_public').notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
export type NewProject = typeof projects.$inferInsert;
|
||||
```
|
||||
|
||||
### 2.3 Table `persons`
|
||||
|
||||
Stocke les informations des personnes qui seront placées dans les groupes.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/persons.ts
|
||||
import { pgTable, uuid, varchar, text, integer, timestamp, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
|
||||
export const persons = pgTable('persons', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }),
|
||||
technicalLevel: integer('technical_level'),
|
||||
gender: varchar('gender', { length: 50 }),
|
||||
attributes: jsonb('attributes').notNull().default({}),
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Person = typeof persons.$inferSelect;
|
||||
export type NewPerson = typeof persons.$inferInsert;
|
||||
```
|
||||
|
||||
### 2.4 Table `groups`
|
||||
|
||||
Stocke les informations des groupes créés dans le cadre d'un projet.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/groups.ts
|
||||
import { pgTable, uuid, varchar, text, timestamp, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
|
||||
export const groups = pgTable('groups', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
settings: jsonb('settings').notNull().default({}),
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Group = typeof groups.$inferSelect;
|
||||
export type NewGroup = typeof groups.$inferInsert;
|
||||
```
|
||||
|
||||
### 2.5 Table `tags`
|
||||
|
||||
Stocke les tags qui peuvent être associés aux personnes et aux projets.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/tags.ts
|
||||
import { pgTable, uuid, varchar, text, timestamp, pgEnum } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const tagTypeEnum = pgEnum('tag_type', ['PROJECT', 'PERSON']);
|
||||
|
||||
export const tags = pgTable('tags', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
color: varchar('color', { length: 50 }),
|
||||
type: tagTypeEnum('type').notNull(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type Tag = typeof tags.$inferSelect;
|
||||
export type NewTag = typeof tags.$inferInsert;
|
||||
```
|
||||
|
||||
## 3. Tables de Relations
|
||||
|
||||
### 3.1 Table `personToGroup`
|
||||
|
||||
Relation many-to-many entre les personnes et les groupes.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/personToGroup.ts
|
||||
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { persons } from './persons';
|
||||
import { groups } from './groups';
|
||||
|
||||
export const personToGroup = pgTable('person_to_group', {
|
||||
personId: uuid('person_id').notNull().references(() => persons.id, { onDelete: 'cascade' }),
|
||||
groupId: uuid('group_id').notNull().references(() => groups.id, { onDelete: 'cascade' }),
|
||||
}, (t) => ({
|
||||
pk: primaryKey({ columns: [t.personId, t.groupId] }),
|
||||
}));
|
||||
```
|
||||
|
||||
### 3.2 Table `personToTag`
|
||||
|
||||
Relation many-to-many entre les personnes et les tags.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/personToTag.ts
|
||||
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { persons } from './persons';
|
||||
import { tags } from './tags';
|
||||
|
||||
export const personToTag = pgTable('person_to_tag', {
|
||||
personId: uuid('person_id').notNull().references(() => persons.id, { onDelete: 'cascade' }),
|
||||
tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
|
||||
}, (t) => ({
|
||||
pk: primaryKey({ columns: [t.personId, t.tagId] }),
|
||||
}));
|
||||
```
|
||||
|
||||
### 3.3 Table `projectToTag`
|
||||
|
||||
Relation many-to-many entre les projets et les tags.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/projectToTag.ts
|
||||
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
import { tags } from './tags';
|
||||
|
||||
export const projectToTag = pgTable('project_to_tag', {
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
|
||||
}, (t) => ({
|
||||
pk: primaryKey({ columns: [t.projectId, t.tagId] }),
|
||||
}));
|
||||
```
|
||||
|
||||
### 3.4 Table `projectCollaborators`
|
||||
|
||||
Relation many-to-many entre les projets et les utilisateurs pour la collaboration.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/projectCollaborators.ts
|
||||
import { pgTable, uuid, primaryKey, varchar } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
import { users } from './users';
|
||||
|
||||
export const projectCollaborators = pgTable('project_collaborators', {
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
role: varchar('role', { length: 50 }).notNull().default('VIEWER'),
|
||||
}, (t) => ({
|
||||
pk: primaryKey({ columns: [t.projectId, t.userId] }),
|
||||
}));
|
||||
```
|
||||
|
||||
## 4. Fichier d'Index
|
||||
|
||||
Fichier qui exporte toutes les tables pour faciliter leur utilisation.
|
||||
|
||||
```typescript
|
||||
// src/database/schema/index.ts
|
||||
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 './projectCollaborators';
|
||||
```
|
||||
|
||||
## 5. Configuration de DrizzleORM
|
||||
|
||||
### 5.1 Module de Base de Données
|
||||
|
||||
```typescript
|
||||
// src/database/database.module.ts
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import * as schema from './schema';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'DATABASE_CONNECTION',
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const connectionString = configService.get<string>('DATABASE_URL');
|
||||
const pool = new Pool({ connectionString });
|
||||
return drizzle(pool, { schema });
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: ['DATABASE_CONNECTION'],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
```
|
||||
|
||||
## 6. Migrations
|
||||
|
||||
### 6.1 Configuration des Migrations
|
||||
|
||||
```typescript
|
||||
// drizzle.config.ts
|
||||
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,
|
||||
},
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
### 6.2 Script de Migration
|
||||
|
||||
```typescript
|
||||
// src/database/migrations/migrate.ts
|
||||
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();
|
||||
|
||||
async function main() {
|
||||
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!', err);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## 7. Optimisations
|
||||
|
||||
### 7.1 Indexation
|
||||
|
||||
Pour optimiser les performances des requêtes fréquentes, nous ajouterons des index sur les colonnes suivantes :
|
||||
|
||||
```typescript
|
||||
// Exemple d'ajout d'index sur la table persons
|
||||
import { pgTable, uuid, varchar, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
|
||||
export const persons = pgTable('persons', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }),
|
||||
technicalLevel: integer('technical_level'),
|
||||
gender: varchar('gender', { length: 50 }),
|
||||
attributes: jsonb('attributes').notNull().default({}),
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
nameIdx: index('person_name_idx').on(table.name),
|
||||
projectIdIdx: index('person_project_id_idx').on(table.projectId),
|
||||
technicalLevelIdx: index('person_technical_level_idx').on(table.technicalLevel),
|
||||
}));
|
||||
```
|
||||
|
||||
### 7.2 Types de Données Optimisés
|
||||
|
||||
Utilisation de types de données optimisés pour réduire l'espace de stockage et améliorer les performances :
|
||||
|
||||
- `uuid` pour les identifiants uniques
|
||||
- `varchar` avec longueur limitée pour les chaînes de caractères
|
||||
- `jsonb` pour les données structurées flexibles
|
||||
- `timestamp` pour les dates et heures
|
||||
|
||||
### 7.3 Contraintes d'Intégrité
|
||||
|
||||
Utilisation de contraintes d'intégrité référentielle pour garantir la cohérence des données :
|
||||
|
||||
- Clés primaires sur toutes les tables
|
||||
- Clés étrangères avec actions en cascade pour la suppression
|
||||
- Contraintes d'unicité sur les colonnes appropriées
|
||||
|
||||
## 8. Requêtes Communes
|
||||
|
||||
### 8.1 Récupération d'un Projet avec ses Personnes et Groupes
|
||||
|
||||
```typescript
|
||||
// Exemple de requête pour récupérer un projet avec ses personnes et groupes
|
||||
const getProjectWithPersonsAndGroups = async (db, projectId) => {
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: (projects, { eq }) => eq(projects.id, projectId),
|
||||
with: {
|
||||
persons: true,
|
||||
groups: {
|
||||
with: {
|
||||
persons: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return project;
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 Récupération des Personnes avec leurs Tags
|
||||
|
||||
```typescript
|
||||
// Exemple de requête pour récupérer les personnes avec leurs tags
|
||||
const getPersonsWithTags = async (db, projectId) => {
|
||||
const persons = await db.query.persons.findMany({
|
||||
where: (persons, { eq }) => eq(persons.projectId, projectId),
|
||||
with: {
|
||||
tags: {
|
||||
columns: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return persons;
|
||||
};
|
||||
```
|
||||
|
||||
## 9. Conclusion
|
||||
|
||||
Ce schéma de base de données fournit une structure solide pour l'application de création de groupes, avec une conception qui prend en compte les performances, la flexibilité et l'intégrité des données. Les relations entre les entités sont clairement définies, et les types de données sont optimisés pour les besoins de l'application.
|
||||
|
||||
L'utilisation de DrizzleORM permet une intégration transparente avec NestJS et offre une expérience de développement type-safe, facilitant la maintenance et l'évolution du schéma au fil du temps.
|
||||
33
docs/implementation/README.md
Normal file
33
docs/implementation/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Implementation Guides
|
||||
|
||||
This directory contains detailed implementation guides for different aspects of the "Application de Création de Groupes" project.
|
||||
|
||||
## Available Guides
|
||||
|
||||
1. [Backend Implementation Plan](../BACKEND_IMPLEMENTATION_PLAN.md) - Comprehensive plan for implementing the backend of the application
|
||||
2. [Authentication Implementation Plan](../AUTH_IMPLEMENTATION_PLAN.md) - Detailed guide for implementing OAuth 2.0 authentication with GitHub
|
||||
3. [Database Schema Plan](../DATABASE_SCHEMA_PLAN.md) - Plan for implementing the database schema with DrizzleORM
|
||||
4. [WebSocket Implementation Plan](../WEBSOCKET_IMPLEMENTATION_PLAN.md) - Guide for implementing real-time communication with Socket.IO
|
||||
|
||||
## How to Use These Guides
|
||||
|
||||
These implementation guides are designed to be followed in a specific order:
|
||||
|
||||
1. Start with the **Database Schema Plan** to set up the foundation of your data model
|
||||
2. Follow the **Backend Implementation Plan** to create the basic structure of your NestJS application
|
||||
3. Implement the **Authentication Implementation Plan** to add user authentication
|
||||
4. Finally, add real-time capabilities using the **WebSocket Implementation Plan**
|
||||
|
||||
Each guide includes:
|
||||
- Detailed steps for implementation
|
||||
- Code examples
|
||||
- Configuration instructions
|
||||
- Best practices
|
||||
|
||||
## Recommended Development Approach
|
||||
|
||||
1. **Iterative Development**: Implement features incrementally, starting with the core functionality
|
||||
2. **Test-Driven Development**: Write tests before implementing features
|
||||
3. **Continuous Integration**: Set up CI/CD pipelines to automate testing and deployment
|
||||
4. **Code Reviews**: Have team members review code changes before merging
|
||||
5. **Documentation**: Keep documentation up-to-date as the implementation progresses
|
||||
900
docs/implementation/WEBSOCKET_IMPLEMENTATION_PLAN.md
Normal file
900
docs/implementation/WEBSOCKET_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,900 @@
|
||||
# Plan d'Implémentation des WebSockets
|
||||
|
||||
Ce document détaille le plan d'implémentation du système de communication en temps réel avec WebSockets pour l'application de création de groupes, basé sur les spécifications du cahier des charges.
|
||||
|
||||
## 1. Vue d'Ensemble
|
||||
|
||||
L'application utilisera Socket.IO pour la communication en temps réel entre les clients et le serveur. Cette approche permettra :
|
||||
|
||||
- La collaboration en temps réel entre les utilisateurs travaillant sur le même projet
|
||||
- Les notifications instantanées pour les actions importantes
|
||||
- La mise à jour automatique de l'interface utilisateur lorsque des changements sont effectués par d'autres utilisateurs
|
||||
|
||||
## 2. Architecture WebSocket
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["Clients"]
|
||||
C1["Client 1"]
|
||||
C2["Client 2"]
|
||||
C3["Client 3"]
|
||||
end
|
||||
|
||||
subgraph Server["Serveur NestJS"]
|
||||
GW["WebSocket Gateway"]
|
||||
AS["Authentication Service"]
|
||||
PS["Project Service"]
|
||||
GS["Group Service"]
|
||||
NS["Notification Service"]
|
||||
end
|
||||
|
||||
C1 <--> GW
|
||||
C2 <--> GW
|
||||
C3 <--> GW
|
||||
|
||||
GW <--> AS
|
||||
GW <--> PS
|
||||
GW <--> GS
|
||||
GW <--> NS
|
||||
```
|
||||
|
||||
## 3. Configuration de Socket.IO avec NestJS
|
||||
|
||||
### 3.1 Installation des Dépendances
|
||||
|
||||
```bash
|
||||
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
|
||||
pnpm add -D @types/socket.io
|
||||
```
|
||||
|
||||
### 3.2 Module WebSockets
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/websockets.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProjectsGateway } from './gateways/projects.gateway';
|
||||
import { GroupsGateway } from './gateways/groups.gateway';
|
||||
import { NotificationsGateway } from './gateways/notifications.gateway';
|
||||
import { WebSocketService } from './services/websocket.service';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { ProjectsModule } from '../projects/projects.module';
|
||||
import { GroupsModule } from '../groups/groups.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, ProjectsModule, GroupsModule],
|
||||
providers: [
|
||||
ProjectsGateway,
|
||||
GroupsGateway,
|
||||
NotificationsGateway,
|
||||
WebSocketService,
|
||||
],
|
||||
exports: [WebSocketService],
|
||||
})
|
||||
export class WebSocketsModule {}
|
||||
```
|
||||
|
||||
## 4. Service WebSocket
|
||||
|
||||
Le service WebSocket sera responsable de la gestion des connexions et des salles.
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/services/websocket.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketService {
|
||||
private server: Server;
|
||||
|
||||
setServer(server: Server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
getServer(): Server {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
joinRoom(socket: Socket, room: string) {
|
||||
socket.join(room);
|
||||
}
|
||||
|
||||
leaveRoom(socket: Socket, room: string) {
|
||||
socket.leave(room);
|
||||
}
|
||||
|
||||
emitToRoom(room: string, event: string, data: any) {
|
||||
this.server.to(room).emit(event, data);
|
||||
}
|
||||
|
||||
emitToAll(event: string, data: any) {
|
||||
this.server.emit(event, data);
|
||||
}
|
||||
|
||||
emitToUser(userId: string, event: string, data: any) {
|
||||
this.emitToRoom(`user:${userId}`, event, data);
|
||||
}
|
||||
|
||||
emitToProject(projectId: string, event: string, data: any) {
|
||||
this.emitToRoom(`project:${projectId}`, event, data);
|
||||
}
|
||||
|
||||
emitToGroup(groupId: string, event: string, data: any) {
|
||||
this.emitToRoom(`group:${groupId}`, event, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Gateways WebSocket
|
||||
|
||||
### 5.1 Gateway de Base
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/gateways/base.gateway.ts
|
||||
import {
|
||||
WebSocketGateway,
|
||||
OnGatewayInit,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN,
|
||||
credentials: true,
|
||||
},
|
||||
})
|
||||
export class BaseGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer() server: Server;
|
||||
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
constructor(
|
||||
protected readonly webSocketService: WebSocketService,
|
||||
protected readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.webSocketService.setServer(server);
|
||||
this.logger.log('WebSocket Gateway initialized');
|
||||
}
|
||||
|
||||
async handleConnection(client: Socket) {
|
||||
try {
|
||||
const token = client.handshake.auth.token;
|
||||
|
||||
if (!token) {
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.authService.validateToken(token);
|
||||
|
||||
if (!user) {
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
client.data.user = user;
|
||||
|
||||
// Rejoindre la salle personnelle de l'utilisateur
|
||||
this.webSocketService.joinRoom(client, `user:${user.id}`);
|
||||
|
||||
this.logger.log(`Client connected: ${client.id}, User: ${user.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Connection error: ${error.message}`);
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
handleDisconnect(client: Socket) {
|
||||
this.logger.log(`Client disconnected: ${client.id}`);
|
||||
}
|
||||
|
||||
getUserFromSocket(client: Socket) {
|
||||
return client.data.user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Gateway Projets
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/gateways/projects.gateway.ts
|
||||
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { BaseGateway } from './base.gateway';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { ProjectsService } from '../../projects/services/projects.service';
|
||||
|
||||
export class ProjectsGateway extends BaseGateway {
|
||||
constructor(
|
||||
protected readonly webSocketService: WebSocketService,
|
||||
protected readonly authService: AuthService,
|
||||
private readonly projectsService: ProjectsService,
|
||||
) {
|
||||
super(webSocketService, authService);
|
||||
}
|
||||
|
||||
@SubscribeMessage('project:join')
|
||||
async handleJoinProject(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { projectId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { projectId } = data;
|
||||
|
||||
// Vérifier si l'utilisateur a accès au projet
|
||||
const hasAccess = await this.projectsService.hasAccess(projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Rejoindre la salle du projet
|
||||
this.webSocketService.joinRoom(client, `project:${projectId}`);
|
||||
|
||||
this.logger.log(`User ${user.id} joined project ${projectId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error joining project: ${error.message}`);
|
||||
return { error: 'Failed to join project' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('project:leave')
|
||||
handleLeaveProject(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { projectId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { projectId } = data;
|
||||
|
||||
// Quitter la salle du projet
|
||||
this.webSocketService.leaveRoom(client, `project:${projectId}`);
|
||||
|
||||
this.logger.log(`User ${user.id} left project ${projectId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error leaving project: ${error.message}`);
|
||||
return { error: 'Failed to leave project' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('project:update')
|
||||
async handleUpdateProject(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { projectId: string, changes: any },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { projectId, changes } = data;
|
||||
|
||||
// Vérifier si l'utilisateur a accès au projet
|
||||
const hasAccess = await this.projectsService.hasAccess(projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Émettre l'événement de mise à jour à tous les clients dans la salle du projet
|
||||
this.webSocketService.emitToProject(projectId, 'project:updated', {
|
||||
projectId,
|
||||
changes,
|
||||
updatedBy: user.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating project: ${error.message}`);
|
||||
return { error: 'Failed to update project' };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Gateway Groupes
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/gateways/groups.gateway.ts
|
||||
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { BaseGateway } from './base.gateway';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { GroupsService } from '../../groups/services/groups.service';
|
||||
import { ProjectsService } from '../../projects/services/projects.service';
|
||||
|
||||
export class GroupsGateway extends BaseGateway {
|
||||
constructor(
|
||||
protected readonly webSocketService: WebSocketService,
|
||||
protected readonly authService: AuthService,
|
||||
private readonly groupsService: GroupsService,
|
||||
private readonly projectsService: ProjectsService,
|
||||
) {
|
||||
super(webSocketService, authService);
|
||||
}
|
||||
|
||||
@SubscribeMessage('group:join')
|
||||
async handleJoinGroup(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { groupId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { groupId } = data;
|
||||
|
||||
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
|
||||
const group = await this.groupsService.findById(groupId);
|
||||
|
||||
if (!group) {
|
||||
return { error: 'Group not found' };
|
||||
}
|
||||
|
||||
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Rejoindre la salle du groupe
|
||||
this.webSocketService.joinRoom(client, `group:${groupId}`);
|
||||
|
||||
this.logger.log(`User ${user.id} joined group ${groupId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error joining group: ${error.message}`);
|
||||
return { error: 'Failed to join group' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('group:leave')
|
||||
handleLeaveGroup(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { groupId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { groupId } = data;
|
||||
|
||||
// Quitter la salle du groupe
|
||||
this.webSocketService.leaveRoom(client, `group:${groupId}`);
|
||||
|
||||
this.logger.log(`User ${user.id} left group ${groupId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error leaving group: ${error.message}`);
|
||||
return { error: 'Failed to leave group' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('group:update')
|
||||
async handleUpdateGroup(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { groupId: string, changes: any },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { groupId, changes } = data;
|
||||
|
||||
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
|
||||
const group = await this.groupsService.findById(groupId);
|
||||
|
||||
if (!group) {
|
||||
return { error: 'Group not found' };
|
||||
}
|
||||
|
||||
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Émettre l'événement de mise à jour à tous les clients dans la salle du groupe
|
||||
this.webSocketService.emitToGroup(groupId, 'group:updated', {
|
||||
groupId,
|
||||
changes,
|
||||
updatedBy: user.id,
|
||||
});
|
||||
|
||||
// Émettre également l'événement au niveau du projet
|
||||
this.webSocketService.emitToProject(group.projectId, 'group:updated', {
|
||||
groupId,
|
||||
changes,
|
||||
updatedBy: user.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating group: ${error.message}`);
|
||||
return { error: 'Failed to update group' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('group:addPerson')
|
||||
async handleAddPersonToGroup(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { groupId: string, personId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { groupId, personId } = data;
|
||||
|
||||
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
|
||||
const group = await this.groupsService.findById(groupId);
|
||||
|
||||
if (!group) {
|
||||
return { error: 'Group not found' };
|
||||
}
|
||||
|
||||
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Émettre l'événement d'ajout de personne à tous les clients dans la salle du groupe
|
||||
this.webSocketService.emitToGroup(groupId, 'group:personAdded', {
|
||||
groupId,
|
||||
personId,
|
||||
addedBy: user.id,
|
||||
});
|
||||
|
||||
// Émettre également l'événement au niveau du projet
|
||||
this.webSocketService.emitToProject(group.projectId, 'group:personAdded', {
|
||||
groupId,
|
||||
personId,
|
||||
addedBy: user.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error adding person to group: ${error.message}`);
|
||||
return { error: 'Failed to add person to group' };
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('group:removePerson')
|
||||
async handleRemovePersonFromGroup(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { groupId: string, personId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { groupId, personId } = data;
|
||||
|
||||
// Récupérer le groupe et vérifier si l'utilisateur a accès au projet associé
|
||||
const group = await this.groupsService.findById(groupId);
|
||||
|
||||
if (!group) {
|
||||
return { error: 'Group not found' };
|
||||
}
|
||||
|
||||
const hasAccess = await this.projectsService.hasAccess(group.projectId, user.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
return { error: 'Access denied' };
|
||||
}
|
||||
|
||||
// Émettre l'événement de suppression de personne à tous les clients dans la salle du groupe
|
||||
this.webSocketService.emitToGroup(groupId, 'group:personRemoved', {
|
||||
groupId,
|
||||
personId,
|
||||
removedBy: user.id,
|
||||
});
|
||||
|
||||
// Émettre également l'événement au niveau du projet
|
||||
this.webSocketService.emitToProject(group.projectId, 'group:personRemoved', {
|
||||
groupId,
|
||||
personId,
|
||||
removedBy: user.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing person from group: ${error.message}`);
|
||||
return { error: 'Failed to remove person from group' };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Gateway Notifications
|
||||
|
||||
```typescript
|
||||
// src/modules/websockets/gateways/notifications.gateway.ts
|
||||
import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { BaseGateway } from './base.gateway';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
|
||||
export class NotificationsGateway extends BaseGateway {
|
||||
constructor(
|
||||
protected readonly webSocketService: WebSocketService,
|
||||
protected readonly authService: AuthService,
|
||||
) {
|
||||
super(webSocketService, authService);
|
||||
}
|
||||
|
||||
@SubscribeMessage('notification:read')
|
||||
handleReadNotification(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() data: { notificationId: string },
|
||||
) {
|
||||
try {
|
||||
const user = this.getUserFromSocket(client);
|
||||
const { notificationId } = data;
|
||||
|
||||
// Logique pour marquer la notification comme lue
|
||||
|
||||
this.logger.log(`User ${user.id} read notification ${notificationId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error reading notification: ${error.message}`);
|
||||
return { error: 'Failed to read notification' };
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour envoyer une notification à un utilisateur spécifique
|
||||
sendNotificationToUser(userId: string, notification: any) {
|
||||
this.webSocketService.emitToUser(userId, 'notification:new', notification);
|
||||
}
|
||||
|
||||
// Méthode pour envoyer une notification à tous les utilisateurs d'un projet
|
||||
sendNotificationToProject(projectId: string, notification: any) {
|
||||
this.webSocketService.emitToProject(projectId, 'notification:new', notification);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Intégration avec les Services Existants
|
||||
|
||||
### 6.1 Service Projets
|
||||
|
||||
```typescript
|
||||
// src/modules/projects/services/projects.service.ts
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { WebSocketService } from '../../websockets/services/websocket.service';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectsService {
|
||||
constructor(
|
||||
// Autres injections...
|
||||
private readonly webSocketService: WebSocketService,
|
||||
) {}
|
||||
|
||||
// Méthodes existantes...
|
||||
|
||||
async update(id: string, data: any, userId: string) {
|
||||
// Logique de mise à jour du projet
|
||||
|
||||
// Notification en temps réel
|
||||
this.webSocketService.emitToProject(id, 'project:updated', {
|
||||
projectId: id,
|
||||
changes: data,
|
||||
updatedBy: userId,
|
||||
});
|
||||
|
||||
return updatedProject;
|
||||
}
|
||||
|
||||
async addCollaborator(projectId: string, collaboratorId: string, role: string, userId: string) {
|
||||
// Logique d'ajout de collaborateur
|
||||
|
||||
// Notification en temps réel
|
||||
this.webSocketService.emitToProject(projectId, 'project:collaboratorAdded', {
|
||||
projectId,
|
||||
collaboratorId,
|
||||
role,
|
||||
addedBy: userId,
|
||||
});
|
||||
|
||||
// Notification personnelle au collaborateur
|
||||
this.webSocketService.emitToUser(collaboratorId, 'notification:new', {
|
||||
type: 'PROJECT_INVITATION',
|
||||
projectId,
|
||||
invitedBy: userId,
|
||||
role,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Service Groupes
|
||||
|
||||
```typescript
|
||||
// src/modules/groups/services/groups.service.ts
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { WebSocketService } from '../../websockets/services/websocket.service';
|
||||
|
||||
@Injectable()
|
||||
export class GroupsService {
|
||||
constructor(
|
||||
// Autres injections...
|
||||
private readonly webSocketService: WebSocketService,
|
||||
) {}
|
||||
|
||||
// Méthodes existantes...
|
||||
|
||||
async create(data: any, userId: string) {
|
||||
// Logique de création de groupe
|
||||
|
||||
// Notification en temps réel
|
||||
this.webSocketService.emitToProject(data.projectId, 'group:created', {
|
||||
groupId: createdGroup.id,
|
||||
group: createdGroup,
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
return createdGroup;
|
||||
}
|
||||
|
||||
async addPerson(groupId: string, personId: string, userId: string) {
|
||||
// Logique d'ajout de personne au groupe
|
||||
|
||||
const group = await this.findById(groupId);
|
||||
|
||||
// Notification en temps réel
|
||||
this.webSocketService.emitToGroup(groupId, 'group:personAdded', {
|
||||
groupId,
|
||||
personId,
|
||||
addedBy: userId,
|
||||
});
|
||||
|
||||
this.webSocketService.emitToProject(group.projectId, 'group:personAdded', {
|
||||
groupId,
|
||||
personId,
|
||||
addedBy: userId,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Intégration avec le Frontend
|
||||
|
||||
### 7.1 Configuration du Client Socket.IO
|
||||
|
||||
```typescript
|
||||
// frontend/lib/socket.ts
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
let socket: Socket | null = null;
|
||||
|
||||
export const initializeSocket = (token: string) => {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
socket = io(process.env.NEXT_PUBLIC_API_URL, {
|
||||
auth: { token },
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
return socket;
|
||||
};
|
||||
|
||||
export const getSocket = () => {
|
||||
return socket;
|
||||
};
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useSocket = (token: string | null) => {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
disconnectSocket();
|
||||
setIsConnected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const socketInstance = initializeSocket(token);
|
||||
|
||||
socketInstance.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socketInstance.off('connect');
|
||||
socketInstance.off('disconnect');
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
return { socket, isConnected };
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 Hook pour les Projets
|
||||
|
||||
```typescript
|
||||
// frontend/hooks/useProjectSocket.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSocket } from '../lib/socket';
|
||||
|
||||
export const useProjectSocket = (projectId: string | null) => {
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
const socket = getSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !projectId) {
|
||||
setIsJoined(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rejoindre la salle du projet
|
||||
socket.emit('project:join', { projectId }, (response) => {
|
||||
if (response.success) {
|
||||
setIsJoined(true);
|
||||
} else {
|
||||
console.error('Failed to join project:', response.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Quitter la salle du projet lors du démontage
|
||||
return () => {
|
||||
socket.emit('project:leave', { projectId });
|
||||
setIsJoined(false);
|
||||
};
|
||||
}, [socket, projectId]);
|
||||
|
||||
const updateProject = (changes: any) => {
|
||||
if (!socket || !projectId || !isJoined) {
|
||||
return Promise.reject('Not connected to project');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('project:update', { projectId, changes }, (response) => {
|
||||
if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { isJoined, updateProject };
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 Hook pour les Groupes
|
||||
|
||||
```typescript
|
||||
// frontend/hooks/useGroupSocket.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSocket } from '../lib/socket';
|
||||
|
||||
export const useGroupSocket = (groupId: string | null) => {
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
const socket = getSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !groupId) {
|
||||
setIsJoined(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rejoindre la salle du groupe
|
||||
socket.emit('group:join', { groupId }, (response) => {
|
||||
if (response.success) {
|
||||
setIsJoined(true);
|
||||
} else {
|
||||
console.error('Failed to join group:', response.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Quitter la salle du groupe lors du démontage
|
||||
return () => {
|
||||
socket.emit('group:leave', { groupId });
|
||||
setIsJoined(false);
|
||||
};
|
||||
}, [socket, groupId]);
|
||||
|
||||
const updateGroup = (changes: any) => {
|
||||
if (!socket || !groupId || !isJoined) {
|
||||
return Promise.reject('Not connected to group');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('group:update', { groupId, changes }, (response) => {
|
||||
if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addPersonToGroup = (personId: string) => {
|
||||
if (!socket || !groupId || !isJoined) {
|
||||
return Promise.reject('Not connected to group');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('group:addPerson', { groupId, personId }, (response) => {
|
||||
if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const removePersonFromGroup = (personId: string) => {
|
||||
if (!socket || !groupId || !isJoined) {
|
||||
return Promise.reject('Not connected to group');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('group:removePerson', { groupId, personId }, (response) => {
|
||||
if (response.success) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { isJoined, updateGroup, addPersonToGroup, removePersonFromGroup };
|
||||
};
|
||||
```
|
||||
|
||||
### 7.4 Hook pour les Notifications
|
||||
|
||||
```typescript
|
||||
// frontend/hooks/useNotifications.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSocket } from '../lib/socket';
|
||||
|
||||
export const useNotifications = () => {
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const socket = getSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Écouter les nouvelles notifications
|
||||
socket.on('notification:new', (notification) => {
|
||||
setNotifications((prev) => [notification, ...prev]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('notification:new');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const markAsRead = (notificationId: string) => {
|
||||
if (!socket) {
|
||||
Reference in New Issue
Block a user