feat: Intégration du backend NestJS depuis YNOV

- Framework: NestJS avec TypeORM
- Authentification: JWT (access + refresh tokens)
- Gestion utilisateurs: CRUD complet avec validation
- Routes: auth, users, parents, assistantes maternelles
- Dockerfile pour conteneurisation
This commit is contained in:
MARTIN Julien 2025-11-24 15:44:07 +01:00
parent 49f0684ad3
commit 33d6e7b0c3
108 changed files with 13872 additions and 1908 deletions

23
backend/.env.example Normal file
View File

@ -0,0 +1,23 @@
# Fichier: .env.example
# Copier ce fichier vers .env et adapter les valeurs selon votre environnement
# Configuration de la base de données PostgreSQL
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin123
POSTGRES_DB=ptitpas_db
# Configuration PgAdmin (accessible sur http://localhost:8080)
PGADMIN_DEFAULT_EMAIL=admin@localhost
PGADMIN_DEFAULT_PASSWORD=admin123
# Configuration de l'API
API_PORT=3000
# Secrets pour l'authentification JWT
JWT_SECRET=dev-jwt-secret-key-change-me
JWT_EXPIRATION_TIME=7d
# Environnement
NODE_ENV=development

64
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,64 @@
# 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
.env
# Tests bdd
.vscode/
BDD.sql
migrations/
src/seed/

4
backend/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

39
backend/Dockerfile Normal file
View File

@ -0,0 +1,39 @@
FROM node:22-alpine AS builder
WORKDIR /app
# Copier les fichiers de configuration
COPY package*.json ./
COPY tsconfig*.json ./
COPY nest-cli.json ./
# Installer TOUTES les dépendances (dev + prod pour le build)
RUN npm install && npm cache clean --force
# Copier le code source
COPY src ./src
# Builder l'application
RUN npm run build
# Stage production
FROM node:22-alpine AS production
WORKDIR /app
# Installer seulement les dépendances de production
COPY package*.json ./
RUN npm install --only=production && npm cache clean --force
# Copier le build depuis le stage builder
COPY --from=builder /app/dist ./dist
# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
USER nestjs
EXPOSE 3000
CMD ["node", "dist/main"]

63
backend/README-DEV.md Normal file
View File

@ -0,0 +1,63 @@
# 🚀 Guide de développement local
## Prérequis
- Docker et Docker Compose installés
- Git
## 🏃‍♂️ Démarrage rapide
### 1. Cloner le projet
```bash
git clone <url-du-depot-backend>
cd ptitspas-backend
```
### 2. Configuration de l'environnement
```bash
# Copier le fichier d'exemple
cp .env.example .env
# Optionnel : adapter les valeurs dans .env selon vos besoins
```
### 3. Lancer l'application
```bash
# Démarrer tous les services (PostgreSQL + PgAdmin + Backend)
docker compose -f docker-compose.dev.yml up -d
# Voir les logs
docker compose -f docker-compose.dev.yml logs -f
```
## 🌐 Accès aux services
- **Backend API** : http://localhost:3000
- **PgAdmin** : http://localhost:8080
- Email : admin@localhost
- Mot de passe : admin123
- **PostgreSQL** : localhost:5432
- Utilisateur : admin
- Mot de passe : admin123
- Base : ptitpas_db
## 🛠️ Commandes utiles
```bash
# Arrêter les services
docker compose -f docker-compose.dev.yml down
# Rebuild le backend après modification du Dockerfile
docker compose -f docker-compose.dev.yml up --build backend
# Voir l'état des services
docker compose -f docker-compose.dev.yml ps
# Accéder au container backend
docker exec -it ptitspas-backend-dev sh
```
## 📝 Notes de développement
- Les modifications du code source sont automatiquement prises en compte (hot reload)
- Les données PostgreSQL sont persistantes via le volume `postgres_dev_data`
- Le fichier `.env` n'est pas versionné pour des raisons de sécurité

158
backend/README.md Normal file
View File

@ -0,0 +1,158 @@
# P'titsPas API Backend ✨
Ce dépôt contient le code source de l'API backend pour la plateforme **P'titsPas**. L'API est construite avec NestJS et est responsable de toute la logique métier, de la gestion des données et de l'authentification des utilisateurs.
---
## 📚 Table des matières
- [Technologies utilisées](#-technologies-utilisées)
- [Prérequis](#-prérequis)
- [Installation](#-installation)
- [Lancement de l'application](#-lancement-de-lapplication)
- [Scripts principaux](#-scripts-principaux)
- [Tests](#-tests)
- [Gestion des migrations](#-gestion-des-migrations)
- [Documentation de l'API](#-documentation-de-lapi)
---
## 🛠️ Technologies utilisées
- **Framework**: [NestJS](https://nestjs.com/) (TypeScript)
- **Base de données**: [PostgreSQL](https://www.postgresql.org/)
- **ORM**: [TypeORM](https://typeorm.io/)
- **Authentification**: JWT avec [Passport.js](http://www.passportjs.org/)
- **Stockage Fichiers**: [MinIO](https://min.io/) (Compatible S3)
- **Tâches Asynchrones**: [Redis](https://redis.io/) avec [BullMQ](https://bullmq.io/)
- **Conteneurisation**: [Docker](https://www.docker.com/)
---
## 📋 Prérequis
Avant de commencer, assurez-vous d'avoir installé les outils suivants sur votre machine :
- [Node.js](https://nodejs.org/) (v18 ou supérieure)
- [npm](https://www.npmjs.com/) ou [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/products/docker-desktop/) et Docker Compose
- [Git](https://git-scm.com/)
---
## 🚀 Installation
1. **Clonez le dépôt :**
```bash
git clone https://github.com/votre-username/ptitspas-backend.git
cd ptitspas-backend
```
2. **Créez le fichier d'environnement :**
Copiez le fichier d'exemple `.env.example` et renommez-le en `.env`. Ce fichier est ignoré par Git et contiendra vos secrets locaux.
```bash
cp .env.example .env
```
➡️ **Important :** Ouvrez le fichier `.env` et remplissez les variables (identifiants de la base de données, secrets JWT, etc.).
3. **Installez les dépendances du projet :**
```bash
npm install
```
---
## ▶️ Lancement de l'application
### Méthode recommandée : avec Docker
Cette méthode lance l'ensemble des services nécessaires (API, base de données, MinIO, Redis) dans des conteneurs isolés.
```bash
docker-compose up --build
```
L'API sera accessible à l'adresse `http://localhost:3000` (ou le port que vous avez configuré dans votre `.env`).
### Méthode locale (pour le développement)
Cette méthode ne lance que le serveur NestJS. Assurez-vous que les autres services (PostgreSQL, Redis, etc.) sont déjà en cours d'exécution (par exemple, via Docker).
```
npm run start:dev
```
Le serveur redémarrera automatiquement à chaque modification de fichier.
---
## ⚙️ Scripts principaux
| Commande | Description |
| :------------------ | :-------------------------------------------------------------------- |
| `npm run start:dev` | Lance le serveur en mode développement avec rechargement automatique. |
| `npm run build` | Compile le projet TypeScript en JavaScript. |
| `npm start` | Lance l'application depuis les fichiers compilés (mode production). |
| `npm run lint` | Analyse le code pour détecter les erreurs de style et de syntaxe. |
---
## 🧪 Tests
Pour lancer les tests, utilisez les commandes suivantes :
| Commande | Description |
| :----------------- | :--------------------------------------------------------------- |
| `npm test` | Lance les tests unitaires. |
| `npm run test:e2e` | Lance les tests de bout en bout (end-to-end). |
| `npm run test:cov` | Lance tous les tests et génère un rapport de couverture de code. |
---
## 🗄️ Gestion des migrations
La structure de la base de données est gérée par des fichiers de migration TypeORM.
1. **Générer une nouvelle migration :**
Après avoir modifié une entité TypeORM, générez automatiquement le fichier de migration correspondant.
```bash
npm run migration:generate -- src/database/migrations/NomDeLaMigration
```
2. **Appliquer les migrations :**
Pour mettre à jour le schéma de votre base de données avec les nouvelles migrations.
```bash
npm run migration:run
```
---
## 📖 Documentation de l'API
Une fois l'application lancée, la documentation de l'API générée avec **Swagger (OpenAPI)** est disponible à l'adresse suivante :
➡️ **[http://localhost:3000/api-docs](http://localhost:3000/api-docs)**
Cette interface vous permet d'explorer et de tester toutes les routes de l'API directement depuis votre navigateur.
Excellente idée. C'est un élément crucial qui définit les droits et les devoirs liés à votre code.
En me basant sur la section `10.5 Propriété intellectuelle et licence de lapplication` de votre cahier des charges, j'ai rédigé une section "Licence" qui reflète précisément le statut propriétaire de votre projet.
Voici le `README.md` complet et mis à jour.
---
## 📜 Licence
Ce projet est distribué sous une **licence propriétaire**.
Le code source, la marque "P'titsPas" et la documentation associée sont la propriété exclusive de l'éditeur, Julien MARTIN.
Toute reproduction, distribution, modification ou utilisation du code source est strictement interdite sans un accord écrit préalable de l'auteur. Les clients et partenaires autorisés disposent d'une licence d'utilisation non-exclusive et non-transférable, conformément aux termes de leur contrat.
Pour toute question relative à l'utilisation ou à l'acquisition d'une licence, veuillez contacter l'auteur.

View File

@ -0,0 +1,72 @@
# Docker Compose pour développement local
# Usage: docker compose -f docker-compose.dev.yml up -d
services:
# Base de données PostgreSQL
postgres:
image: postgres:17
container_name: ptitspas-postgres-dev
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123}
POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db}
ports:
- "5433:5432"
volumes:
# Si le fichier d'init existe dans le dépôt database
- ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql
- postgres_dev_data:/var/lib/postgresql/data
networks:
- ptitspas_dev
# Interface d'administration DB
pgadmin:
image: dpage/pgadmin4
container_name: ptitspas-pgadmin-dev
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@ptits-pas.fr}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123}
ports:
- "8080:80"
depends_on:
- postgres
networks:
- ptitspas_dev
# Backend NestJS
backend:
build:
context: .
dockerfile: Dockerfile
container_name: ptitspas-backend-dev
restart: unless-stopped
environment:
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_USER: ${POSTGRES_USER:-admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123}
POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db}
API_PORT: ${API_PORT:-3000}
JWT_SECRET: ${JWT_SECRET:-dev-jwt-secret-key}
JWT_EXPIRATION_TIME: ${JWT_EXPIRATION_TIME:-7d}
NODE_ENV: ${NODE_ENV:-development}
ports:
- "3000:3000"
depends_on:
- postgres
volumes:
# Pour le hot reload en développement
- ./src:/app/src
- /app/node_modules
networks:
- ptitspas_dev
volumes:
postgres_dev_data:
networks:
ptitspas_dev:
driver: bridge

34
backend/eslint.config.mjs Normal file
View File

@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);

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
}
}

11571
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,91 @@
{
"name": "petitspas-backend",
"version": "1.0.0",
"description": "Backend pour l'application P'titsPas",
"main": "dist/index.js",
"name": "ptitspas-ynov-back",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"typeorm": "typeorm-ts-node-commonjs",
"migration:run": "npm run typeorm migration:run",
"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",
"init-admin": "ts-node src/scripts/initAdmin.ts"
"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"
},
"dependencies": {
"@nestjs/common": "^11.1.0",
"@prisma/client": "^6.7.0",
"@types/jsonwebtoken": "^9.0.9",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0"
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"@sentry/nestjs": "^10.10.0",
"bcrypt": "^6.0.0",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"joi": "^18.0.0",
"mapped-types": "^0.0.1",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.26"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/helmet": "^4.0.0",
"@types/morgan": "^1.9.9",
"@types/node": "^20.11.19",
"prisma": "^6.7.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"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",
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.3"
"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

@ -1,108 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Modèle pour les parents
model Parent {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
phoneNumber String?
address String?
status AccountStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
children Child[]
contracts Contract[]
}
// Modèle pour les enfants
model Child {
id String @id @default(uuid())
firstName String
dateOfBirth DateTime
photoUrl String?
photoConsent Boolean @default(false)
isMultiple Boolean @default(false)
isUnborn Boolean @default(false)
parentId String
parent Parent @relation(fields: [parentId], references: [id])
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les contrats
model Contract {
id String @id @default(uuid())
parentId String
childId String
startDate DateTime
endDate DateTime?
status ContractStatus @default(ACTIVE)
parent Parent @relation(fields: [parentId], references: [id])
child Child @relation(fields: [childId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les thèmes
model Theme {
id String @id @default(uuid())
name String @unique
primaryColor String
secondaryColor String
backgroundColor String
textColor String
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appSettings AppSettings[]
}
// Modèle pour les paramètres de l'application
model AppSettings {
id String @id @default(uuid())
currentThemeId String
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([currentThemeId])
}
// Modèle pour les administrateurs
model Admin {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
passwordChanged Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Enums
enum AccountStatus {
PENDING
VALIDATED
REJECTED
SUSPENDED
}
enum ContractStatus {
ACTIVE
ENDED
CANCELLED
}

View File

@ -1,18 +0,0 @@
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post('change-password')
@UseGuards(JwtAuthGuard)
async changePassword(
@Req() req,
@Body('oldPassword') oldPassword: string,
@Body('newPassword') newPassword: string,
) {
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
}
}

View File

@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AdminController],
providers: [AdminService],
})
export class AdminModule {}

View File

@ -1,40 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
// Récupérer l'administrateur
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
});
if (!admin) {
throw new UnauthorizedException('Administrateur non trouvé');
}
// Vérifier l'ancien mot de passe
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Ancien mot de passe incorrect');
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await this.prisma.admin.update({
where: { id: adminId },
data: { password: hashedPassword },
});
return { message: 'Mot de passe modifié avec succès' };
}
}

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,17 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getOverView() {
return this.appService.getOverView();
}
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,17 +1,67 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { AdminModule } from './admin/admin.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import appConfig from './config/app.config';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import { configValidationSchema } from './config/validation.schema';
import { UserModule } from './routes/user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_FILTER } from '@nestjs/core';
import { ParentsModule } from './routes/parents/parents.module';
import { AuthModule } from './routes/auth/auth.module';
import { SentryGlobalFilter } from '@sentry/nestjs/setup';
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
import { EnfantsModule } from './routes/enfants/enfants.module';
@Module({
imports: [
ConfigModule.forRoot({
// Gestion dynamique des fichiers .env
envFilePath: [`.env.${process.env.NODE_ENV || 'development'}`, '.env'],
// envFilePath: '.env',
// Chargement de configurations typées
load: [appConfig, databaseConfig, jwtConfig],
isGlobal: true,
validationSchema: configValidationSchema,
}),
PrismaModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule,
],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('database.host'),
port: config.get<number>('database.port'),
username: config.get('database.username'),
password: config.get('database.password'),
database: config.get('database.database'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
logging: true,
}),
}),
UserModule,
ParentsModule,
EnfantsModule,
AuthModule,
AdminModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: SentryGlobalFilter
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
}
],
})
export class AppModule {}
export class AppModule { }

View File

@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello Test!!!';
}
getOverView() {
return {
name: "P'titsPas API",
version: "1.0",
description: "Documentation rapide des endpoints disponibles",
authentication: "JWT Bearer Token requis",
endpoints: [
{
method: "GET",
path: "/parents",
description: "Liste tous les parents",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"]
},
{
method: "GET",
path: "/parents/:id",
description: "Récupère un parent par ID utilisateur",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
params: ["id (UUID)"]
},
{
method: "POST",
path: "/parents",
description: "Crée un parent",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
body: {
user_id: "UUID",
co_parent_id: "UUID (optionnel)"
}
},
{
method: "PATCH",
path: "/parents/:id",
description: "Met à jour un parent",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
params: ["id (UUID)"],
body: {
user_id: "UUID (optionnel)",
co_parent_id: "UUID (optionnel)"
}
},
{
method: "DELETE",
path: "/parents/:id",
description: "Supprime un parent",
roles: ["SUPER_ADMIN"],
params: ["id (UUID)"]
}
],
docs: "/api/docs"
};
}
}

View File

@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@ -0,0 +1,3 @@
import { SetMetadata } from "@nestjs/common";
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);

View File

@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const User = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
});

View File

@ -0,0 +1,11 @@
import { IsDateString, IsOptional } from "class-validator";
export class DateRangeQueryDto {
@IsOptional()
@IsDateString()
start?: string;
@IsOptional()
@IsDateString()
end?: string;
}

View File

@ -0,0 +1,6 @@
import { IsUUID } from "class-validator";
export class IdParamDto {
@IsUUID()
id: string;
}

View File

@ -0,0 +1,11 @@
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@IsPositive()
offset?: number;
@IsOptional()
@IsPositive()
limit?: number;
}

View File

@ -0,0 +1,8 @@
import { IsOptional, IsString, MinLength } from "class-validator";
export class SearchQueryDto {
@IsOptional()
@IsString()
@MinLength(2)
q?: string;
}

View File

@ -0,0 +1,27 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: { message: 'Internal server error' };
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}

View File

@ -0,0 +1,49 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from 'express';
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest<Request>();
if (request.path.startsWith('/api-docs')) {
return true;
}
const authHeader = request.headers['authorization'] as string | undefined;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant ou invalide');
}
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.accessSecret'),
});
request.user = {
...payload,
id: payload.sub,
};
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}

View File

@ -0,0 +1,26 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.role) {
return false;
}
return requiredRoles.includes(user.role);
}
}

View File

@ -0,0 +1,15 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map, Observable, timestamp } from "rxjs";
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
success: true,
timestamp: new Date().toISOString(),
data
})),
);
}
}

View File

@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('', () => ({
port: process.env.PORT,
env: process.env.NODE_ENV,
}));

View File

@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
}));

View File

@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
accessSecret: process.env.JWT_ACCESS_SECRET,
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES,
refreshSecret: process.env.JWT_REFRESH_SECRET,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES,
}));

View File

@ -0,0 +1,21 @@
import * as Joi from 'joi';
export const configValidationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().optional(),
// Base de données
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
// JWT
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRES: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_REFRESH_EXPIRES: Joi.string().required(),
});

View File

@ -1,72 +0,0 @@
import { Request, Response } from 'express';
import { ThemeService, ThemeData } from '../services/theme.service';
export class ThemeController {
// Créer un nouveau thème
static async createTheme(req: Request, res: Response) {
try {
const themeData: ThemeData = req.body;
const theme = await ThemeService.createTheme(themeData);
res.status(201).json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la création du thème' });
}
}
// Récupérer tous les thèmes
static async getAllThemes(req: Request, res: Response) {
try {
const themes = await ThemeService.getAllThemes();
res.json(themes);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' });
}
}
// Récupérer le thème actif
static async getActiveTheme(req: Request, res: Response) {
try {
const theme = await ThemeService.getActiveTheme();
if (!theme) {
return res.status(404).json({ error: 'Aucun thème actif trouvé' });
}
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' });
}
}
// Activer un thème
static async activateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const theme = await ThemeService.activateTheme(themeId);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de l\'activation du thème' });
}
}
// Mettre à jour un thème
static async updateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const themeData: Partial<ThemeData> = req.body;
const theme = await ThemeService.updateTheme(themeId, themeData);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' });
}
}
// Supprimer un thème
static async deleteTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
await ThemeService.deleteTheme(themeId);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la suppression du thème' });
}
}
}

View File

@ -0,0 +1,51 @@
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Users } from './users.entity';
@Entity('assistantes_maternelles')
export class AssistanteMaternelle {
// PK = FK vers utilisateurs.id
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
user_id: string;
@OneToOne(() => Users, (user) => user.assistanteMaternelle, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ name: 'numero_agrement', length: 50, nullable: true })
approval_number?: string;
@Column({ name: 'nir_chiffre', length: 15, nullable: true })
nir?: string;
@Column({ name: 'nb_max_enfants', type: 'int', nullable: true })
max_children?: number;
@Column({ name: 'biographie', type: 'text', nullable: true })
biography?: string;
@Column({
name: 'disponible',
type: 'boolean',
default: true,
nullable: true,
})
available?: boolean;
@Column({ name: 'ville_residence', length: 100, nullable: true })
residence_city?: string;
@Column( { name: 'date_agrement', type: 'date', nullable: true })
agreement_date?: Date;
@Column( { name: 'annee_experience', type: 'smallint', nullable: true })
years_experience?: number;
@Column( { name: 'specialite', length: 100, nullable: true })
specialty?: string;
@Column( { name: 'place_disponible', type: 'integer', nullable: true })
places_available?: number;
}

View File

@ -0,0 +1,42 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Contrat } from "./contrats.entity";
import { Users } from "./users.entity";
export enum StatutAvenantType {
PROPOSE = 'propose',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
}
@Entity('avenants_contrats')
export class AvenantContrat {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Contrat, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_contrat' })
contrat: Contrat;
@Column({ type: 'jsonb', nullable: true, name: 'modifications' })
modifications?: any;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'initie_par', referencedColumnName: 'id' })
initiator?: Users;
@Column({
type: 'enum',
enum: StatutAvenantType,
enumName: 'statut_avenant_type',
default: StatutAvenantType.PROPOSE,
name: 'statut'
})
statut: StatutAvenantType;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,74 @@
import {
Entity, PrimaryGeneratedColumn, Column,
OneToMany, ManyToMany, CreateDateColumn, JoinTable
} from 'typeorm';
import { Parents } from './parents.entity';
import { ParentsChildren } from './parents_children.entity';
import { Dossier } from './dossiers.entity';
export enum StatutEnfantType {
A_NAITRE = 'a_naitre',
ACTIF = 'actif',
SCOLARISE = 'scolarise',
}
export enum GenreType {
H = 'H',
F = 'F',
AUTRE = 'Autre',
}
@Entity('enfants')
export class Children {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: StatutEnfantType,
enumName: 'statut_enfant_type',
name: 'statut'
})
status: StatutEnfantType;
@Column({ name: 'prenom', length: 100, nullable: true })
first_name?: string;
@Column({ name: 'nom', length: 100, nullable: true })
last_name?: string;
@Column({
type: 'enum',
enum: GenreType,
enumName: 'genre_type',
nullable: true,
name: 'genre'
})
gender?: GenreType;
@Column({ type: 'date', nullable: true, name: 'date_naissance' })
birth_date?: Date;
@Column({ type: 'date', nullable: true, name: 'date_prevue_naissance' })
due_date?: Date;
@Column({ nullable: true, name: 'photo_url', type: 'text' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo', type: 'boolean' })
consent_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
consent_photo_at?: Date;
@Column({ default: false, name: 'est_multiple', type: 'boolean' })
is_multiple: boolean;
// Lien via table de jointure enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.child)
parentLinks: ParentsChildren[];
// Relation avec Dossier
@OneToMany(() => Dossier, d => d.child)
dossiers: Dossier[];
}

View File

@ -0,0 +1,57 @@
import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Dossier } from "./dossiers.entity";
export enum StatutContratType {
BROUILLON = 'brouillon',
EN_ATTENTE_SIGNATURE = 'en_attente_signature',
VALIDE = 'valide',
RESILIE = 'resilie',
}
@Entity('contrats')
export class Contrat {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToOne(() => Dossier, {onDelete: 'CASCADE'} )
@JoinColumn({ name: 'id_dossier'})
dossier: Dossier;
@Column({type: 'jsonb', nullable: true, name: 'planning'})
planning?: any;
@Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'tarif_horaire'})
hourly_rate?: string;
@Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'indemnites_repas'})
meal_indemnity?: string;
@Column( { name: 'date_debut', type: 'date', nullable: true })
start_date?: Date;
@Column({
type: 'enum',
enum: StatutContratType,
enumName: 'statut_contrat_type',
default: StatutContratType.BROUILLON,
name: 'statut'
})
statut: StatutContratType;
@Column({type: 'boolean', default: false, name: 'signe_parent'})
signed_by_parent: boolean;
@Column({type: 'boolean', default: false, name: 'signe_am'})
signed_by_am: boolean;
@Column({type: 'timestamptz', nullable: true, name: 'finalise_le'})
finalized_at?: Date;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}

View File

@ -0,0 +1,61 @@
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn
} from 'typeorm';
import { Parents } from './parents.entity';
import { Children } from './children.entity';
import { Message } from './messages.entity';
export enum StatutDossierType {
ENVOYE = 'envoye',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
CLOTURE = 'cloture',
}
@Entity('dossiers')
export class Dossier {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Parents, p => p.dossiers, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent: Parents;
@ManyToOne(() => Children, c => c.dossiers, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child: Children;
@Column({ type: 'text', nullable: true, name: 'presentation' })
presentation?: string;
@Column({ type: 'varchar', length: 50, nullable: true, name: 'type_contrat' })
type_contrat?: string;
@Column({ type: 'boolean', default: false, name: 'repas' })
meals: boolean;
@Column({ type: 'numeric', precision: 10, scale: 2, nullable: true, name: 'budget' })
budget?: number;
@Column({ type: 'jsonb', nullable: true, name: 'planning_souhaite' })
desired_schedule?: any;
@Column({
type: 'enum',
enum: StatutDossierType,
enumName: 'statut_dossier_type',
default: StatutDossierType.ENVOYE,
name: 'statut'
})
status: StatutDossierType;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
@OneToMany(() => Message, m => m.dossier)
messages: Message[];
}

View File

@ -0,0 +1,79 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Children } from "./children.entity";
import { Users } from "./users.entity";
import { Parents } from "./parents.entity";
export enum TypeEvenementType {
ABSENCE_ENFANT = 'absence_enfant',
CONGE_AM = 'conge_am',
CONGE_PARENT = 'conge_parent',
ARRET_MALADIE_AM = 'arret_maladie_am',
EVENEMENT_RPE = 'evenement_rpe',
}
export enum StatutEvenementType {
PROPOSE = 'propose',
VALIDE = 'valide',
REFUSE = 'refuse',
}
@Entity('evenements')
export class Evenement {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: TypeEvenementType,
enumName: 'type_evenement_type',
name: 'type'
})
type: TypeEvenementType;
@ManyToOne(() => Children, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child?: Children;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_am', referencedColumnName: 'id' })
assistanteMaternelle?: Users;
@ManyToOne(() => Parents, { nullable: true })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent?: Parents;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
created_by?: Users;
@Column({ type: 'timestamptz', nullable: true, name: 'date_debut' })
start_date?: Date;
@Column({ type: 'timestamptz', nullable: true, name: 'date_fin' })
end_date?: Date;
@Column({ type: 'text', nullable: true, name: 'commentaires' })
comments?: string;
@Column({
type: 'enum',
enum: StatutEvenementType,
enumName: 'statut_evenement_type',
name: 'statut',
default: StatutEvenementType.PROPOSE
})
status: StatutEvenementType;
@Column({type: 'timestamptz', nullable: true, name: 'delai_grace'})
grace_deadline?: Date;
@Column({type: 'boolean', default: false, name: 'urgent'})
urgent: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}

View File

@ -0,0 +1,29 @@
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, JoinColumn, CreateDateColumn
} from 'typeorm';
import { Dossier } from './dossiers.entity';
import { Users } from './users.entity';
@Entity('messages')
export class Message {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Dossier, d => d.messages, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_dossier' })
dossier: Dossier;
@ManyToOne(() => Users, u => u.messages, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_expediteur' })
sender: Users;
@Column({ type: 'text', name: 'contenu' })
content: string;
@Column({ type: 'boolean', name: 're_redige_par_ia', default: false })
reRedigeParIA: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}

View File

@ -0,0 +1,23 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('notifications')
export class Notification {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ type: 'text', name: 'contenu' })
content: string;
@Column({type: 'boolean', name: 'lu', default: false})
read: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}

View File

@ -0,0 +1,31 @@
import {
Entity, PrimaryColumn, OneToOne, JoinColumn,
ManyToOne, OneToMany
} from 'typeorm';
import { Users } from './users.entity';
import { ParentsChildren } from './parents_children.entity';
import { Dossier } from './dossiers.entity';
@Entity('parents', { schema: 'public' })
export class Parents {
// PK = FK vers utilisateurs.id
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
user_id: string;
@OneToOne(() => Users, user => user.parent, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
// Co-parent (nullable) → FK vers utilisateurs.id
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' })
co_parent?: Users;
// Lien vers enfants via la table enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.parent)
parentChildren: ParentsChildren[];
// Lien vers les dossiers de ce parent
@OneToMany(() => Dossier, d => d.parent)
dossiers: Dossier[];
}

View File

@ -0,0 +1,22 @@
import {
Entity, ManyToOne, JoinColumn, PrimaryColumn
} from 'typeorm';
import { Parents } from './parents.entity';
import { Children } from './children.entity';
@Entity('enfants_parents', { schema: 'public' })
export class ParentsChildren {
@PrimaryColumn('uuid', { name: 'id_parent' })
parentId: string;
@PrimaryColumn('uuid', { name: 'id_enfant' })
enfantId: string;
@ManyToOne(() => Parents, p => p.parentChildren, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent: Parents;
@ManyToOne(() => Children, c => c.parentLinks, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child: Children;
}

View File

@ -0,0 +1,19 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('signalements_bugs')
export class SignalementBug {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, {nullable: true})
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'text', name: 'description'})
description: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}

View File

@ -0,0 +1,21 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('uploads')
export class Upload {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'text', name: 'fichier_url' })
file_url: string;
@Column({type: 'varchar', length: 50, nullable: true, name: 'type'})
type?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}

View File

@ -0,0 +1,150 @@
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn,
OneToOne, OneToMany
} from 'typeorm';
import { AssistanteMaternelle } from './assistantes_maternelles.entity';
import { Parents } from './parents.entity';
import { Message } from './messages.entity';
// Enums alignés avec la BDD PostgreSQL
export enum RoleType {
PARENT = 'parent',
GESTIONNAIRE = 'gestionnaire',
SUPER_ADMIN = 'super_admin',
ASSISTANTE_MATERNELLE = 'assistante_maternelle',
ADMINISTRATEUR = 'administrateur',
}
//Enum pour definir le genre
export enum GenreType {
H = 'H',
F = 'F',
AUTRE = 'Autre',
}
//Enum pour definir le statut utilisateur
export enum StatutUtilisateurType {
EN_ATTENTE = 'en_attente',
ACTIF = 'actif',
SUSPENDU = 'suspendu',
}
export enum SituationFamilialeType {
CELIBATAIRE = 'celibataire',
MARIE = 'marie',
DIVORCE = 'divorce',
VEUF = 'veuf',
PACSE = 'pacse',
SEPARE = 'separe',
PARENT_ISOLE = 'parent_isole',
CONCUBINAGE = 'concubinage',
}
//Declaration de l'entite utilisateur
@Entity('utilisateurs', { schema: 'public' })
export class Users {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, name: 'email' })
email: string;
@Column({ name: 'password' })
password: string;
@Column({ name: 'prenom', nullable: true })
prenom?: string;
@Column({ name: 'nom', nullable: true })
nom?: string;
@Column({
type: 'enum',
enum: GenreType,
enumName: 'genre_type', // correspond à l'enum de la db psql
nullable: true,
name: 'genre'
})
genre?: GenreType;
@Column({
type: 'enum',
enum: RoleType,
enumName: 'role_type', // correspond à l'enum de la db psql
name: 'role'
})
role: RoleType;
@Column({
type: 'enum',
enum: StatutUtilisateurType,
enumName: 'statut_utilisateur_type', // correspond à l'enum de la db psql
default: StatutUtilisateurType.EN_ATTENTE,
name: 'statut'
})
statut: StatutUtilisateurType;
@Column({ type: 'enum',
enum: SituationFamilialeType,
enumName: 'situation_familiale_type',
nullable: true,
name: 'situation_familiale'
})
situation_familiale?: SituationFamilialeType;
@Column({ nullable: true, name: 'telephone' })
telephone?: string;
@Column({ name: 'mobile', nullable: true })
mobile?: string;
@Column({ name: 'telephone_fixe', nullable: true })
telephone_fixe?: string;
@Column({ nullable: true, name: 'adresse' })
adresse?: string;
@Column({ nullable: true, name: 'photo_url' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo' })
consentement_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
date_consentement_photo?: Date;
@Column({ default: false, name: 'changement_mdp_obligatoire' })
changement_mdp_obligatoire: boolean;
@Column({ nullable: true, name: 'ville' })
ville?: string;
@Column({ nullable: true, name: 'code_postal' })
code_postal?: string;
@Column({ nullable: true, name: 'profession' })
profession?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
modifie_le: Date;
// Relations
@OneToOne(() => AssistanteMaternelle, a => a.user)
assistanteMaternelle?: AssistanteMaternelle;
@OneToOne(() => Parents, p => p.user)
parent?: Parents;
@OneToMany(() => Message, m => m.sender)
messages?: Message[];
@OneToMany(() => Parents, parent => parent.co_parent)
co_parent_in?: Parents[];
}

View File

@ -0,0 +1,44 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Users } from "./users.entity";
export enum StatutValidationType {
EN_ATTENTE = 'en_attente',
VALIDE = 'valide',
REFUSE = 'refuse',
}
@Entity('validations')
export class Validation {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'varchar', length: 50, name: 'type', nullable: true })
type: string;
@Column({
type: 'enum',
enum: StatutValidationType,
enumName: 'statut_validation_type',
name: 'statut',
default: StatutValidationType.EN_ATTENTE
})
status: StatutValidationType;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'valide_par', referencedColumnName: 'id' })
validated_by?: Users;
@Column( { name: 'commentaire', type: 'text', nullable: true })
comment?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}

View File

@ -1,28 +0,0 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import themeRoutes from './routes/theme.routes';
const app = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());
// Routes
app.use('/api/themes', themeRoutes);
// Gestion des erreurs
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Une erreur est survenue' });
});
// Démarrage du serveur
app.listen(port, () => {
console.log(`Serveur démarré sur le port ${port}`);
});

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

@ -0,0 +1,59 @@
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
import { DocumentBuilder } from '@nestjs/swagger';
import { AuthGuard } from './common/guards/auth.guard';
import { JwtService } from '@nestjs/jwt';
import { RolesGuard } from './common/guards/roles.guard';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule,
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
const configService = app.get(ConfigService);
const port = configService.get<number>('app.port', 3000);
app.setGlobalPrefix('api/v1');
const config = new DocumentBuilder()
.setTitle("P'titsPas API")
.setDescription("API pour l'application P'titsPas")
.setVersion('1.0.0')
.addBearerAuth(
{
type: 'http',
scheme: 'Bearer',
bearerFormat: 'JWT',
name: 'Authorization',
description: 'Enter JWT token',
in: 'header',
},
'access-token',
)
//.addServer('/api/v1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1/swagger', app, document);
await app.listen(port);
console.log(`✅ P'titsPas API is running on: ${await app.getUrl()}`);
}
bootstrap().catch((err) => {
console.error('❌ Error starting the application:', err);
process.exit(1);
});

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
describe('AssistantesMaternellesController', () => {
let controller: AssistantesMaternellesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AssistantesMaternellesController],
providers: [AssistantesMaternellesService],
}).compile();
controller = module.get<AssistantesMaternellesController>(AssistantesMaternellesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,81 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity';
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { AuthGuard } from 'src/common/guards/auth.guard';
@ApiTags("Assistantes Maternelles")
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, RolesGuard)
@Controller('assistantes-maternelles')
export class AssistantesMaternellesController {
constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) { }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Créer nounou' })
@ApiResponse({ status: 201, description: 'Nounou créée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiBody({ type: CreateAssistanteDto })
@Post()
create(@Body() dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des nounous' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getAll(): Promise<AssistanteMaternelle[]> {
return this.assistantesMaternellesService.findAll();
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id')
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@ApiOperation({ summary: 'Récupérer une nounou par id' })
@ApiResponse({ status: 200, description: 'Détails de la nounou' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.findOne(user_id);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiBody({ type: UpdateAssistanteDto })
@ApiOperation({ summary: 'Mettre à jour une nounou' })
@ApiResponse({ status: 200, description: 'Nounou mise à jour avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Supprimer une nounou' })
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@Delete(':id')
remove(@Param('id') id: string): Promise<{ message: string }>
{
return this.assistantesMaternellesService.remove(id);
}
}

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/users.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, Users]),
AuthModule
],
controllers: [AssistantesMaternellesController],
providers: [AssistantesMaternellesService],
exports: [
AssistantesMaternellesService,
TypeOrmModule,
],
})
export class AssistantesMaternellesModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
describe('AssistantesMaternellesService', () => {
let service: AssistantesMaternellesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AssistantesMaternellesService],
}).compile();
service = module.get<AssistantesMaternellesService>(AssistantesMaternellesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,80 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RoleType, Users } from 'src/entities/users.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
@Injectable()
export class AssistantesMaternellesService {
constructor(
@InjectRepository(AssistanteMaternelle)
private readonly assistantesMaternelleRepository: Repository<AssistanteMaternelle>,
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>
) {}
// Création dune assistante maternelle
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
if (!user) throw new NotFoundException('Utilisateur introuvable');
if (user.role !== RoleType.ASSISTANTE_MATERNELLE) {
throw new BadRequestException('Accès réservé aux assistantes maternelles');
}
const exist = await this.assistantesMaternelleRepository.findOneBy({ user_id: dto.user_id });
if (exist) throw new ConflictException('Assistante maternelle déjà existante');
const entity = this.assistantesMaternelleRepository.create({
user_id: dto.user_id,
user: { ...user, role: RoleType.ASSISTANTE_MATERNELLE },
approval_number: dto.approval_number,
nir: dto.nir,
max_children: dto.max_children,
biography: dto.biography,
available: dto.available ?? true,
residence_city: dto.residence_city,
agreement_date: dto.agreement_date ? new Date(dto.agreement_date) : undefined,
years_experience: dto.years_experience,
specialty: dto.specialty,
places_available: dto.places_available,
});
return this.assistantesMaternelleRepository.save(entity);
}
// Liste des assistantes maternelles
async findAll(): Promise<AssistanteMaternelle[]> {
return this.assistantesMaternelleRepository.find({
relations: ['user'],
});
}
// Récupérer une assistante maternelle par user_id
async findOne(user_id: string): Promise<AssistanteMaternelle> {
const assistante = await this.assistantesMaternelleRepository.findOne({
where: { user_id },
relations: ['user'],
});
if (!assistante) throw new NotFoundException('Assistante maternelle introuvable');
return assistante;
}
// Mise à jour
async update(id: string, dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
await this.assistantesMaternelleRepository.update(id, dto);
return this.findOne(id);
}
// Suppression dune assistante maternelle
async remove(id: string): Promise<{ message: string }> {
await this.assistantesMaternelleRepository.delete(id);
return { message: 'Assistante maternelle supprimée' };
}
}

View File

@ -1,95 +0,0 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const router = Router();
const prisma = new PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Route de connexion
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Vérifier les identifiants
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier le mot de passe
const validPassword = await bcrypt.compare(password, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier si le mot de passe doit être changé
if (!admin.passwordChanged) {
return res.status(403).json({
error: 'Changement de mot de passe requis',
requiresPasswordChange: true
});
}
// Générer le token JWT
const token = jwt.sign(
{
id: admin.id,
email: admin.email,
role: 'admin'
},
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token });
} catch (error) {
console.error('Erreur lors de la connexion:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Route de changement de mot de passe
router.post('/change-password', async (req, res) => {
try {
const { email, currentPassword, newPassword } = req.body;
// Vérifier l'administrateur
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(404).json({ error: 'Administrateur non trouvé' });
}
// Vérifier l'ancien mot de passe
const validPassword = await bcrypt.compare(currentPassword, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await prisma.admin.update({
where: { id: admin.id },
data: {
password: hashedPassword,
passwordChanged: true
}
});
res.json({ message: 'Mot de passe changé avec succès' });
} catch (error) {
console.error('Erreur lors du changement de mot de passe:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
export default router;

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,75 @@
import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express';
import { UserService } from '../user/user.service';
import { ProfileResponseDto } from './dto/profile_response.dto';
import { RefreshTokenDto } from './dto/refresh_token.dto';
import { User } from 'src/common/decorators/user.decorator';
import { Users } from 'src/entities/users.entity';
@ApiTags('Authentification')
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) { }
@Public()
@ApiOperation({ summary: 'Connexion' })
@Post('login')
async login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@Public()
@Post('register')
@ApiOperation({ summary: 'Inscription' })
async register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Public()
@Post('refresh')
@ApiBearerAuth('refresh_token')
@ApiResponse({ status: 200, description: 'Nouveaux tokens générés avec succès.' })
@ApiResponse({ status: 401, description: 'Token de rafraîchissement invalide ou expiré.' })
@ApiOperation({ summary: 'Rafraichir les tokens' })
async refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refreshTokens(dto.refresh_token);
}
@Get('me')
@UseGuards(AuthGuard)
@ApiBearerAuth('access-token')
@ApiOperation({ summary: "Récupérer le profil complet de l'utilisateur connecté" })
@ApiResponse({ status: 200, type: ProfileResponseDto })
async getProfile(@Req() req: Request): Promise<ProfileResponseDto> {
if (!req.user || !req.user.sub) {
throw new UnauthorizedException('Utilisateur non authentifié');
}
const user = await this.userService.findOne(req.user.sub);
return {
id: user.id,
email: user.email,
role: user.role,
prenom: user.prenom ?? '',
nom: user.nom ?? '',
statut: user.statut,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth('access-token')
@Post('logout')
logout(@User() currentUser: Users) {
return this.authService.logout(currentUser.id);
}
}

View File

@ -0,0 +1,24 @@
import { forwardRef, Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
forwardRef(() => UserModule),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
secret: config.get('jwt.secret'),
signOptions: { expiresIn: config.get('jwt.expirationTime') },
}),
inject: [ConfigService],
})
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService, JwtModule],
})
export class AuthModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,137 @@
import {
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UserService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) { }
/**
* Génère un access_token et un refresh_token
*/
async generateTokens(userId: string, email: string, role: RoleType) {
const accessSecret = this.configService.get<string>('jwt.accessSecret');
const accessExpiresIn = this.configService.get<string>('jwt.accessExpiresIn');
const refreshSecret = this.configService.get<string>('jwt.refreshSecret');
const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn');
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }),
]);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
/**
* Connexion utilisateur
*/
async login(dto: LoginDto) {
try {
const user = await this.usersService.findByEmailOrNull(dto.email);
if (!user) {
throw new UnauthorizedException('Email invalide');
}
console.log("Tentative login:", dto.email, JSON.stringify(dto.password));
console.log("Utilisateur trouvé:", user.email, user.password);
const isMatch = await bcrypt.compare(dto.password, user.password);
console.log("Résultat bcrypt.compare:", isMatch);
if (!isMatch) {
throw new UnauthorizedException('Mot de passe invalide');
}
// if (user.password !== dto.password) {
// throw new UnauthorizedException('Mot de passe invalide');
// }
return this.generateTokens(user.id, user.email, user.role);
} catch (error) {
console.error('Erreur de connexion :', error);
throw new UnauthorizedException('Identifiants invalides');
}
}
/**
* Rafraîchir les tokens
*/
async refreshTokens(refreshToken: string) {
try {
const payload = await this.jwtService.verifyAsync(refreshToken, {
secret: this.configService.get<string>('jwt.refreshSecret'),
});
const user = await this.usersService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException('Utilisateur introuvable');
}
return this.generateTokens(user.id, user.email, user.role);
} catch {
throw new UnauthorizedException('Refresh token invalide');
}
}
/**
* Inscription utilisateur lambda (parent ou assistante maternelle)
*/
async register(registerDto: RegisterDto) {
const exists = await this.usersService.findByEmailOrNull(registerDto.email);
if (exists) {
throw new ConflictException('Email déjà utilisé');
}
const allowedRoles = new Set<RoleType>([RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE]);
if (!allowedRoles.has(registerDto.role)) {
registerDto.role = RoleType.PARENT;
}
registerDto.statut = StatutUtilisateurType.EN_ATTENTE;
if (!registerDto.consentement_photo) {
registerDto.date_consentement_photo = null;
} else if (registerDto.date_consentement_photo) {
const date = new Date(registerDto.date_consentement_photo);
if (isNaN(date.getTime())) {
registerDto.date_consentement_photo = null;
}
}
const user = await this.usersService.createUser(registerDto);
const tokens = await this.generateTokens(user.id, user.email, user.role);
return {
...tokens,
user: {
id: user.id,
email: user.email,
role: user.role,
prenom: user.prenom,
nom: user.nom,
statut: user.statut,
},
};
}
async logout(userId: string) {
// Pour le moment envoyer un message clair
return { success: true, message: 'Deconnexion'}
}
}

View File

@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsString, MaxLength, MinLength } from "class-validator";
export class LoginDto {
@ApiProperty({ example: 'mon.utilisateur@exemple.com', description: "Adresse email de l'utililisateur" })
@IsEmail()
email: string;
@ApiProperty({
example: "Mon_motdepasse_fort_1234?",
description: "Mot de passe de l'utilisateur"
})
@IsString({ message: 'Le mot de passe doit etre une chaine de caracteres' })
//@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caracteres' })
@MaxLength(50)
password: string;
}

View File

@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class ProfileResponseDto {
@ApiProperty()
id: string;
@ApiProperty()
email: string;
@ApiProperty({ enum: RoleType })
role: RoleType;
@ApiProperty()
prenom?: string;
@ApiProperty()
nom?: string;
@ApiProperty({ enum: StatutUtilisateurType })
statut: StatutUtilisateurType;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class RefreshTokenDto {
@ApiProperty({
description: 'Token de rafraîchissement',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString()
refresh_token: string;
}

View File

@ -0,0 +1,14 @@
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator';
import { CreateUserDto } from '../../user/dto/create_user.dto';
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class RegisterDto extends OmitType(CreateUserDto, ['changement_mdp_obligatoire'] as const) {
@ApiProperty({ enum: [RoleType.ASSISTANTE_MATERNELLE, RoleType.PARENT], default: RoleType.PARENT })
@IsEnum(RoleType)
role: RoleType = RoleType.PARENT;
@IsEnum(StatutUtilisateurType)
@IsOptional()
statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
}

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class DossiersModule {}

View File

@ -0,0 +1,66 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
ValidateIf,
} from 'class-validator';
import { GenreType, StatutEnfantType } from 'src/entities/children.entity';
export class CreateEnfantsDto {
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.ACTIF })
@IsEnum(StatutEnfantType)
@IsNotEmpty()
status: StatutEnfantType;
@ApiProperty({ example: 'Georges', required: false })
@IsOptional()
@IsString()
@MaxLength(100)
first_name?: string;
@ApiProperty({ example: 'Dupont', required: false })
@IsOptional()
@IsString()
@MaxLength(100)
last_name?: string;
@ApiProperty({ enum: GenreType, required: false })
@IsOptional()
@IsEnum(GenreType)
gender?: GenreType;
@ApiProperty({ example: '2018-06-24', required: false })
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
@IsOptional()
@IsDateString()
birth_date?: string;
@ApiProperty({ example: '2025-12-15', required: false })
@ValidateIf(o => o.status === StatutEnfantType.A_NAITRE)
@IsOptional()
@IsDateString()
due_date?: string;
@ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false })
@IsOptional()
@IsString()
photo_url?: string;
@ApiProperty({ default: false })
@IsBoolean()
consent_photo: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
consent_photo_at?: string;
@ApiProperty({ default: false })
@IsBoolean()
is_multiple: boolean;
}

View File

@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { GenreType, StatutEnfantType } from 'src/entities/children.entity';
export class EnfantResponseDto {
@ApiProperty({ example: 'UUID-enfant' })
id: string;
@ApiProperty({ enum: StatutEnfantType })
status: StatutEnfantType;
@ApiProperty({ example: 'Georges', required: false })
first_name?: string;
@ApiProperty({ example: 'Dupont', required: false })
last_name?: string;
@ApiProperty({ enum: GenreType, required: false })
gender?: GenreType;
@ApiProperty({ example: '2018-06-24', required: false })
birth_date?: string;
@ApiProperty({ example: '2025-12-15', required: false })
due_date?: string;
@ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false })
photo_url?: string;
@ApiProperty({ example: false })
consent_photo: boolean;
@ApiProperty({ example: false })
is_multiple: boolean;
@ApiProperty({ example: 'UUID-parent' })
parent_id: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateEnfantsDto } from './create_enfants.dto';
export class UpdateEnfantsDto extends PartialType(CreateEnfantsDto) {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnfantsController } from './enfants.controller';
describe('EnfantsController', () => {
let controller: EnfantsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EnfantsController],
}).compile();
controller = module.get<EnfantsController>(EnfantsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,70 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { EnfantsService } from './enfants.service';
import { CreateEnfantsDto } from './dto/create_enfants.dto';
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
import { RoleType, Users } from 'src/entities/users.entity';
import { User } from 'src/common/decorators/user.decorator';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RolesGuard } from 'src/common/guards/roles.guard';
@ApiBearerAuth('access-token')
@ApiTags('Enfants')
@UseGuards(AuthGuard, RolesGuard)
@Controller('enfants')
export class EnfantsController {
constructor(private readonly enfantsService: EnfantsService) { }
@Roles(RoleType.PARENT)
@Post()
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) {
return this.enfantsService.create(dto, currentUser);
}
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@Get()
findAll() {
return this.enfantsService.findAll();
}
@Roles(
RoleType.PARENT,
RoleType.ADMINISTRATEUR,
RoleType.SUPER_ADMIN,
RoleType.GESTIONNAIRE
)
@Get(':id')
findOne(
@Param('id', new ParseUUIDPipe()) id: string,
@User() currentUser: Users
) {
return this.enfantsService.findOne(id, currentUser);
}
@Roles(RoleType.ADMINISTRATEUR, RoleType.SUPER_ADMIN, RoleType.PARENT)
@Patch(':id')
update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: UpdateEnfantsDto,
@User() currentUser: Users,
) {
return this.enfantsService.update(id, dto, currentUser);
}
@Roles(RoleType.SUPER_ADMIN)
@Delete(':id')
remove(@Param('id', new ParseUUIDPipe()) id: string) {
return this.enfantsService.remove(id);
}
}

View File

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { EnfantsController } from './enfants.controller';
import { EnfantsService } from './enfants.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Children } from 'src/entities/children.entity';
import { Parents } from 'src/entities/parents.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([Children, Parents, ParentsChildren]),
AuthModule
],
controllers: [EnfantsController],
providers: [EnfantsService]
})
export class EnfantsModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnfantsService } from './enfants.service';
describe('EnfantsService', () => {
let service: EnfantsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EnfantsService],
}).compile();
service = module.get<EnfantsService>(EnfantsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,113 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Children, StatutEnfantType } from 'src/entities/children.entity';
import { Parents } from 'src/entities/parents.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateEnfantsDto } from './dto/create_enfants.dto';
@Injectable()
export class EnfantsService {
constructor(
@InjectRepository(Children)
private readonly childrenRepository: Repository<Children>,
@InjectRepository(Parents)
private readonly parentsRepository: Repository<Parents>,
@InjectRepository(ParentsChildren)
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
) { }
// Création dun enfant
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> {
const parent = await this.parentsRepository.findOne({
where: { user_id: currentUser.id },
});
if (!parent) throw new NotFoundException('Parent introuvable');
// Vérif métier simple
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
throw new BadRequestException('Un enfant actif doit avoir une date de naissance');
}
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
const exist = await this.childrenRepository.findOne({
where: {
first_name: dto.first_name,
last_name: dto.last_name,
birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined,
},
});
if (exist) throw new ConflictException('Cet enfant existe déjà');
// Création
const child = this.childrenRepository.create(dto);
await this.childrenRepository.save(child);
// Lien parent-enfant
const parentLink = this.parentsChildrenRepository.create({
parentId: parent.user_id,
enfantId: child.id,
});
await this.parentsChildrenRepository.save(parentLink);
return this.findOne(child.id, currentUser);
}
// Liste des enfants
async findAll(): Promise<Children[]> {
return this.childrenRepository.find({
relations: ['parentLinks'],
order: { last_name: 'ASC', first_name: 'ASC' },
});
}
// Récupérer un enfant par id
async findOne(id: string, currentUser: Users): Promise<Children> {
const child = await this.childrenRepository.findOne({
where: { id },
relations: ['parentLinks'],
});
if (!child) throw new NotFoundException('Enfant introuvable');
switch (currentUser.role) {
case RoleType.PARENT:
if (!child.parentLinks.some(link => link.parentId === currentUser.id)) {
throw new ForbiddenException('Cet enfant ne vous appartient pas');
}
break;
case RoleType.ADMINISTRATEUR:
case RoleType.SUPER_ADMIN:
case RoleType.GESTIONNAIRE:
// accès complet
break;
default:
throw new ForbiddenException('Accès interdit');
}
return child;
}
// Mise à jour
async update(id: string, dto: Partial<CreateEnfantsDto>, currentUser: Users): Promise<Children> {
const child = await this.childrenRepository.findOne({ where: { id } });
if (!child) throw new NotFoundException('Enfant introuvable');
await this.childrenRepository.update(id, dto);
return this.findOne(id, currentUser);
}
// Suppression
async remove(id: string): Promise<void> {
await this.childrenRepository.delete(id);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ParentsController } from './parents.controller';
describe('ParentsController', () => {
let controller: ParentsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ParentsController],
}).compile();
controller = module.get<ParentsController>(ParentsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,58 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import { ParentsService } from './parents.service';
import { Parents } from 'src/entities/parents.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity';
import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateParentDto } from '../user/dto/create_parent.dto';
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
@ApiTags('Parents')
@Controller('parents')
export class ParentsController {
constructor(private readonly parentsService: ParentsService) {}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
getAll(): Promise<Parents[]> {
return this.parentsService.findAll();
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id')
@ApiResponse({ status: 200, type: Parents, description: 'Détails du parent par ID utilisateur' })
@ApiResponse({ status: 404, description: 'Parent non trouvé' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
getOne(@Param('id') user_id: string): Promise<Parents> {
return this.parentsService.findOne(user_id);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Post()
@ApiBody({ type: CreateParentDto })
@ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
create(@Body() dto: CreateParentDto): Promise<Parents> {
return this.parentsService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Patch(':id')
@ApiBody({ type: UpdateParentsDto })
@ApiResponse({ status: 200, type: Parents, description: 'Parent mis à jour avec succès' })
@ApiResponse({ status: 404, description: 'Parent introuvable' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
return this.parentsService.update(id, dto);
}
}

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Parents } from 'src/entities/parents.entity';
import { ParentsController } from './parents.controller';
import { ParentsService } from './parents.service';
import { Users } from 'src/entities/users.entity';
@Module({
imports: [TypeOrmModule.forFeature([Parents, Users])],
controllers: [ParentsController],
providers: [ParentsService],
exports: [ParentsService,
TypeOrmModule,
],
})
export class ParentsModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ParentsService } from './parents.service';
describe('ParentsService', () => {
let service: ParentsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ParentsService],
}).compile();
service = module.get<ParentsService>(ParentsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,74 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Parents } from 'src/entities/parents.entity';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateParentDto } from '../user/dto/create_parent.dto';
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
@Injectable()
export class ParentsService {
constructor(
@InjectRepository(Parents)
private readonly parentsRepository: Repository<Parents>,
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>,
) {}
// Création dun parent
async create(dto: CreateParentDto): Promise<Parents> {
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
if (!user) throw new NotFoundException('Utilisateur introuvable');
if (user.role !== RoleType.PARENT) {
throw new BadRequestException('Accès réservé aux parents');
}
const exist = await this.parentsRepository.findOneBy({ user_id: dto.user_id });
if (exist) throw new ConflictException('Ce parent existe déjà');
let co_parent: Users | null = null;
if (dto.co_parent_id) {
co_parent = await this.usersRepository.findOneBy({ id: dto.co_parent_id });
if (!co_parent) throw new NotFoundException('Co-parent introuvable');
if (co_parent.role !== RoleType.PARENT) {
throw new BadRequestException('Accès réservé aux parents');
}
}
const entity = this.parentsRepository.create({
user_id: dto.user_id,
user,
co_parent: co_parent ?? undefined,
});
return this.parentsRepository.save(entity);
}
// Liste des parents
async findAll(): Promise<Parents[]> {
return this.parentsRepository.find({
relations: ['user', 'co_parent', 'parentChildren', 'dossiers'],
});
}
// Récupérer un parent par user_id
async findOne(user_id: string): Promise<Parents> {
const parent = await this.parentsRepository.findOne({
where: { user_id },
relations: ['user', 'co_parent', 'parentChildren', 'dossiers'],
});
if (!parent) throw new NotFoundException('Parent introuvable');
return parent;
}
// Mise à jour
async update(id: string, dto: UpdateParentsDto): Promise<Parents> {
await this.parentsRepository.update(id, dto);
return this.findOne(id);
}
}

View File

@ -1,14 +0,0 @@
import { Router } from 'express';
import { ThemeController } from '../controllers/theme.controller';
const router = Router();
// Routes pour les thèmes
router.post('/', ThemeController.createTheme);
router.get('/', ThemeController.getAllThemes);
router.get('/active', ThemeController.getActiveTheme);
router.put('/:themeId/activate', ThemeController.activateTheme);
router.put('/:themeId', ThemeController.updateTheme);
router.delete('/:themeId', ThemeController.deleteTheme);
export default router;

View File

@ -0,0 +1,4 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class CreateAdminDto extends OmitType(CreateUserDto, ['role'] as const) {}

View File

@ -0,0 +1,56 @@
import { OmitType } from "@nestjs/swagger";
import { IsBoolean, IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Length, Matches, Max, Min } from "class-validator";
import { CreateUserDto } from "./create_user.dto";
export class CreateAssistanteDto extends OmitType(CreateUserDto, ['role', 'photo_url', 'consentement_photo'] as const) {
@IsUUID()
@IsNotEmpty()
user_id?: string;
@IsString()
@IsNotEmpty()
@Length(1, 50)
approval_number: string;
@Matches(/^\d{15}$/)
@IsNotEmpty()
nir: string;
@IsInt()
@Min(1)
@Max(10)
@IsNotEmpty()
max_children: number;
@IsString()
@IsNotEmpty()
photo_url: string;
@IsBoolean()
@IsNotEmpty()
consentement_photo: boolean;
@IsDateString()
@IsNotEmpty()
agreement_date: string;
@IsString()
@IsNotEmpty()
@Length(1, 100)
residence_city: string;
@IsOptional()
biography?: string;
@IsOptional()
available?: boolean;
@IsOptional()
years_experience?: number;
@IsOptional()
specialty?: string;
@IsOptional()
places_available?: number;
}

View File

@ -0,0 +1,4 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {}

View File

@ -0,0 +1,17 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
export class CreateParentDto extends OmitType(CreateUserDto, ['role', 'photo_url'] as const) {
@IsUUID()
@IsNotEmpty()
user_id: string;
@IsOptional()
@IsUUID()
co_parent_id?: string;
@IsString()
@IsNotEmpty()
photo_url: string;
}

View File

@ -0,0 +1,105 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsEmail,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
MinLength,
MaxLength,
} from 'class-validator';
import { RoleType, GenreType, StatutUtilisateurType, SituationFamilialeType } from 'src/entities/users.entity';
export class CreateUserDto {
@ApiProperty({ example: 'sosso.test@example.com' })
@IsEmail()
@IsNotEmpty()
email: string;
@ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' })
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
@ApiProperty({ example: 'Julien' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
prenom: string;
@ApiProperty({ example: 'Dupont' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
nom: string;
@ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE })
@IsOptional()
@IsEnum(GenreType)
genre?: GenreType = GenreType.AUTRE;
@ApiProperty({ enum: RoleType })
@IsEnum(RoleType)
role: RoleType;
@ApiProperty({ enum: StatutUtilisateurType, required: false, default: StatutUtilisateurType.EN_ATTENTE })
@IsOptional()
@IsEnum(StatutUtilisateurType)
statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
@ApiProperty({ example: SituationFamilialeType.MARIE, required: false, enum: SituationFamilialeType, default: SituationFamilialeType.MARIE})
@IsOptional()
@IsEnum(SituationFamilialeType)
situation_familiale?: SituationFamilialeType;
@ApiProperty({ example: '+33123456789' })
@IsString()
@IsNotEmpty()
@MaxLength(20)
telephone: string;
@ApiProperty({ example: 'Paris', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
ville?: string;
@ApiProperty({ example: '75000', required: false })
@IsOptional()
@IsString()
@MaxLength(10)
code_postal?: string;
@ApiProperty({ example: '10 rue de la paix, 75000 Paris' })
@IsString()
@IsNotEmpty()
adresse: string;
@ApiProperty({ example: 'https://example.com/photo.jpg', required: false })
@IsOptional()
@IsString()
photo_url?: string;
@ApiProperty({ default: false })
@IsOptional()
@IsBoolean()
consentement_photo?: boolean = false;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString({}, { message: 'date_consentement_photo doit être une date ISO valide' })
date_consentement_photo?: string | null;
@ApiProperty({ default: false })
@IsOptional()
@IsBoolean()
changement_mdp_obligatoire?: boolean = false;
@ApiProperty({ example: true })
@IsBoolean()
@IsNotEmpty()
cguAccepted: boolean;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateAdminDto } from "./create_admin.dto";
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateAssistanteDto } from "./create_assistante.dto";
export class UpdateAssistanteDto extends PartialType(CreateAssistanteDto) {}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateGestionnaireDto } from "./create_gestionnaire.dto";
export class UpdateGestionnaireDto extends PartialType(CreateGestionnaireDto) {}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateParentDto } from "./create_parent.dto";
export class UpdateParentsDto extends PartialType(CreateParentDto) {}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class UpdateUserDto extends PartialType(CreateUserDto) {}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GestionnairesController } from './gestionnaires.controller';
import { GestionnairesService } from './gestionnaires.service';
describe('GestionnairesController', () => {
let controller: GestionnairesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GestionnairesController],
providers: [GestionnairesService],
}).compile();
controller = module.get<GestionnairesController>(GestionnairesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,73 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { GestionnairesService } from './gestionnaires.service';
import { RoleType, Users } from 'src/entities/users.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
@ApiTags('Gestionnaires')
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, RolesGuard)
@Controller('gestionnaires')
export class GestionnairesController {
constructor(private readonly gestionnairesService: GestionnairesService) { }
@Roles(RoleType.SUPER_ADMIN)
@ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users })
@ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' })
@ApiOperation({ summary: 'Création d\'un gestionnaire' })
@ApiBody({ type: CreateGestionnaireDto })
@Post()
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
return this.gestionnairesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Liste des gestionnaires' })
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
@Get()
getAll(): Promise<Users[]> {
return this.gestionnairesService.findAll();
}
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Récupérer un gestionnaire par ID' })
@ApiResponse({ status: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 401, description: 'Non authentifié' })
@ApiParam({ name: 'id', description: 'ID du gestionnaire' })
@ApiResponse({ status: 200, description: 'Gestionnaire trouvé', type: Users })
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
@Get(':id')
findOne(@Param('id') id: string): Promise<Users> {
return this.gestionnairesService.findOne(id);
}
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Mettre à jour un gestionnaire' })
@ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users })
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 401, description: 'Non authentifié' })
@ApiParam({ name: 'id', description: 'ID du gestionnaire' })
@Patch(':id')
update(
@Param('id') id: string,
@Body() dto: UpdateGestionnaireDto,
): Promise<Users | null> {
return this.gestionnairesService.update(id, dto);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { GestionnairesService } from './gestionnaires.service';
import { GestionnairesController } from './gestionnaires.controller';
import { Users } from 'src/entities/users.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Users])],
controllers: [GestionnairesController],
providers: [GestionnairesService],
})
export class GestionnairesModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GestionnairesService } from './gestionnaires.service';
describe('GestionnairesService', () => {
let service: GestionnairesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GestionnairesService],
}).compile();
service = module.get<GestionnairesService>(GestionnairesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,87 @@
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class GestionnairesService {
constructor(
@InjectRepository(Users)
private readonly gestionnaireRepository: Repository<Users>,
) { }
// Création dun gestionnaire
async create(dto: CreateGestionnaireDto): Promise<Users> {
const exist = await this.gestionnaireRepository.findOneBy({ email: dto.email });
if (exist) throw new ConflictException('Email déjà utilisé');
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(dto.password, salt);
const entity = this.gestionnaireRepository.create({
email: dto.email,
password: hashedPassword,
prenom: dto.prenom,
nom: dto.nom,
genre: dto.genre,
statut: dto.statut,
telephone: dto.telephone,
adresse: dto.adresse,
photo_url: dto.photo_url,
consentement_photo: dto.consentement_photo ?? false,
date_consentement_photo: dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined,
changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false,
role: RoleType.GESTIONNAIRE,
});
return this.gestionnaireRepository.save(entity);
}
// Liste des gestionnaires
async findAll(): Promise<Users[]> {
return this.gestionnaireRepository.find({ where: { role: RoleType.GESTIONNAIRE } });
}
// Récupérer un gestionnaire par ID
async findOne(id: string): Promise<Users> {
const gestionnaire = await this.gestionnaireRepository.findOne({
where: { id, role: RoleType.GESTIONNAIRE },
});
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
return gestionnaire;
}
// Mise à jour dun gestionnaire
async update(id: string, dto: UpdateGestionnaireDto): Promise<Users> {
const gestionnaire = await this.findOne(id);
if (dto.password) {
const salt = await bcrypt.genSalt();
gestionnaire.password = await bcrypt.hash(dto.password, salt);
}
if (dto.date_consentement_photo !== undefined) {
gestionnaire.date_consentement_photo = dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined;
}
const { password, date_consentement_photo, ...rest } = dto;
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined) {
(gestionnaire as any)[key] = value;
}
});
return this.gestionnaireRepository.save(gestionnaire);
}
}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,94 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { User } from 'src/common/decorators/user.decorator';
import { RoleType, Users } from 'src/entities/users.entity';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create_user.dto';
import { UpdateUserDto } from './dto/update_user.dto';
@ApiTags('Utilisateurs')
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard)
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) { }
// Création d'un utilisateur (réservée aux super admins)
@Post()
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Créer un nouvel utilisateur (super admin seulement)' })
createUser(
@Body() dto: CreateUserDto,
@User() currentUser: Users
) {
return this.userService.createUser(dto, currentUser);
}
// Lister tous les utilisateurs (super_admin uniquement)
@Get()
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
findAll() {
return this.userService.findAll();
}
// Récupérer un utilisateur par son ID
@Get(':id')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Trouver un utilisateur par son id' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
// Modifier un utilisateur (réservé super_admin)
@Patch(':id')
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Mettre à jour un utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
updateUser(
@Param('id') id: string,
@Body() dto: UpdateUserDto,
@User() currentUser: Users
) {
return this.userService.updateUser(id, dto, currentUser);
}
@Patch(':id/valider')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Valider un compte utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
@ApiResponse({ status: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 200, description: 'Compte validé avec succès' })
validate(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
) {
return this.userService.validateUser(id, currentUser, comment);
}
@Patch(':id/suspendre')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Suspendre un compte utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
suspend(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
) {
return this.userService.suspendUser(id, currentUser, comment);
}
// Supprimer un utilisateur (super_admin uniquement)
@Delete(':id')
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Supprimer un utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
remove(@Param('id') id: string, @User() currentUser: Users) {
return this.userService.remove(id, currentUser);
}
}

View File

@ -0,0 +1,28 @@
import { forwardRef, Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/users.entity';
import { AuthModule } from '../auth/auth.module';
import { Validation } from 'src/entities/validations.entity';
import { ParentsModule } from '../parents/parents.module';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
import { Parents } from 'src/entities/parents.entity';
@Module({
imports: [TypeOrmModule.forFeature(
[
Users,
Validation,
Parents,
AssistanteMaternelle,
]), forwardRef(() => AuthModule),
ParentsModule,
AssistantesMaternellesModule,
],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More