Compare commits

..

3 Commits

Author SHA1 Message Date
9f99b80784 feat: implement authentication and database modules with relations and group management
Added new authentication strategies (JWT and GitHub OAuth), guards, and controllers. Implemented database module, schema with relations, and group management features, including CRD operations and person-to-group associations. Integrated validation and CORS configuration.
2025-05-15 17:09:36 +02:00
f6f0888bd7 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.
2025-05-15 17:08:53 +02:00
6d6ecdaec1 chore: add IntelliJ IDEA configuration and project files to version control
Included `.idea` configuration files, project module settings, VCS mappings, and default ignore rules for IntelliJ IDEA.
2025-05-15 14:01:01 +02:00
79 changed files with 5950 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/brief-20.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/discord.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/brief-20.iml" filepath="$PROJECT_DIR$/.idea/brief-20.iml" />
</modules>
</component>
</project>

13
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/backend" vcs="Git" />
</component>
</project>

56
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

36
backend/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM node:20-alpine AS base
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy package.json and install dependencies
FROM base AS dependencies
COPY package.json ./
RUN pnpm install
# Build the application
FROM dependencies AS build
COPY . .
RUN pnpm run build
# Production image
FROM node:20-alpine AS production
WORKDIR /app
# Copy built application
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Expose port
EXPOSE ${PORT}
# Start the application
CMD ["node", "dist/main"]

136
backend/README.md Normal file
View File

@ -0,0 +1,136 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ pnpm install
```
### Environment Variables
The application uses environment variables for configuration. Create a `.env` file in the root directory with the following variables:
```
# Database configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=bypass
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
# Node environment
NODE_ENV=development
# Application port
PORT=3000
```
These variables are used by:
- `database.service.ts` - For connecting to the database
- `drizzle.config.ts` - For database migrations
- `main.ts` - For setting the application port
When running in Docker, these variables are set in the `docker-compose.yml` file.
## Database Management
The application uses Drizzle ORM for database management. The following scripts are available:
```bash
# Generate database migrations
$ pnpm run db:generate
# Run database migrations
$ pnpm run db:migrate
```
## Compile and run the project
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Run tests
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ pnpm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

26
backend/drizzle.config.ts Normal file
View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2025 Yidhra Studio. - All Rights Reserved
* Updated : 25/04/2025 10:33
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
import { defineConfig } from 'drizzle-kit';
import * as process from "node:process";
export default defineConfig({
schema: './src/database/schema/index.ts',
out: './src/database/migrations',
dialect: "postgresql",
dbCredentials: {
host: String(process.env.POSTGRES_HOST || "localhost"),
port: Number(process.env.POSTGRES_PORT || 5432),
database: String(process.env.POSTGRES_DB || "groupmaker"),
user: String(process.env.POSTGRES_USER || "postgres"),
password: String(process.env.POSTGRES_PASSWORD || "<PASSWORD>"),
ssl: false,
},
verbose: true,
strict: true,
});

8
backend/nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

97
backend/package.json Normal file
View File

@ -0,0 +1,97 @@
{
"name": "backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "ts-node src/database/migrations/migrate.ts",
"db:generate:ts": "ts-node src/database/migrations/generate-migrations.ts",
"db:studio": "drizzle-kit studio",
"db:push": "drizzle-kit push:pg"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.1.1",
"@nestjs/websockets": "^11.1.1",
"@node-rs/argon2": "^2.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.30.4",
"jose": "^6.0.11",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-jwt": "^4.0.1",
"pg": "^8.16.0",
"postgres": "^3.4.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"uuid": "^11.1.0",
"zod": "^3.24.4",
"zod-validation-error": "^3.4.1"
},
"devDependencies": {
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/passport-github2": "^1.2.9",
"@types/pg": "^8.15.1",
"@types/socket.io": "^3.0.2",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"drizzle-kit": "^0.21.1",
"globals": "^16.0.0",
"jest": "^29.7.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

28
backend/src/app.module.ts Normal file
View File

@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './modules/users/users.module';
import { ProjectsModule } from './modules/projects/projects.module';
import { AuthModule } from './modules/auth/auth.module';
import { GroupsModule } from './modules/groups/groups.module';
import { TagsModule } from './modules/tags/tags.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
UsersModule,
ProjectsModule,
AuthModule,
GroupsModule,
TagsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,33 @@
import { Module, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';
import { DatabaseService } from './database.service';
export const DATABASE_POOL = 'DATABASE_POOL';
export const DRIZZLE = 'DRIZZLE';
@Global()
@Module({
imports: [ConfigModule],
providers: [
DatabaseService,
{
provide: DATABASE_POOL,
useFactory: (databaseService: DatabaseService) => {
return databaseService.getPool();
},
inject: [DatabaseService],
},
{
provide: DRIZZLE,
useFactory: (databaseService: DatabaseService) => {
return databaseService.getDb();
},
inject: [DatabaseService],
},
],
exports: [DatabaseService, DATABASE_POOL, DRIZZLE],
})
export class DatabaseModule {}

View File

@ -0,0 +1,97 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
import { runMigrations } from './migrations/migrate';
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly pool: Pool;
private readonly db: ReturnType<typeof drizzle>;
constructor(private configService: ConfigService) {
// Create the PostgreSQL pool
const connectionString = this.getDatabaseConnectionString();
this.pool = new Pool({
connectionString,
});
// Create the Drizzle ORM instance
this.db = drizzle(this.pool, { schema });
}
async onModuleInit() {
// Log database connection
console.log('Connecting to database...');
// Test the connection
try {
const client = await this.pool.connect();
try {
await client.query('SELECT NOW()');
console.log('Database connection established successfully');
} finally {
client.release();
}
} catch (error) {
console.error('Failed to connect to database:', error.message);
throw error;
}
// Run migrations in all environments
const result = await runMigrations({ migrationsFolder: './src/database/migrations' });
// In production, we want to fail if migrations fail
if (!result.success && this.configService.get('NODE_ENV') === 'production') {
throw result.error;
}
}
async onModuleDestroy() {
// Close the database connection
await this.pool.end();
console.log('Database connection closed');
}
// Get the database connection string from environment variables
private getDatabaseConnectionString(): string {
// First try to get the full DATABASE_URL
const databaseUrl = this.configService.get<string>('DATABASE_URL');
if (databaseUrl) {
return databaseUrl;
}
// If DATABASE_URL is not provided, construct it from individual variables
const password = this.configService.get<string>('POSTGRES_PASSWORD');
const username = this.configService.get<string>('POSTGRES_USER');
const host = this.configService.get<string>('POSTGRES_HOST');
const port = this.configService.get<string>('POSTGRES_PORT');
const database = this.configService.get<string>('POSTGRES_DB');
const missingVars: string[] = [];
if (!password) missingVars.push('POSTGRES_PASSWORD');
if (!username) missingVars.push('POSTGRES_USER');
if (!host) missingVars.push('POSTGRES_HOST');
if (!port) missingVars.push('POSTGRES_PORT');
if (!database) missingVars.push('POSTGRES_DB');
if (missingVars.length > 0) {
throw new Error(
`Database configuration is missing. Missing variables: ${missingVars.join(', ')}. Please check your .env file.`,
);
}
return `postgres://${username}:${password}@${host}:${port}/${database}`;
}
// Get the Drizzle ORM instance
getDb() {
return this.db;
}
// Get the PostgreSQL pool
getPool() {
return this.pool;
}
}

View File

@ -0,0 +1,55 @@
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
/**
* Script to generate migrations using drizzle-kit
*
* This script:
* 1. Runs drizzle-kit generate to create migration files
* 2. Ensures the migrations directory exists
* 3. Handles errors and provides feedback
*/
const main = async () => {
console.log('Generating migrations...');
// Ensure migrations directory exists
const migrationsDir = path.join(__dirname);
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
// Run drizzle-kit generate command
const command = 'npx drizzle-kit generate:pg';
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error generating migrations: ${error.message}`);
return;
}
if (stderr) {
console.error(`Migration generation stderr: ${stderr}`);
}
console.log(stdout);
console.log('Migrations generated successfully');
// List generated migration files
const files = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'));
if (files.length === 0) {
console.log('No migration files were generated. Your schema might be up to date.');
} else {
console.log('Generated migration files:');
files.forEach(file => console.log(`- ${file}`));
}
});
};
main().catch(err => {
console.error('Migration generation failed');
console.error(err);
process.exit(1);
});

View File

@ -0,0 +1,91 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
import * as dotenv from 'dotenv';
import * as path from 'path';
import * as schema from '../schema';
/**
* Script to run database migrations
*
* This script:
* 1. Establishes a connection to the PostgreSQL database
* 2. Creates a Drizzle ORM instance
* 3. Runs all pending migrations
*
* It can be used:
* - As a standalone script: `node dist/database/migrations/migrate.js`
* - Integrated with NestJS application lifecycle in database.service.ts
*/
// Load environment variables
dotenv.config();
export const runMigrations = async (options?: { migrationsFolder?: string }) => {
// First try to get the full DATABASE_URL
let connectionString = process.env.DATABASE_URL;
// If DATABASE_URL is not provided, construct it from individual variables
if (!connectionString) {
const password = process.env.POSTGRES_PASSWORD || 'postgres';
const username = process.env.POSTGRES_USER || 'postgres';
const host = process.env.POSTGRES_HOST || 'localhost';
const port = Number(process.env.POSTGRES_PORT || 5432);
const database = process.env.POSTGRES_DB || 'groupmaker';
connectionString = `postgres://${username}:${password}@${host}:${port}/${database}`;
}
// Create the PostgreSQL pool
const pool = new Pool({
connectionString,
});
// Create the Drizzle ORM instance
const db = drizzle(pool, { schema });
console.log('Running migrations...');
try {
// Test the connection
const client = await pool.connect();
try {
await client.query('SELECT NOW()');
console.log('Database connection established successfully');
} finally {
client.release();
}
// Determine migrations folder path
const migrationsFolder = options?.migrationsFolder || path.join(__dirname);
console.log(`Using migrations folder: ${migrationsFolder}`);
// Run migrations
await migrate(db, { migrationsFolder });
console.log('Migrations completed successfully');
return { success: true };
} catch (error) {
console.error('Migration failed');
console.error(error);
return { success: false, error };
} finally {
await pool.end();
console.log('Database connection closed');
}
};
// Run migrations if this script is executed directly
if (require.main === module) {
runMigrations()
.then(result => {
if (!result.success) {
process.exit(1);
}
})
.catch(err => {
console.error('Migration failed');
console.error(err);
process.exit(1);
});
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (C) 2025 Yidhra Studio. - All Rights Reserved
* Updated : 25/04/2025 10:33
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
import { pgSchema } from "drizzle-orm/pg-core";
/**
* Defines the PostgreSQL schema for the application.
* All database tables are created within this schema namespace.
* The schema name "bypass" is used to isolate the application's tables
* from other applications that might share the same database.
*/
export const DbSchema = pgSchema("groupmaker");

View File

@ -0,0 +1,16 @@
import { pgEnum } from 'drizzle-orm/pg-core';
/**
* Enum for gender values
*/
export const gender = pgEnum('gender', ['MALE', 'FEMALE', 'NON_BINARY']);
/**
* Enum for oral ease level values
*/
export const oralEaseLevel = pgEnum('oralEaseLevel', ['SHY', 'RESERVED', 'COMFORTABLE']);
/**
* Enum for tag types
*/
export const tagType = pgEnum('tagType', ['PROJECT', 'PERSON']);

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, varchar, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { projects } from './projects';
/**
* Groups table schema
*/
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('group_name_idx').on(table.name),
projectIdIdx: index('group_projectId_idx').on(table.projectId)
};
});
/**
* Group type definitions
*/
export type Group = typeof groups.$inferSelect;
export type NewGroup = typeof groups.$inferInsert;

View File

@ -0,0 +1,23 @@
/**
* This file serves as the main entry point for the database schema definitions.
* It exports all schema definitions from various modules.
*/
// Export schema
export * from './db-schema';
// Export enums
export * from './enums';
// Export tables
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 relations
export * from './relations';

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { groups } from './groups';
/**
* Person to Group relation table schema
*/
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('ptg_personId_idx').on(table.personId),
groupIdIdx: index('ptg_groupId_idx').on(table.groupId),
personGroupUniqueIdx: uniqueIndex('ptg_person_group_unique_idx').on(table.personId, table.groupId)
};
});
/**
* PersonToGroup type definitions
*/
export type PersonToGroup = typeof personToGroup.$inferSelect;
export type NewPersonToGroup = typeof personToGroup.$inferInsert;

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { tags } from './tags';
/**
* Person to Tag relation table schema
*/
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('ptt_personId_idx').on(table.personId),
tagIdIdx: index('ptt_tagId_idx').on(table.tagId),
personTagUniqueIdx: uniqueIndex('ptt_person_tag_unique_idx').on(table.personId, table.tagId)
};
});
/**
* PersonToTag type definitions
*/
export type PersonToTag = typeof personToTag.$inferSelect;
export type NewPersonToTag = typeof personToTag.$inferInsert;

View File

@ -0,0 +1,35 @@
import { pgTable, uuid, varchar, smallint, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { gender, oralEaseLevel } from './enums';
/**
* Persons table schema
*/
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('person_firstName_idx').on(table.firstName),
lastNameIdx: index('person_lastName_idx').on(table.lastName),
projectIdIdx: index('person_projectId_idx').on(table.projectId),
nameCompositeIdx: index('person_name_composite_idx').on(table.firstName, table.lastName)
};
});
/**
* Person type definitions
*/
export type Person = typeof persons.$inferSelect;
export type NewPerson = typeof persons.$inferInsert;

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { tags } from './tags';
/**
* Project to Tag relation table schema
*/
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('pjt_projectId_idx').on(table.projectId),
tagIdIdx: index('pjt_tagId_idx').on(table.tagId),
projectTagUniqueIdx: uniqueIndex('pjt_project_tag_unique_idx').on(table.projectId, table.tagId)
};
});
/**
* ProjectToTag type definitions
*/
export type ProjectToTag = typeof projectToTag.$inferSelect;
export type NewProjectToTag = typeof projectToTag.$inferInsert;

View File

@ -0,0 +1,27 @@
import { pgTable, uuid, varchar, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './users';
/**
* Projects table schema
*/
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('project_name_idx').on(table.name),
ownerIdIdx: index('project_ownerId_idx').on(table.ownerId),
createdAtIdx: index('project_createdAt_idx').on(table.createdAt)
};
});
/**
* Project type definitions
*/
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;

View File

@ -0,0 +1,102 @@
import { relations } from 'drizzle-orm';
import { users } from './users';
import { projects } from './projects';
import { persons } from './persons';
import { groups } from './groups';
import { tags } from './tags';
import { personToGroup } from './personToGroup';
import { personToTag } from './personToTag';
import { projectToTag } from './projectToTag';
/**
* Define relations for users table
*/
export const usersRelations = relations(users, ({ many }) => ({
projects: many(projects),
}));
/**
* Define relations for projects table
*/
export const projectsRelations = relations(projects, ({ one, many }) => ({
owner: one(users, {
fields: [projects.ownerId],
references: [users.id],
}),
persons: many(persons),
groups: many(groups),
projectToTags: many(projectToTag),
}));
/**
* Define relations for persons table
*/
export const personsRelations = relations(persons, ({ one, many }) => ({
project: one(projects, {
fields: [persons.projectId],
references: [projects.id],
}),
personToGroups: many(personToGroup),
personToTags: many(personToTag),
}));
/**
* Define relations for groups table
*/
export const groupsRelations = relations(groups, ({ one, many }) => ({
project: one(projects, {
fields: [groups.projectId],
references: [projects.id],
}),
personToGroups: many(personToGroup),
}));
/**
* Define relations for tags table
*/
export const tagsRelations = relations(tags, ({ many }) => ({
personToTags: many(personToTag),
projectToTags: many(projectToTag),
}));
/**
* Define relations for personToGroup table
*/
export const personToGroupRelations = relations(personToGroup, ({ one }) => ({
person: one(persons, {
fields: [personToGroup.personId],
references: [persons.id],
}),
group: one(groups, {
fields: [personToGroup.groupId],
references: [groups.id],
}),
}));
/**
* Define relations for personToTag table
*/
export const personToTagRelations = relations(personToTag, ({ one }) => ({
person: one(persons, {
fields: [personToTag.personId],
references: [persons.id],
}),
tag: one(tags, {
fields: [personToTag.tagId],
references: [tags.id],
}),
}));
/**
* Define relations for projectToTag table
*/
export const projectToTagRelations = relations(projectToTag, ({ one }) => ({
project: one(projects, {
fields: [projectToTag.projectId],
references: [projects.id],
}),
tag: one(tags, {
fields: [projectToTag.tagId],
references: [tags.id],
}),
}));

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, varchar, timestamp, index } from 'drizzle-orm/pg-core';
import { tagType } from './enums';
/**
* Tags table schema
*/
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('tag_name_idx').on(table.name),
typeIdx: index('tag_type_idx').on(table.type)
};
});
/**
* Tag type definitions
*/
export type Tag = typeof tags.$inferSelect;
export type NewTag = typeof tags.$inferInsert;

View File

@ -0,0 +1,27 @@
import { pgTable, uuid, varchar, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { DbSchema } from './db-schema';
/**
* Users table schema
*/
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), // UUIDv7 for chronological order
name: varchar('name', { length: 100 }).notNull(),
avatar: text('avatar'), // URL from Github API
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)
};
});
/**
* User type definitions
*/
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

33
backend/src/main.ts Normal file
View File

@ -0,0 +1,33 @@
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', 'http://localhost:3000'),
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();

View File

@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
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';
@Module({
imports: [
ConfigModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION') || '15m',
},
}),
}),
UsersModule,
],
controllers: [AuthController],
providers: [
AuthService,
GithubStrategy,
JwtStrategy,
JwtRefreshStrategy,
],
exports: [AuthService, JwtStrategy, JwtRefreshStrategy, PassportModule],
})
export class AuthModule {}

View File

@ -0,0 +1,78 @@
import {
Body,
Controller,
Get,
Post,
Req,
Res,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response } from 'express';
import { AuthService } from '../services/auth.service';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { GithubAuthGuard } from '../guards/github-auth.guard';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
import { GetUser } from '../decorators/get-user.decorator';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
) {}
/**
* Initiate GitHub OAuth flow
*/
@Get('github')
@UseGuards(GithubAuthGuard)
githubAuth() {
// This route is handled by the GitHub strategy
// The actual implementation is in the GithubAuthGuard
}
/**
* Handle GitHub OAuth callback
*/
@Get('github/callback')
@UseGuards(GithubAuthGuard)
async githubAuthCallback(@Req() req: Request, @Res() res: Response) {
// The user is already validated by the GitHub strategy
// and attached to the request object
const user = req.user as any;
if (!user) {
throw new UnauthorizedException('Authentication failed');
}
// Generate tokens
const tokens = await this.authService.generateTokens(user.id);
// Redirect to the frontend with tokens
const frontendUrl = this.configService.get<string>('FRONTEND_URL') || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/auth/callback?accessToken=${tokens.accessToken}&refreshToken=${tokens.refreshToken}`;
return res.redirect(redirectUrl);
}
/**
* Refresh tokens
*/
@Post('refresh')
@UseGuards(JwtRefreshGuard)
async refreshTokens(@GetUser() user) {
return this.authService.refreshTokens(user.id, user.refreshToken);
}
/**
* Get current user profile
*/
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@GetUser() user) {
return user;
}
}

View File

@ -0,0 +1,18 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
* Decorator to extract user information from the request
*
* Usage:
* - @GetUser() user: any - Get the entire user object
* - @GetUser('id') userId: string - Get a specific property from the user object
*/
export const GetUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// Return the specific property if data is provided, otherwise return the entire user object
return data ? user?.[data] : user;
},
);

View File

@ -0,0 +1,13 @@
import { IsNotEmpty, IsString } from 'class-validator';
/**
* DTO for refresh token request
*/
export class RefreshTokenDto {
/**
* The refresh token
*/
@IsNotEmpty()
@IsString()
refreshToken: string;
}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
/**
* Guard for GitHub OAuth authentication
*/
@Injectable()
export class GithubAuthGuard extends AuthGuard('github') {}

View File

@ -0,0 +1,18 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
/**
* Guard for JWT authentication
*/
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
/**
* Handle unauthorized errors
*/
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
throw err || new UnauthorizedException('Authentication required');
}
return user;
}
}

View File

@ -0,0 +1,18 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
/**
* Guard for JWT refresh token authentication
*/
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {
/**
* Handle unauthorized errors
*/
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
throw err || new UnauthorizedException('Valid refresh token required');
}
return user;
}
}

View File

@ -0,0 +1,24 @@
/**
* Interface for JWT payload
*/
export interface JwtPayload {
/**
* Subject (user ID)
*/
sub: string;
/**
* Flag to indicate if this is a refresh token
*/
isRefreshToken?: boolean;
/**
* Token issued at timestamp
*/
iat?: number;
/**
* Token expiration timestamp
*/
exp?: number;
}

View File

@ -0,0 +1,14 @@
/**
* Interface for tokens response
*/
export interface TokensResponse {
/**
* JWT access token
*/
accessToken: string;
/**
* JWT refresh token
*/
refreshToken: string;
}

View File

@ -0,0 +1,96 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../../users/services/users.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { TokensResponse } from '../interfaces/tokens-response.interface';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
/**
* Validate a user by GitHub ID
*/
async validateGithubUser(
githubId: string,
email: string,
name: string,
avatarUrl: string,
) {
// Try to find the user by GitHub ID
let user = await this.usersService.findByGithubId(githubId);
// If user doesn't exist, create a new one
if (!user) {
user = await this.usersService.create({
githubId,
name,
avatar: avatarUrl,
metadata: { email },
});
}
return user;
}
/**
* Generate JWT tokens (access and refresh)
*/
async generateTokens(userId: string): Promise<TokensResponse> {
const payload: JwtPayload = { sub: userId };
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload),
this.jwtService.signAsync(
{ ...payload, isRefreshToken: true },
{
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRATION') || '7d',
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
},
),
]);
return {
accessToken,
refreshToken,
};
}
/**
* Refresh tokens using a valid refresh token
*/
async refreshTokens(userId: string, refreshToken: string): Promise<TokensResponse> {
// Verify the refresh token
try {
const payload = await this.jwtService.verifyAsync(refreshToken, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
});
// Check if the token is a refresh token and belongs to the user
if (!payload.isRefreshToken || payload.sub !== userId) {
throw new UnauthorizedException('Invalid refresh token');
}
// Generate new tokens
return this.generateTokens(userId);
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
/**
* Validate a user by JWT payload
*/
async validateJwtUser(payload: JwtPayload) {
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
return user;
}
}

View File

@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
import { AuthService } from '../services/auth.service';
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
) {
const clientID = configService.get<string>('GITHUB_CLIENT_ID') || 'dummy-client-id';
const clientSecret = configService.get<string>('GITHUB_CLIENT_SECRET') || 'dummy-client-secret';
const callbackURL = configService.get<string>('GITHUB_CALLBACK_URL') || 'http://localhost:3001/api/auth/github/callback';
super({
clientID,
clientSecret,
callbackURL,
scope: ['user:email'],
});
}
/**
* Validate the GitHub profile and return the user
*/
async validate(accessToken: string, refreshToken: string, profile: any) {
// Extract user information from GitHub profile
const { id, displayName, emails, photos } = profile;
// Get primary email or first email
const email = emails && emails.length > 0
? (emails.find(e => e.primary)?.value || emails[0].value)
: null;
// Get avatar URL
const avatarUrl = photos && photos.length > 0 ? photos[0].value : null;
// Validate or create user
const user = await this.authService.validateGithubUser(
id,
email,
displayName || 'GitHub User',
avatarUrl,
);
return user;
}
}

View File

@ -0,0 +1,51 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../services/auth.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true,
});
}
/**
* Validate the JWT refresh token payload and return the user
*/
async validate(req: any, payload: JwtPayload) {
try {
// Check if this is a refresh token
if (!payload.isRefreshToken) {
throw new UnauthorizedException('Invalid token type');
}
// Extract the refresh token from the request
const refreshToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
if (!refreshToken) {
throw new UnauthorizedException('Refresh token not found');
}
// Validate the user
const user = await this.authService.validateJwtUser(payload);
// Attach the refresh token to the user object for later use
return {
...user,
refreshToken,
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}

View File

@ -0,0 +1,38 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../services/auth.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
/**
* Validate the JWT payload and return the user
*/
async validate(payload: JwtPayload) {
try {
// Check if this is a refresh token
if (payload.isRefreshToken) {
throw new UnauthorizedException('Invalid token type');
}
// Validate the user
const user = await this.authService.validateJwtUser(payload);
return user;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}

View File

@ -0,0 +1,94 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
UseGuards,
Query,
} from '@nestjs/common';
import { GroupsService } from '../services/groups.service';
import { CreateGroupDto } from '../dto/create-group.dto';
import { UpdateGroupDto } from '../dto/update-group.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
@Controller('groups')
@UseGuards(JwtAuthGuard)
export class GroupsController {
constructor(private readonly groupsService: GroupsService) {}
/**
* Create a new group
*/
@Post()
create(@Body() createGroupDto: CreateGroupDto) {
return this.groupsService.create(createGroupDto);
}
/**
* Get all groups or filter by project ID
*/
@Get()
findAll(@Query('projectId') projectId?: string) {
if (projectId) {
return this.groupsService.findByProjectId(projectId);
}
return this.groupsService.findAll();
}
/**
* Get a group by ID
*/
@Get(':id')
findOne(@Param('id') id: string) {
return this.groupsService.findById(id);
}
/**
* Update a group
*/
@Put(':id')
update(@Param('id') id: string, @Body() updateGroupDto: UpdateGroupDto) {
return this.groupsService.update(id, updateGroupDto);
}
/**
* Delete a group
*/
@Delete(':id')
remove(@Param('id') id: string) {
return this.groupsService.remove(id);
}
/**
* Add a person to a group
*/
@Post(':id/persons/:personId')
addPersonToGroup(
@Param('id') groupId: string,
@Param('personId') personId: string,
) {
return this.groupsService.addPersonToGroup(groupId, personId);
}
/**
* Remove a person from a group
*/
@Delete(':id/persons/:personId')
removePersonFromGroup(
@Param('id') groupId: string,
@Param('personId') personId: string,
) {
return this.groupsService.removePersonFromGroup(groupId, personId);
}
/**
* Get all persons in a group
*/
@Get(':id/persons')
getPersonsInGroup(@Param('id') groupId: string) {
return this.groupsService.getPersonsInGroup(groupId);
}
}

View File

@ -0,0 +1,27 @@
import { IsNotEmpty, IsString, IsUUID, IsObject, IsOptional } from 'class-validator';
/**
* DTO for creating a new group
*/
export class CreateGroupDto {
/**
* The name of the group
*/
@IsNotEmpty()
@IsString()
name: string;
/**
* The ID of the project this group belongs to
*/
@IsNotEmpty()
@IsUUID()
projectId: string;
/**
* Optional metadata for the group
*/
@IsOptional()
@IsObject()
metadata?: Record<string, any>;
}

View File

@ -0,0 +1,27 @@
import { IsString, IsUUID, IsObject, IsOptional } from 'class-validator';
/**
* DTO for updating an existing group
*/
export class UpdateGroupDto {
/**
* The name of the group
*/
@IsOptional()
@IsString()
name?: string;
/**
* The ID of the project this group belongs to
*/
@IsOptional()
@IsUUID()
projectId?: string;
/**
* Metadata for the group
*/
@IsOptional()
@IsObject()
metadata?: Record<string, any>;
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { GroupsController } from './controllers/groups.controller';
import { GroupsService } from './services/groups.service';
@Module({
controllers: [GroupsController],
providers: [GroupsService],
exports: [GroupsService],
})
export class GroupsModule {}

View File

@ -0,0 +1,167 @@
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { DRIZZLE } from '../../../database/database.module';
import * as schema from '../../../database/schema';
import { CreateGroupDto } from '../dto/create-group.dto';
import { UpdateGroupDto } from '../dto/update-group.dto';
@Injectable()
export class GroupsService {
constructor(@Inject(DRIZZLE) private readonly db: any) {}
/**
* Create a new group
*/
async create(createGroupDto: CreateGroupDto) {
const [group] = await this.db
.insert(schema.groups)
.values({
...createGroupDto,
})
.returning();
return group;
}
/**
* Find all groups
*/
async findAll() {
return this.db.select().from(schema.groups);
}
/**
* Find groups by project ID
*/
async findByProjectId(projectId: string) {
return this.db
.select()
.from(schema.groups)
.where(eq(schema.groups.projectId, projectId));
}
/**
* Find a group by ID
*/
async findById(id: string) {
const [group] = await this.db
.select()
.from(schema.groups)
.where(eq(schema.groups.id, id));
if (!group) {
throw new NotFoundException(`Group with ID ${id} not found`);
}
return group;
}
/**
* Update a group
*/
async update(id: string, updateGroupDto: UpdateGroupDto) {
const [group] = await this.db
.update(schema.groups)
.set({
...updateGroupDto,
updatedAt: new Date(),
})
.where(eq(schema.groups.id, id))
.returning();
if (!group) {
throw new NotFoundException(`Group with ID ${id} not found`);
}
return group;
}
/**
* Delete a group
*/
async remove(id: string) {
const [group] = await this.db
.delete(schema.groups)
.where(eq(schema.groups.id, id))
.returning();
if (!group) {
throw new NotFoundException(`Group with ID ${id} not found`);
}
return group;
}
/**
* Add a person to a group
*/
async addPersonToGroup(groupId: string, personId: string) {
// Check if the group exists
await this.findById(groupId);
// Check if the person exists
const [person] = await this.db
.select()
.from(schema.persons)
.where(eq(schema.persons.id, personId));
if (!person) {
throw new NotFoundException(`Person with ID ${personId} not found`);
}
// Check if the person is already in the group
const [existingRelation] = await this.db
.select()
.from(schema.personToGroup)
.where(eq(schema.personToGroup.personId, personId))
.where(eq(schema.personToGroup.groupId, groupId));
if (existingRelation) {
return existingRelation;
}
// Add the person to the group
const [relation] = await this.db
.insert(schema.personToGroup)
.values({
personId,
groupId,
})
.returning();
return relation;
}
/**
* Remove a person from a group
*/
async removePersonFromGroup(groupId: string, personId: string) {
const [relation] = await this.db
.delete(schema.personToGroup)
.where(eq(schema.personToGroup.personId, personId))
.where(eq(schema.personToGroup.groupId, groupId))
.returning();
if (!relation) {
throw new NotFoundException(`Person with ID ${personId} is not in group with ID ${groupId}`);
}
return relation;
}
/**
* Get all persons in a group
*/
async getPersonsInGroup(groupId: string) {
// Check if the group exists
await this.findById(groupId);
// Get all persons in the group
return this.db
.select({
person: schema.persons,
})
.from(schema.personToGroup)
.innerJoin(schema.persons, eq(schema.personToGroup.personId, schema.persons.id))
.where(eq(schema.personToGroup.groupId, groupId));
}
}

View File

@ -0,0 +1,94 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
HttpStatus,
Query,
} from '@nestjs/common';
import { PersonsService } from '../services/persons.service';
import { CreatePersonDto } from '../dto/create-person.dto';
import { UpdatePersonDto } from '../dto/update-person.dto';
@Controller('persons')
export class PersonsController {
constructor(private readonly personsService: PersonsService) {}
/**
* Create a new person
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createPersonDto: CreatePersonDto) {
return this.personsService.create(createPersonDto);
}
/**
* Get all persons or filter by project ID
*/
@Get()
findAll(@Query('projectId') projectId?: string) {
if (projectId) {
return this.personsService.findByProjectId(projectId);
}
return this.personsService.findAll();
}
/**
* Get a person by ID
*/
@Get(':id')
findOne(@Param('id') id: string) {
return this.personsService.findById(id);
}
/**
* Update a person
*/
@Patch(':id')
update(@Param('id') id: string, @Body() updatePersonDto: UpdatePersonDto) {
return this.personsService.update(id, updatePersonDto);
}
/**
* Delete a person
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.personsService.remove(id);
}
/**
* Get persons by project ID and group ID
*/
@Get('project/:projectId/group/:groupId')
findByProjectIdAndGroupId(
@Param('projectId') projectId: string,
@Param('groupId') groupId: string,
) {
return this.personsService.findByProjectIdAndGroupId(projectId, groupId);
}
/**
* Add a person to a group
*/
@Post(':id/groups/:groupId')
@HttpCode(HttpStatus.CREATED)
addToGroup(@Param('id') id: string, @Param('groupId') groupId: string) {
return this.personsService.addToGroup(id, groupId);
}
/**
* Remove a person from a group
*/
@Delete(':id/groups/:groupId')
@HttpCode(HttpStatus.NO_CONTENT)
removeFromGroup(@Param('id') id: string, @Param('groupId') groupId: string) {
return this.personsService.removeFromGroup(id, groupId);
}
}

View File

@ -0,0 +1,83 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsObject,
IsUUID,
IsEnum,
IsInt,
IsBoolean,
Min,
Max
} from 'class-validator';
import { Type } from 'class-transformer';
/**
* Enum for gender values
*/
export enum Gender {
MALE = 'MALE',
FEMALE = 'FEMALE',
NON_BINARY = 'NON_BINARY',
}
/**
* Enum for oral ease level values
*/
export enum OralEaseLevel {
SHY = 'SHY',
RESERVED = 'RESERVED',
COMFORTABLE = 'COMFORTABLE',
}
/**
* DTO for creating a new person
*/
export class CreatePersonDto {
@IsString()
@IsNotEmpty()
firstName: string;
@IsString()
@IsNotEmpty()
lastName: string;
@IsEnum(Gender)
@IsNotEmpty()
gender: Gender;
@IsInt()
@Min(1)
@Max(5)
@Type(() => Number)
technicalLevel: number;
@IsBoolean()
@Type(() => Boolean)
hasTechnicalTraining: boolean;
@IsInt()
@Min(1)
@Max(5)
@Type(() => Number)
frenchSpeakingLevel: number;
@IsEnum(OralEaseLevel)
@IsNotEmpty()
oralEaseLevel: OralEaseLevel;
@IsInt()
@IsOptional()
@Min(18)
@Max(100)
@Type(() => Number)
age?: number;
@IsUUID()
@IsNotEmpty()
projectId: string;
@IsObject()
@IsOptional()
attributes?: Record<string, any>;
}

View File

@ -0,0 +1,68 @@
import {
IsString,
IsOptional,
IsObject,
IsUUID,
IsEnum,
IsInt,
IsBoolean,
Min,
Max
} from 'class-validator';
import { Type } from 'class-transformer';
import { Gender, OralEaseLevel } from './create-person.dto';
/**
* DTO for updating a person
*/
export class UpdatePersonDto {
@IsString()
@IsOptional()
firstName?: string;
@IsString()
@IsOptional()
lastName?: string;
@IsEnum(Gender)
@IsOptional()
gender?: Gender;
@IsInt()
@Min(1)
@Max(5)
@IsOptional()
@Type(() => Number)
technicalLevel?: number;
@IsBoolean()
@IsOptional()
@Type(() => Boolean)
hasTechnicalTraining?: boolean;
@IsInt()
@Min(1)
@Max(5)
@IsOptional()
@Type(() => Number)
frenchSpeakingLevel?: number;
@IsEnum(OralEaseLevel)
@IsOptional()
oralEaseLevel?: OralEaseLevel;
@IsInt()
@IsOptional()
@Min(18)
@Max(100)
@Type(() => Number)
age?: number;
@IsUUID()
@IsOptional()
projectId?: string;
@IsObject()
@IsOptional()
attributes?: Record<string, any>;
}

View File

@ -0,0 +1,145 @@
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DRIZZLE } from '../../../database/database.module';
import * as schema from '../../../database/schema';
import { CreatePersonDto } from '../dto/create-person.dto';
import { UpdatePersonDto } from '../dto/update-person.dto';
@Injectable()
export class PersonsService {
constructor(@Inject(DRIZZLE) private readonly db: any) {}
/**
* Create a new person
*/
async create(createPersonDto: CreatePersonDto) {
const [person] = await this.db
.insert(schema.persons)
.values(createPersonDto)
.returning();
return person;
}
/**
* Find all persons
*/
async findAll() {
return this.db.select().from(schema.persons);
}
/**
* Find persons by project ID
*/
async findByProjectId(projectId: string) {
return this.db
.select()
.from(schema.persons)
.where(eq(schema.persons.projectId, projectId));
}
/**
* Find a person by ID
*/
async findById(id: string) {
const [person] = await this.db
.select()
.from(schema.persons)
.where(eq(schema.persons.id, id));
if (!person) {
throw new NotFoundException(`Person with ID ${id} not found`);
}
return person;
}
/**
* Update a person
*/
async update(id: string, updatePersonDto: UpdatePersonDto) {
const [person] = await this.db
.update(schema.persons)
.set({
...updatePersonDto,
updatedAt: new Date(),
})
.where(eq(schema.persons.id, id))
.returning();
if (!person) {
throw new NotFoundException(`Person with ID ${id} not found`);
}
return person;
}
/**
* Delete a person
*/
async remove(id: string) {
const [person] = await this.db
.delete(schema.persons)
.where(eq(schema.persons.id, id))
.returning();
if (!person) {
throw new NotFoundException(`Person with ID ${id} not found`);
}
return person;
}
/**
* Find persons by project ID and group ID
*/
async findByProjectIdAndGroupId(projectId: string, groupId: string) {
return this.db
.select({
person: schema.persons,
})
.from(schema.persons)
.innerJoin(
schema.personToGroup,
and(
eq(schema.persons.id, schema.personToGroup.personId),
eq(schema.personToGroup.groupId, groupId)
)
)
.where(eq(schema.persons.projectId, projectId));
}
/**
* Add a person to a group
*/
async addToGroup(personId: string, groupId: string) {
const [relation] = await this.db
.insert(schema.personToGroup)
.values({
personId,
groupId,
})
.returning();
return relation;
}
/**
* Remove a person from a group
*/
async removeFromGroup(personId: string, groupId: string) {
const [relation] = await this.db
.delete(schema.personToGroup)
.where(
and(
eq(schema.personToGroup.personId, personId),
eq(schema.personToGroup.groupId, groupId)
)
)
.returning();
if (!relation) {
throw new NotFoundException(`Person with ID ${personId} not found in group with ID ${groupId}`);
}
return relation;
}
}

View File

@ -0,0 +1,73 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
HttpStatus,
Query,
} from '@nestjs/common';
import { ProjectsService } from '../services/projects.service';
import { CreateProjectDto } from '../dto/create-project.dto';
import { UpdateProjectDto } from '../dto/update-project.dto';
@Controller('projects')
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
/**
* Create a new project
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createProjectDto: CreateProjectDto) {
return this.projectsService.create(createProjectDto);
}
/**
* Get all projects or filter by owner ID
*/
@Get()
findAll(@Query('ownerId') ownerId?: string) {
if (ownerId) {
return this.projectsService.findByOwnerId(ownerId);
}
return this.projectsService.findAll();
}
/**
* Get a project by ID
*/
@Get(':id')
findOne(@Param('id') id: string) {
return this.projectsService.findById(id);
}
/**
* Update a project
*/
@Patch(':id')
update(@Param('id') id: string, @Body() updateProjectDto: UpdateProjectDto) {
return this.projectsService.update(id, updateProjectDto);
}
/**
* Delete a project
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.projectsService.remove(id);
}
/**
* Check if a user has access to a project
*/
@Get(':id/check-access/:userId')
checkUserAccess(@Param('id') id: string, @Param('userId') userId: string) {
return this.projectsService.checkUserAccess(id, userId);
}
}

View File

@ -0,0 +1,22 @@
import { IsString, IsNotEmpty, IsOptional, IsObject, IsUUID } from 'class-validator';
/**
* DTO for creating a new project
*/
export class CreateProjectDto {
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
description?: string;
@IsUUID()
@IsNotEmpty()
ownerId: string;
@IsObject()
@IsOptional()
settings?: Record<string, any>;
}

View File

@ -0,0 +1,22 @@
import { IsString, IsOptional, IsObject, IsUUID } from 'class-validator';
/**
* DTO for updating a project
*/
export class UpdateProjectDto {
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
description?: string;
@IsUUID()
@IsOptional()
ownerId?: string;
@IsObject()
@IsOptional()
settings?: Record<string, any>;
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ProjectsController } from './controllers/projects.controller';
import { ProjectsService } from './services/projects.service';
@Module({
controllers: [ProjectsController],
providers: [ProjectsService],
exports: [ProjectsService],
})
export class ProjectsModule {}

View File

@ -0,0 +1,108 @@
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DRIZZLE } from '../../../database/database.module';
import * as schema from '../../../database/schema';
import { CreateProjectDto } from '../dto/create-project.dto';
import { UpdateProjectDto } from '../dto/update-project.dto';
@Injectable()
export class ProjectsService {
constructor(@Inject(DRIZZLE) private readonly db: any) {}
/**
* Create a new project
*/
async create(createProjectDto: CreateProjectDto) {
const [project] = await this.db
.insert(schema.projects)
.values(createProjectDto)
.returning();
return project;
}
/**
* Find all projects
*/
async findAll() {
return this.db.select().from(schema.projects);
}
/**
* Find projects by owner ID
*/
async findByOwnerId(ownerId: string) {
return this.db
.select()
.from(schema.projects)
.where(eq(schema.projects.ownerId, ownerId));
}
/**
* Find a project by ID
*/
async findById(id: string) {
const [project] = await this.db
.select()
.from(schema.projects)
.where(eq(schema.projects.id, id));
if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
return project;
}
/**
* Update a project
*/
async update(id: string, updateProjectDto: UpdateProjectDto) {
const [project] = await this.db
.update(schema.projects)
.set({
...updateProjectDto,
updatedAt: new Date(),
})
.where(eq(schema.projects.id, id))
.returning();
if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
return project;
}
/**
* Delete a project
*/
async remove(id: string) {
const [project] = await this.db
.delete(schema.projects)
.where(eq(schema.projects.id, id))
.returning();
if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
return project;
}
/**
* Check if a user has access to a project
*/
async checkUserAccess(projectId: string, userId: string) {
const [project] = await this.db
.select()
.from(schema.projects)
.where(
and(
eq(schema.projects.id, projectId),
eq(schema.projects.ownerId, userId)
)
);
return !!project;
}
}

View File

@ -0,0 +1,77 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UsersService } from '../services/users.service';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
/**
* Create a new user
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
/**
* Get all users
*/
@Get()
findAll() {
return this.usersService.findAll();
}
/**
* Get a user by ID
*/
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findById(id);
}
/**
* Update a user
*/
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
/**
* Delete a user
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
/**
* Update GDPR consent timestamp
*/
@Post(':id/gdpr-consent')
updateGdprConsent(@Param('id') id: string) {
return this.usersService.updateGdprConsent(id);
}
/**
* Export user data (for GDPR compliance)
*/
@Get(':id/export-data')
exportUserData(@Param('id') id: string) {
return this.usersService.exportUserData(id);
}
}

View File

@ -0,0 +1,22 @@
import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator';
/**
* DTO for creating a new user
*/
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
avatar?: string;
@IsString()
@IsNotEmpty()
githubId: string;
@IsObject()
@IsOptional()
metadata?: Record<string, any>;
}

View File

@ -0,0 +1,28 @@
import { IsString, IsOptional, IsObject, IsDate } from 'class-validator';
import { Type } from 'class-transformer';
/**
* DTO for updating a user
*/
export class UpdateUserDto {
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
avatar?: string;
@IsString()
@IsOptional()
githubId?: string;
@IsDate()
@IsOptional()
@Type(() => Date)
gdprTimestamp?: Date;
@IsObject()
@IsOptional()
metadata?: Record<string, any>;
}

View File

@ -0,0 +1,119 @@
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { DRIZZLE } from '../../../database/database.module';
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) {}
/**
* Create a new user
*/
async create(createUserDto: CreateUserDto) {
const [user] = await this.db
.insert(schema.users)
.values({
...createUserDto,
gdprTimestamp: new Date(),
})
.returning();
return user;
}
/**
* Find all users
*/
async findAll() {
return this.db.select().from(schema.users);
}
/**
* Find a user by ID
*/
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;
}
/**
* Find a user by GitHub ID
*/
async findByGithubId(githubId: string) {
const [user] = await this.db
.select()
.from(schema.users)
.where(eq(schema.users.githubId, githubId));
return user;
}
/**
* Update a 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;
}
/**
* Delete a 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;
}
/**
* Update GDPR consent timestamp
*/
async updateGdprConsent(id: string) {
return this.update(id, { gdprTimestamp: new Date() });
}
/**
* Export user data (for GDPR compliance)
*/
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,
};
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { UsersController } from './controllers/users.controller';
import { UsersService } from './services/users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
backend/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2022",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@ -0,0 +1,148 @@
# Business Flow Diagrams
This document contains diagrams illustrating the main business flows of the "Application de Création de Groupes" project.
## Authentication Flow
```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
```
## Project Creation Flow
```mermaid
sequenceDiagram
participant User as Utilisateur
participant Frontend as Frontend (Next.js)
participant Backend as Backend (NestJS)
participant DB as Base de données
User->>Frontend: Crée un nouveau projet
Frontend->>Backend: POST /api/projects
Backend->>Backend: Valide les données
Backend->>DB: Insère le projet
DB->>Backend: Confirme l'insertion
Backend->>Frontend: Retourne le projet créé
Frontend->>User: Affiche le projet créé
```
## Person Management Flow
```mermaid
sequenceDiagram
participant User as Utilisateur
participant Frontend as Frontend (Next.js)
participant Backend as Backend (NestJS)
participant DB as Base de données
User->>Frontend: Ajoute une personne au projet
Frontend->>Backend: POST /api/projects/{id}/persons
Backend->>Backend: Valide les données
Backend->>DB: Insère la personne
DB->>Backend: Confirme l'insertion
Backend->>Frontend: Retourne la personne créée
Frontend->>User: Affiche la personne dans le projet
User->>Frontend: Modifie les attributs d'une personne
Frontend->>Backend: PATCH /api/persons/{id}
Backend->>Backend: Valide les données
Backend->>DB: Met à jour la personne
DB->>Backend: Confirme la mise à jour
Backend->>Frontend: Retourne la personne mise à jour
Frontend->>User: Affiche la personne mise à jour
```
## Group Creation Flow
```mermaid
sequenceDiagram
participant User as Utilisateur
participant Frontend as Frontend (Next.js)
participant Backend as Backend (NestJS)
participant DB as Base de données
alt Création manuelle
User->>Frontend: Crée un groupe manuellement
Frontend->>Backend: POST /api/projects/{id}/groups
Backend->>Backend: Valide les données
Backend->>DB: Insère le groupe
DB->>Backend: Confirme l'insertion
Backend->>Frontend: Retourne le groupe créé
Frontend->>User: Affiche le groupe créé
User->>Frontend: Ajoute des personnes au groupe (drag & drop)
Frontend->>Backend: PATCH /api/groups/{id}/persons
Backend->>Backend: Valide les données
Backend->>DB: Met à jour les relations groupe-personnes
DB->>Backend: Confirme la mise à jour
Backend->>Frontend: Retourne le groupe mis à jour
Frontend->>User: Affiche le groupe mis à jour
else Création automatique
User->>Frontend: Demande la création automatique de groupes
Frontend->>Backend: POST /api/projects/{id}/auto-groups
Backend->>Backend: Exécute l'algorithme de création de groupes
Backend->>DB: Insère les groupes générés
DB->>Backend: Confirme l'insertion
Backend->>Frontend: Retourne les groupes créés
Frontend->>User: Affiche les groupes créés
end
```
## Real-time Collaboration Flow
```mermaid
sequenceDiagram
participant User1 as Utilisateur 1
participant User2 as Utilisateur 2
participant Frontend1 as Frontend (User 1)
participant Frontend2 as Frontend (User 2)
participant Backend as Backend (NestJS)
participant WebSocket as WebSocket Server
participant DB as Base de données
User1->>Frontend1: Modifie un projet
Frontend1->>Backend: PATCH /api/projects/{id}
Backend->>DB: Met à jour le projet
DB->>Backend: Confirme la mise à jour
Backend->>WebSocket: Émet événement "project:updated"
WebSocket->>Frontend2: Transmet événement "project:updated"
Frontend2->>User2: Affiche les modifications en temps réel
```
## Export Data Flow (GDPR)
```mermaid
sequenceDiagram
participant User as Utilisateur
participant Frontend as Frontend (Next.js)
participant Backend as Backend (NestJS)
participant DB as Base de données
User->>Frontend: Demande l'export de ses données
Frontend->>Backend: GET /api/users/me/export
Backend->>DB: Récupère toutes les données de l'utilisateur
DB->>Backend: Retourne les données
Backend->>Backend: Formate les données en JSON
Backend->>Frontend: Retourne le fichier JSON
Frontend->>User: Télécharge le fichier de données
```

View File

@ -0,0 +1,104 @@
# Guide d'Implémentation
Ce document sert de guide complet pour l'implémentation de l'application "Application de Création de Groupes". Il regroupe les différents plans d'implémentation et fournit une feuille de route claire pour le développement.
## Vue d'Ensemble
L'application est construite avec une architecture moderne, utilisant NestJS pour le backend et Next.js pour le frontend. Elle permet aux utilisateurs de créer et gérer des groupes de personnes selon différents critères, avec des fonctionnalités de collaboration en temps réel.
## Plans d'Implémentation Détaillés
Pour faciliter le développement, nous avons divisé l'implémentation en plusieurs plans détaillés, chacun se concentrant sur un aspect spécifique de l'application :
1. [Plan d'Implémentation du Backend](implementation/BACKEND_IMPLEMENTATION_PLAN.md) - Plan détaillé pour l'implémentation du backend avec NestJS
2. [Plan du Schéma de Base de Données](implementation/DATABASE_SCHEMA_PLAN.md) - Définition du schéma de base de données avec DrizzleORM
3. [Plan d'Implémentation de l'Authentification](implementation/AUTH_IMPLEMENTATION_PLAN.md) - Détails sur l'implémentation de l'authentification OAuth avec GitHub
4. [Plan d'Implémentation des WebSockets](implementation/WEBSOCKET_IMPLEMENTATION_PLAN.md) - Plan pour la communication en temps réel
## Ordre d'Implémentation Recommandé
Pour une implémentation efficace, nous recommandons de suivre l'ordre suivant :
### Phase 1 : Configuration et Base de Données
1. Configurer l'environnement de développement
2. Mettre en place la structure de base du projet NestJS
3. Implémenter le schéma de base de données avec DrizzleORM
4. Configurer le système de migrations
### Phase 2 : Authentification et Utilisateurs
1. Implémenter l'authentification OAuth avec GitHub
2. Mettre en place la gestion des JWT
3. Développer le module utilisateurs
4. Configurer les guards et décorateurs pour la protection des routes
### Phase 3 : Modules Principaux
1. Implémenter le module projets
2. Implémenter le module personnes
3. Implémenter le module groupes
4. Implémenter le module tags
5. Établir les relations entre les modules
### Phase 4 : Communication en Temps Réel
1. Configurer Socket.IO avec NestJS
2. Implémenter les gateways WebSocket
3. Mettre en place la gestion des salles et des événements
4. Intégrer les WebSockets avec les services existants
### Phase 5 : Frontend
1. Configurer la structure de base du projet Next.js
2. Implémenter les pages d'authentification
3. Développer les pages principales (projets, personnes, groupes)
4. Intégrer les fonctionnalités de collaboration en temps réel
### Phase 6 : Tests et Documentation
1. Écrire des tests unitaires et e2e
2. Documenter l'API avec Swagger
3. Finaliser la documentation utilisateur
## Bonnes Pratiques de Développement
### Architecture et Structure
- Suivre le principe de responsabilité unique (SRP)
- Utiliser l'injection de dépendances pour faciliter les tests
- Séparer clairement les couches (contrôleurs, services, repositories)
- Utiliser des DTOs pour la validation des entrées
### Sécurité
- Valider toutes les entrées utilisateur
- Utiliser des tokens JWT avec une durée de vie limitée
- Mettre en place des protections contre les attaques courantes (CSRF, XSS, injections SQL)
- Respecter les principes de la RGPD
### Performance
- Optimiser les requêtes de base de données
- Utiliser le caching lorsque c'est approprié
- Implémenter la pagination pour les listes volumineuses
- Optimiser les assets frontend (lazy loading, code splitting)
### Collaboration
- Suivre les conventions de nommage
- Documenter le code
- Utiliser des messages de commit descriptifs
- Effectuer des revues de code régulières
## Outils et Ressources
### Outils de Développement
- **IDE** : Visual Studio Code avec les extensions appropriées
- **Gestion de Packages** : PNPM pour la gestion des dépendances
- **Base de Données** : PostgreSQL avec DrizzleORM
- **API Testing** : Postman ou Insomnia
- **Versioning** : Git avec GitHub
### Ressources Utiles
- [Documentation NestJS](https://docs.nestjs.com/)
- [Documentation Next.js](https://nextjs.org/docs)
- [Documentation DrizzleORM](https://orm.drizzle.team/docs/overview)
- [Documentation Socket.IO](https://socket.io/docs/v4/)
- [Documentation OAuth 2.0](https://oauth.net/2/)
## Conclusion
Ce guide d'implémentation fournit une feuille de route complète pour le développement de l'application. En suivant les plans détaillés et les bonnes pratiques recommandées, vous pourrez construire une application robuste, sécurisée et performante.
Pour plus de détails sur l'état actuel du projet et les tâches restantes, consultez le document [État d'Avancement du Projet](PROJECT_STATUS.md).

127
docs/PROJECT_OVERVIEW.md Normal file
View File

@ -0,0 +1,127 @@
# Comprehensive Project Overview
## Introduction
This document provides a comprehensive analysis of the "Application de Création de Groupes" project, examining its architecture, technologies, features, and implementation details.
## Project Purpose
The application is designed to facilitate the creation and management of groups of people based on various criteria. It allows users to create projects, add people with different attributes, and organize them into groups either manually or automatically using balancing algorithms.
## Architecture Overview
The project follows a modern full-stack architecture with clear separation between frontend and backend:
### Frontend (Next.js)
- Uses Next.js with App Router pattern
- Implements ShadcnUI for consistent UI components
- Utilizes SWR for data fetching with caching
- Implements real-time updates using Socket.IO client
- Follows a component-based architecture with custom hooks
### Backend (NestJS)
- Built with NestJS framework for scalable server-side applications
- Uses PostgreSQL with DrizzleORM for database operations
- Implements OAuth 2.0 with GitHub for authentication
- Uses JWT for session management
- Provides WebSocket support via Socket.IO for real-time collaboration
- Follows modular architecture with clear separation of concerns
### Database
- PostgreSQL with DrizzleORM
- Well-defined schema with proper relationships
- Optimized data types and indexing strategy
- Support for migrations
### Communication
- REST API for CRUD operations
- WebSockets for real-time updates and collaboration
- JWT-based authentication for securing both REST and WebSocket endpoints
## Key Features
1. **User Authentication**
- OAuth 2.0 with GitHub
- JWT-based session management
- Role-based access control
2. **Project Management**
- Create, read, update, delete projects
- Associate tags with projects
- Track project history
3. **Person Management**
- Add people with various attributes (technical level, gender, language skills, etc.)
- Tag people for easier categorization
- Track person attributes
4. **Group Creation**
- Manual creation via drag-and-drop interface
- Automatic creation using balancing algorithms
- Real-time collaboration between users
5. **Real-time Collaboration**
- See changes made by other users in real-time
- Notifications for important events
- Room-based communication for project-specific updates
## Implementation Details
### Authentication Flow
The authentication flow uses OAuth 2.0 with GitHub as the identity provider:
1. User clicks "Login with GitHub"
2. User is redirected to GitHub for authorization
3. GitHub redirects back to the application with an authorization code
4. Backend exchanges the code for an access token
5. Backend retrieves user information from GitHub
6. Backend creates or updates the user in the database
7. Backend generates JWT tokens (access and refresh)
8. Frontend stores the tokens and uses them for subsequent requests
### Database Schema
The database schema includes the following main entities:
1. Users - Storing user information
2. Projects - Storing project information
3. Persons - Storing information about people to be placed in groups
4. Groups - Storing information about created groups
5. Tags - For categorizing persons and projects
6. Relation tables - For many-to-many relationships
### WebSocket Implementation
The WebSocket implementation uses Socket.IO for real-time communication:
1. Authentication using JWT
2. Room-based communication for project-specific updates
3. Event-based messaging for different types of updates
4. Proper connection and disconnection handling
### Deployment
The application is containerized using Docker, with separate containers for:
1. Frontend (Next.js)
2. Backend (NestJS)
3. PostgreSQL database
## Development Workflow
The project uses:
- PNPM for package management
- ESLint and Prettier for code quality
- TypeScript for type safety
- Jest for testing
- Docker for containerization
- Drizzle for database migrations
## Security Considerations
The application implements several security measures:
1. OAuth 2.0 for secure authentication
2. JWT with short-lived access tokens and refresh tokens
3. CORS configuration to prevent unauthorized access
4. Input validation using class-validator
5. Protection against common attacks (CSRF, XSS, SQL injection)
6. GDPR compliance features
## Performance Optimization
The application is optimized for performance:
1. Efficient database schema with proper indexing
2. Optimized data types for reduced storage requirements
3. Caching strategies for frequently accessed data
4. Lazy loading of components and data
## Conclusion
The "Application de Création de Groupes" is a well-designed, modern web application that follows best practices in software development. It provides a comprehensive solution for creating and managing groups of people, with a focus on user experience, security, and performance.
The clear separation between frontend and backend, the use of modern technologies, and the implementation of real-time collaboration features make it a robust and scalable application that can be extended with additional features in the future.

241
docs/PROJECT_STATUS.md Normal file
View File

@ -0,0 +1,241 @@
# État d'Avancement du Projet
Ce document présente l'état d'avancement actuel du projet "Application de Création de Groupes", les tâches restantes et les prochaines étapes prioritaires.
## 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 qui fournissent une base solide pour le développement, avec des instructions détaillées pour chaque composant du système.
## État Actuel
### Backend
#### Composants Implémentés
- ✅ Structure de base du projet NestJS
- ✅ Configuration de l'environnement et des variables d'environnement
- ✅ Schéma de base de données complet avec DrizzleORM
- ✅ Module de base de données configuré
- ✅ Module utilisateurs (contrôleurs, services, DTOs)
- ✅ Module projets (contrôleurs, services, DTOs)
- ✅ Module personnes (contrôleurs, services, DTOs)
- ✅ Configuration Docker pour le déploiement
#### Composants En Cours
- ⏳ Système de migrations de base de données
- ⏳ Relations entre les modules existants
#### Composants Non Implémentés
- ❌ Module d'authentification avec GitHub OAuth
- ❌ Stratégies JWT pour la gestion des sessions
- ❌ Guards et décorateurs pour la protection des routes
- ❌ Module groupes
- ❌ Module tags
- ❌ Communication en temps réel avec Socket.IO
- ❌ Fonctionnalités de conformité RGPD
- ❌ Tests unitaires et e2e
- ❌ Documentation API avec Swagger
### Frontend
#### Composants Implémentés
- ✅ Structure de base du projet Next.js
- ✅ Configuration de ShadcnUI pour les composants UI
- ✅ Configuration Docker pour le déploiement
#### Composants Non Implémentés
- ❌ Pages d'authentification (login, callback)
- ❌ Page d'accueil et tableau de bord
- ❌ Pages de gestion de projets
- ❌ Pages de gestion de personnes
- ❌ Pages de création et gestion de groupes
- ❌ Fonctionnalités de collaboration en temps réel
- ❌ Optimisations de performance et d'expérience utilisateur
## Tâches Restantes
### Backend
#### Priorité Haute
##### Migrations de Base de Données
- [ ] Configurer le système de migrations avec DrizzleORM
- [ ] Générer les migrations initiales
- [ ] Créer un script pour exécuter les migrations automatiquement au démarrage
##### Authentification
- [ ] Implémenter le module d'authentification
- [ ] Configurer l'authentification OAuth avec GitHub
- [ ] Implémenter les stratégies JWT pour la gestion des sessions
- [ ] Créer les guards et décorateurs pour la protection des routes
- [ ] Implémenter le refresh token
##### Modules Manquants
- [ ] Implémenter le module groupes (contrôleurs, services, DTOs)
- [ ] Implémenter le module tags (contrôleurs, services, DTOs)
- [ ] Compléter les relations entre les modules existants
#### Priorité Moyenne
##### Communication en Temps Réel
- [ ] Configurer Socket.IO avec NestJS
- [ ] Implémenter les gateways WebSocket pour les projets
- [ ] Implémenter les gateways WebSocket pour les groupes
- [ ] Implémenter les gateways WebSocket pour les notifications
- [ ] Mettre en place le service WebSocket pour la gestion des connexions
##### Sécurité et Conformité RGPD
- [ ] Implémenter la validation des entrées avec class-validator
- [ ] Configurer CORS pour sécuriser les API
- [ ] Mettre en place la protection contre les attaques CSRF
- [ ] Implémenter les fonctionnalités d'export de données utilisateur (RGPD)
- [ ] Implémenter le renouvellement du consentement utilisateur
#### Priorité Basse
##### Tests et Documentation
- [ ] Écrire des tests unitaires pour les services
- [ ] Écrire des tests unitaires pour les contrôleurs
- [ ] Développer des tests e2e pour les API
- [ ] Configurer Swagger pour la documentation API
- [ ] Documenter les endpoints API
### Frontend
#### Priorité Haute
##### Authentification
- [ ] Créer la page de login avec le bouton "Login with GitHub"
- [ ] Implémenter la page de callback OAuth
- [ ] Configurer le stockage sécurisé des tokens JWT
- [ ] Implémenter la logique de refresh token
- [ ] Créer les composants de protection des routes authentifiées
##### Pages Principales
- [ ] Implémenter la page d'accueil
- [ ] Créer le tableau de bord utilisateur
- [ ] Développer les pages de gestion de projets (liste, création, détail, édition)
- [ ] Développer les pages de gestion de personnes (liste, création, détail, édition)
- [ ] Implémenter les pages de création et gestion de groupes
#### Priorité Moyenne
##### Fonctionnalités Avancées
- [ ] Implémenter l'interface de création manuelle de groupes (drag-and-drop)
- [ ] Développer l'assistant de création automatique de groupes équilibrés
- [ ] Intégrer les fonctionnalités de collaboration en temps réel avec Socket.IO
- [ ] Implémenter le système de notifications en temps réel
- [ ] Créer les composants pour la gestion des tags
##### Expérience Utilisateur
- [ ] Améliorer la réactivité de l'interface
- [ ] Implémenter les animations et transitions
- [ ] Optimiser les formulaires avec React Hook Form
- [ ] Ajouter des retours visuels pour les actions utilisateur
- [ ] Implémenter la gestion des erreurs et les messages d'information
#### Priorité Basse
##### Optimisation et Finalisation
- [ ] Optimiser les performances (lazy loading, code splitting)
- [ ] Implémenter le mode hors ligne pour certaines fonctionnalités
- [ ] Ajouter le support pour les thèmes (clair/sombre)
- [ ] Optimiser pour les appareils mobiles
- [ ] Réaliser des tests d'accessibilité et corriger les problèmes identifiés
### Intégration et Déploiement
#### Priorité Haute
- [ ] Finaliser la configuration Docker Compose pour le développement local
- [ ] Configurer les variables d'environnement pour les différents environnements
- [ ] Mettre en place un environnement de staging
#### Priorité Moyenne
- [ ] Configurer le monitoring et les alertes
- [ ] Mettre en place un système de logging centralisé
- [ ] Configurer les sauvegardes automatiques de la base de données
#### Priorité Basse
- [ ] Optimiser les images Docker pour la production
- [ ] Configurer un CDN pour les assets statiques
- [ ] Mettre en place un système de déploiement blue/green
## Prochaines Étapes Prioritaires
### Backend (Priorité Haute)
1. **Migrations de Base de Données**
- Configurer le système de migrations avec DrizzleORM
- Générer les migrations initiales
- Créer un script pour exécuter les migrations automatiquement
2. **Authentification**
- Implémenter le module d'authentification avec GitHub OAuth
- Configurer les stratégies JWT pour la gestion des sessions
- Créer les guards et décorateurs pour la protection des routes
3. **Modules Manquants**
- Implémenter le module groupes
- Implémenter le module tags
- Compléter les relations entre les modules existants
### Frontend (Priorité Haute)
1. **Authentification**
- Créer la page de login avec le bouton "Login with GitHub"
- Implémenter la page de callback OAuth
- Configurer le stockage sécurisé des tokens JWT
2. **Pages Principales**
- Implémenter la page d'accueil
- Créer le tableau de bord utilisateur
- Développer les pages de gestion de projets et de personnes
## Progression Globale
| Composant | Progression |
|-----------|-------------|
| Backend - Structure de Base | 90% |
| Backend - Base de Données | 80% |
| Backend - Modules Fonctionnels | 60% |
| Backend - Authentification | 0% |
| Backend - WebSockets | 0% |
| Backend - Tests et Documentation | 0% |
| Frontend - Structure de Base | 70% |
| Frontend - Pages et Composants | 10% |
| Frontend - Authentification | 0% |
| Frontend - Fonctionnalités Avancées | 0% |
| Déploiement | 70% |
## Estimation du Temps Restant
Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du temps nécessaire pour compléter le projet est la suivante:
- **Backend**: ~4-5 semaines
- Authentification: 1 semaine
- Modules manquants: 1-2 semaines
- WebSockets: 1 semaine
- Tests et documentation: 1 semaine
- **Frontend**: ~5-6 semaines
- Authentification: 1 semaine
- Pages principales: 2 semaines
- Fonctionnalités avancées: 1-2 semaines
- Optimisation et finalisation: 1 semaine
- **Intégration et Tests**: ~1-2 semaines
**Temps total estimé**: 10-13 semaines
## 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.
## Conclusion
Le projet a bien avancé sur la structure de base et la définition du schéma de données, mais il reste encore un travail significatif à réaliser. Les prochaines étapes prioritaires devraient se concentrer sur l'authentification et les fonctionnalités de base pour avoir rapidement une version minimale fonctionnelle.

46
docs/README.md Normal file
View File

@ -0,0 +1,46 @@
# Documentation du Projet
Ce répertoire contient la documentation complète pour l'application "Application de Création de Groupes". La documentation a été organisée de manière à faciliter la navigation et la compréhension du projet.
## Structure de la Documentation
### Vue d'Ensemble
- [Vue d'Ensemble du Projet](PROJECT_OVERVIEW.md) - Analyse complète de l'architecture, des technologies et des fonctionnalités
- [État d'Avancement du Projet](PROJECT_STATUS.md) - État actuel, tâches restantes et prochaines étapes
- [Diagrammes de Flux Métier](BUSINESS_FLOW_DIAGRAMS.md) - Diagrammes de séquence pour les principaux flux métier
- [Cahier des Charges](SPECIFICATIONS.md) - Spécifications initiales du projet
### Guides d'Implémentation
- [Guide d'Implémentation](IMPLEMENTATION_GUIDE.md) - Guide complet pour l'implémentation de l'application
- [Plans d'Implémentation Détaillés](implementation/README.md) - Plans détaillés pour chaque composant du système
## Organisation des Fichiers
La documentation est organisée comme suit :
```
docs/
├── PROJECT_OVERVIEW.md # Vue d'ensemble du projet
├── PROJECT_STATUS.md # État d'avancement et tâches restantes
├── BUSINESS_FLOW_DIAGRAMS.md # Diagrammes de flux métier
├── SPECIFICATIONS.md # Cahier des charges
├── IMPLEMENTATION_GUIDE.md # Guide d'implémentation
├── implementation/ # Plans d'implémentation détaillés
│ ├── README.md # Index des plans d'implémentation
│ ├── BACKEND_IMPLEMENTATION_PLAN.md
│ ├── DATABASE_SCHEMA_PLAN.md
│ ├── AUTH_IMPLEMENTATION_PLAN.md
│ └── WEBSOCKET_IMPLEMENTATION_PLAN.md
└── README.md # Ce fichier
```
## Utilisation de la Documentation
- Commencez par la [Vue d'Ensemble du Projet](PROJECT_OVERVIEW.md) pour comprendre l'architecture et les fonctionnalités de l'application
- Consultez l'[État d'Avancement du Projet](PROJECT_STATUS.md) pour connaître l'état actuel et les prochaines étapes
- Référez-vous au [Guide d'Implémentation](IMPLEMENTATION_GUIDE.md) pour obtenir des instructions détaillées sur l'implémentation de l'application
- Explorez les [Plans d'Implémentation Détaillés](implementation/README.md) pour des informations spécifiques sur chaque composant du système
## Maintenance de la Documentation
La documentation doit être maintenue à jour au fur et à mesure que le projet évolue. Assurez-vous de mettre à jour les documents pertinents lorsque vous apportez des modifications significatives au code ou à l'architecture.

283
docs/SPECIFICATIONS.md Normal file
View File

@ -0,0 +1,283 @@
# Cahier des Charges - Application de Création de Groupes
*Note: Ce document est une copie du fichier cdc.md original, renommé en SPECIFICATIONS.md pour une meilleure organisation de la documentation.*
## Introduction
Ce document présente le cahier des charges pour le développement d'une application web dédiée à la création et à la gestion de groupes. L'application permettra aux utilisateurs de créer des groupes selon différents critères et de conserver un historique des groupes précédemment créés.
## 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
## Fonctionnalités Principales
### Gestion des Utilisateurs
- Authentification via OAuth2.0 avec GitHub
- Gestion des sessions utilisateur avec JWT
- Gestion des autorisations basée sur les rôles (RBAC)
- Profil utilisateur personnalisable
- Tableau de bord personnel avec statistiques d'utilisation
- Export des données personnelles (RGPD)
### Création et Gestion de Groupes
- Création de projets de groupe avec liste de personnes
- Attribution de tags aux personnes
- Définition d'échelles de niveau personnalisées
- Interface de création manuelle de groupes
- Assistant à la création automatique de groupes équilibrés
- Collaboration en temps réel entre utilisateurs
### Administration
- Tableau de bord administrateur
- Gestion des tags globaux
- Surveillance de l'activité des utilisateurs
- Gestion des rôles et permissions
## Spécifications Techniques
### Architecture
L'application suivra une architecture monorepo avec séparation claire entre le frontend et le backend:
- **Backend**: NestJS avec PostgreSQL et DrizzleORM
- **Frontend**: Next.js avec ShadcnUI et SWR
- **Authentification**: OAuth2.0 + OIDC via GitHub
- **Communication en temps réel**: Socket.IO
- **Déploiement**: Docker et Gitea Actions
### Modèle de Données
#### Entités Principales
1. **User**: Utilisateurs de l'application
- id (UUID)
- githubId (string)
- name (string)
- avatar (string)
- role (enum: USER, ADMIN)
- gdprTimestamp (datetime)
- createdAt (datetime)
- updatedAt (datetime)
2. **Project**: Projets de création de groupes
- id (UUID)
- name (string)
- description (text)
- settings (JSON)
- userId (UUID, foreign key)
- isPublic (boolean)
- createdAt (datetime)
- updatedAt (datetime)
3. **Person**: Personnes à placer dans les groupes
- id (UUID)
- name (string)
- email (string)
- technicalLevel (integer)
- gender (string)
- attributes (JSON)
- projectId (UUID, foreign key)
- createdAt (datetime)
- updatedAt (datetime)
4. **Group**: Groupes créés dans le cadre d'un projet
- id (UUID)
- name (string)
- description (text)
- settings (JSON)
- projectId (UUID, foreign key)
- createdAt (datetime)
- updatedAt (datetime)
5. **Tag**: Étiquettes pour catégoriser les personnes et les projets
- id (UUID)
- name (string)
- description (text)
- color (string)
- type (enum: PROJECT, PERSON)
- createdAt (datetime)
- updatedAt (datetime)
#### Relations
- **ProjectCollaborators**: Relation many-to-many entre Project et User
- **PersonToGroup**: Relation many-to-many entre Person et Group
- **PersonToTag**: Relation many-to-many entre Person et Tag
- **ProjectToTag**: Relation many-to-many entre Project et Tag
### API REST
L'API REST suivra les principes RESTful avec les endpoints suivants:
#### Authentification
- `GET /api/auth/github`: Redirection vers GitHub pour l'authentification
- `GET /api/auth/github/callback`: Callback OAuth GitHub
- `POST /api/auth/refresh`: Rafraîchissement du token JWT
- `POST /api/auth/logout`: Déconnexion
- `GET /api/auth/profile`: Récupération du profil utilisateur
#### Utilisateurs
- `GET /api/users`: Liste des utilisateurs (admin)
- `GET /api/users/:id`: Détails d'un utilisateur
- `PATCH /api/users/:id`: Mise à jour d'un utilisateur
- `DELETE /api/users/:id`: Suppression d'un utilisateur
- `GET /api/users/me/export`: Export des données personnelles (RGPD)
#### Projets
- `GET /api/projects`: Liste des projets
- `POST /api/projects`: Création d'un projet
- `GET /api/projects/:id`: Détails d'un projet
- `PATCH /api/projects/:id`: Mise à jour d'un projet
- `DELETE /api/projects/:id`: Suppression d'un projet
- `POST /api/projects/:id/collaborators`: Ajout d'un collaborateur
- `DELETE /api/projects/:id/collaborators/:userId`: Suppression d'un collaborateur
#### Personnes
- `GET /api/projects/:id/persons`: Liste des personnes d'un projet
- `POST /api/projects/:id/persons`: Ajout d'une personne à un projet
- `GET /api/persons/:id`: Détails d'une personne
- `PATCH /api/persons/:id`: Mise à jour d'une personne
- `DELETE /api/persons/:id`: Suppression d'une personne
- `POST /api/persons/:id/tags`: Ajout d'un tag à une personne
- `DELETE /api/persons/:id/tags/:tagId`: Suppression d'un tag d'une personne
#### Groupes
- `GET /api/projects/:id/groups`: Liste des groupes d'un projet
- `POST /api/projects/:id/groups`: Création d'un groupe
- `GET /api/groups/:id`: Détails d'un groupe
- `PATCH /api/groups/:id`: Mise à jour d'un groupe
- `DELETE /api/groups/:id`: Suppression d'un groupe
- `POST /api/groups/:id/persons`: Ajout d'une personne à un groupe
- `DELETE /api/groups/:id/persons/:personId`: Suppression d'une personne d'un groupe
- `POST /api/projects/:id/auto-groups`: Création automatique de groupes
#### Tags
- `GET /api/tags`: Liste des tags
- `POST /api/tags`: Création d'un tag
- `GET /api/tags/:id`: Détails d'un tag
- `PATCH /api/tags/:id`: Mise à jour d'un tag
- `DELETE /api/tags/:id`: Suppression d'un tag
### WebSockets
L'application utilisera Socket.IO pour la communication en temps réel avec les événements suivants:
- `project:updated`: Mise à jour d'un projet
- `project:collaboratorAdded`: Ajout d'un collaborateur à un projet
- `group:created`: Création d'un groupe
- `group:updated`: Mise à jour d'un groupe
- `group:personAdded`: Ajout d'une personne à un groupe
- `group:personRemoved`: Suppression d'une personne d'un groupe
- `notification:new`: Nouvelle notification
### Interface Utilisateur
L'interface utilisateur sera développée avec Next.js et ShadcnUI, offrant une expérience responsive et intuitive:
#### Pages Principales
- **Page d'accueil**: Présentation de l'application
- **Tableau de bord**: Vue d'ensemble des projets et activités récentes
- **Page de projet**: Détails d'un projet avec liste des personnes et groupes
- **Éditeur de groupe**: Interface drag-and-drop pour la création manuelle de groupes
- **Assistant de groupe**: Interface pour la création automatique de groupes équilibrés
- **Profil utilisateur**: Gestion des informations personnelles et préférences
#### Composants UI
- Barre de navigation responsive
- Sidebar pour la navigation entre les sections
- Modals pour les formulaires de création et d'édition
- Tableaux interactifs avec tri et filtrage
- Interface drag-and-drop pour la gestion des groupes
- Notifications en temps réel
- Thème clair/sombre
## Sécurité et Conformité
### Sécurité
- Authentification sécurisée via OAuth2.0
- Tokens JWT avec durée de vie limitée
- Protection CSRF
- Validation des entrées utilisateur
- Protection contre les injections SQL
- Rate limiting pour prévenir les attaques par force brute
### Conformité RGPD
- Minimisation des données collectées
- Finalité claire de la collecte de données
- Mise en œuvre des droits des utilisateurs (accès, rectification, effacement, portabilité)
- Renouvellement du consentement utilisateur tous les 13 mois
## Performance
### Objectifs
- Temps de chargement initial < 2 secondes (95ème percentile)
- Temps de réponse API < 300ms (95ème percentile)
- Disponibilité > 99.9%
- Support de 1000 utilisateurs simultanés minimum
### Optimisations
- Utilisation efficiente des index pour les requêtes fréquentes
- Mise en cache des requêtes fréquentes
- Optimisation des requêtes N+1
- Monitoring et alerting automatique
## Livrables
- Code source complet de l'application (backend et frontend)
- Documentation technique
- Documentation utilisateur
- Scripts de déploiement Docker
- Configuration CI/CD
## Calendrier
Le développement de l'application sera réalisé en plusieurs phases:
1. **Phase 1 (2 semaines)**: Configuration et base de données
- Mise en place de l'environnement de développement
- Implémentation du schéma de base de données
- Configuration de l'authentification
2. **Phase 2 (3 semaines)**: Développement des fonctionnalités de base
- Implémentation des modules backend
- Développement des pages frontend principales
- Intégration de l'authentification
3. **Phase 3 (2 semaines)**: Fonctionnalités avancées
- Implémentation de la communication en temps réel
- Développement de l'assistant de création de groupes
- Mise en place des fonctionnalités de collaboration
4. **Phase 4 (1 semaine)**: Tests et optimisation
- Tests unitaires et e2e
- Optimisation des performances
- Correction des bugs
5. **Phase 5 (1 semaine)**: Déploiement et documentation
- Configuration du déploiement Docker
- Mise en place du CI/CD
- Finalisation de la documentation
## Conclusion
Ce cahier des charges définit les spécifications techniques et fonctionnelles pour le développement de l'application de création de groupes. L'application offrira une solution complète pour la gestion de groupes, avec une interface intuitive et des fonctionnalités avancées de collaboration en temps réel.

View 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.

View File

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

View 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.

View 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

View 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) {