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:
2025-11-24 15:44:07 +01:00
parent 49f0684ad3
commit 33d6e7b0c3
108 changed files with 13872 additions and 1908 deletions
+23
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
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
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
+39
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
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
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.
+72
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
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
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+10226 -1341
View File
File diff suppressed because it is too large Load Diff
+81 -26
View File
@@ -1,36 +1,91 @@
{ {
"name": "petitspas-backend", "name": "ptitspas-ynov-back",
"version": "1.0.0", "version": "0.0.1",
"description": "Backend pour l'application P'titsPas", "description": "",
"main": "dist/index.js", "author": "",
"private": true,
"license": "UNLICENSED",
"scripts": { "scripts": {
"start": "node dist/index.js", "typeorm": "typeorm-ts-node-commonjs",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts", "migration:run": "npm run typeorm migration:run",
"build": "tsc", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "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": { "dependencies": {
"@nestjs/common": "^11.1.0", "@nestjs/common": "^11.1.6",
"@prisma/client": "^6.7.0", "@nestjs/config": "^4.0.2",
"@types/jsonwebtoken": "^9.0.9", "@nestjs/core": "^11.0.1",
"bcrypt": "^5.1.1", "@nestjs/jwt": "^11.0.0",
"cors": "^2.8.5", "@nestjs/mapped-types": "^2.1.0",
"express": "^4.18.2", "@nestjs/platform-express": "^11.0.1",
"helmet": "^7.1.0", "@nestjs/swagger": "^11.2.0",
"jsonwebtoken": "^9.0.2", "@nestjs/typeorm": "^11.0.0",
"morgan": "^1.10.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": { "devDependencies": {
"@types/bcrypt": "^5.0.2", "@eslint/eslintrc": "^3.2.0",
"@types/cors": "^2.8.17", "@eslint/js": "^9.18.0",
"@types/express": "^4.17.21", "@nestjs/cli": "^11.0.10",
"@types/helmet": "^4.0.0", "@nestjs/schematics": "^11.0.0",
"@types/morgan": "^1.9.9", "@nestjs/testing": "^11.0.1",
"@types/node": "^20.11.19", "@types/bcrypt": "^6.0.0",
"prisma": "^6.7.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": "^10.9.2",
"ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3" "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"
} }
} }
-108
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
}
-18
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);
}
}
-18
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 {}
-40
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' };
}
}
+22
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!');
});
});
});
+17
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();
}
}
+56 -6
View File
@@ -1,17 +1,67 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module'; import { AppController } from './app.controller';
import { AuthModule } from './auth/auth.module'; import { AppService } from './app.service';
import { AdminModule } from './admin/admin.module'; 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({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ 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, 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, AuthModule,
AdminModule, ],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: SentryGlobalFilter
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
}
], ],
}) })
export class AppModule { } export class AppModule { }
+62
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"
};
}
}
@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@@ -0,0 +1,3 @@
import { SetMetadata } from "@nestjs/common";
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
@@ -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;
});
@@ -0,0 +1,11 @@
import { IsDateString, IsOptional } from "class-validator";
export class DateRangeQueryDto {
@IsOptional()
@IsDateString()
start?: string;
@IsOptional()
@IsDateString()
end?: string;
}
+6
View File
@@ -0,0 +1,6 @@
import { IsUUID } from "class-validator";
export class IdParamDto {
@IsUUID()
id: string;
}
@@ -0,0 +1,11 @@
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@IsPositive()
offset?: number;
@IsOptional()
@IsPositive()
limit?: number;
}
@@ -0,0 +1,8 @@
import { IsOptional, IsString, MinLength } from "class-validator";
export class SearchQueryDto {
@IsOptional()
@IsString()
@MinLength(2)
q?: string;
}
@@ -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,
});
}
}
+49
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é');
}
}
}
+26
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);
}
}
@@ -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
})),
);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('', () => ({
port: process.env.PORT,
env: process.env.NODE_ENV,
}));
+9
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,
}));
+8
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,
}));
+21
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(),
});
@@ -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' });
}
}
}
@@ -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;
}
@@ -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;
}
+74
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[];
}
+57
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;
}
+61
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[];
}
+79
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;
}
+29
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;
}
@@ -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;
}
+31
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[];
}
@@ -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;
}
@@ -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;
}
+21
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;
}
+150
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[];
}
@@ -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;
}
-28
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
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);
});
@@ -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();
});
});
@@ -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);
}
}
@@ -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 { }
@@ -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();
});
});
@@ -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' };
}
}
-95
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;
@@ -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();
});
});
@@ -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);
}
}
+24
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 {}
@@ -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();
});
});
+137
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'}
}
}
+17
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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class DossiersModule {}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateEnfantsDto } from './create_enfants.dto';
export class UpdateEnfantsDto extends PartialType(CreateEnfantsDto) {}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 { }
@@ -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();
});
});
@@ -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);
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 { }
@@ -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();
});
});
@@ -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);
}
}
-14
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;
@@ -0,0 +1,4 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class CreateAdminDto extends OmitType(CreateUserDto, ['role'] as const) {}
@@ -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;
}
@@ -0,0 +1,4 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateAdminDto } from "./create_admin.dto";
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateAssistanteDto } from "./create_assistante.dto";
export class UpdateAssistanteDto extends PartialType(CreateAssistanteDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateGestionnaireDto } from "./create_gestionnaire.dto";
export class UpdateGestionnaireDto extends PartialType(CreateGestionnaireDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateParentDto } from "./create_parent.dto";
export class UpdateParentsDto extends PartialType(CreateParentDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
export class UpdateUserDto extends PartialType(CreateUserDto) {}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 { }
@@ -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();
});
});
@@ -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);
}
}
@@ -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();
});
});
@@ -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);
}
}
+28
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 { }
@@ -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