feat(auth): API inscription parent complete - Workflow 6 etapes
- Refonte complete de l'inscription parent (Tickets #18 et #19 fusionnes) - Workflow CDC 6 etapes en 1 transaction atomique : * Etape 1 : Informations Parent 1 (obligatoire) * Etape 2 : Informations Parent 2 / Co-parent (optionnel) * Etape 3 : Enfants avec photos (au moins 1 requis) * Etape 4 : Presentation du dossier (optionnel) * Etape 5 : Acceptation CGU + Privacy (obligatoire) * Etape 6 : Recapitulatif -> VALIDATION Modifications techniques : - Nouveau DTO RegisterParentCompletDto (Parent1+Parent2+Enfants+Presentation+CGU) - Nouveau DTO EnfantInscriptionDto pour les enfants - Methode inscrireParentComplet() : transaction unique - Generation tokens creation MDP (Parent 1 + Parent 2) - Gestion photos enfants (base64 -> fichier) - Liens parents-enfants via table parents_children - Statut en_attente pour validation gestionnaire Tests : - Teste avec couple MARTIN + 3 triples (Emma, Noah, Lea) - 2 parents crees + 3 enfants lies Documentation : - Ajout 99_REGLES-CODAGE.md : Convention francais/anglais - Tickets Gitea mis a jour (#18 refonte, #19 ferme) Refs: #18, #19
This commit is contained in:
parent
dfad408902
commit
2fb53d20cf
@ -4,6 +4,7 @@ import { AuthService } from './auth.service';
|
|||||||
import { Public } from 'src/common/decorators/public.decorator';
|
import { Public } from 'src/common/decorators/public.decorator';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { RegisterParentDto } from './dto/register-parent.dto';
|
import { RegisterParentDto } from './dto/register-parent.dto';
|
||||||
|
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
@ -39,10 +40,23 @@ export class AuthController {
|
|||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Post('register/parent')
|
@Post('register/parent')
|
||||||
@ApiOperation({ summary: 'Inscription Parent (étape 1/6)' })
|
@ApiOperation({
|
||||||
|
summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes',
|
||||||
|
description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||||
|
async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) {
|
||||||
|
return this.authService.inscrireParentComplet(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('register/parent/legacy')
|
||||||
|
@ApiOperation({ summary: '[OBSOLÈTE] Inscription Parent (étape 1/6 uniquement)' })
|
||||||
@ApiResponse({ status: 201, description: 'Inscription réussie' })
|
@ApiResponse({ status: 201, description: 'Inscription réussie' })
|
||||||
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||||
async registerParent(@Body() dto: RegisterParentDto) {
|
async registerParentLegacy(@Body() dto: RegisterParentDto) {
|
||||||
return this.authService.registerParent(dto);
|
return this.authService.registerParent(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { Children } from 'src/entities/children.entity';
|
||||||
import { AppConfigModule } from 'src/modules/config';
|
import { AppConfigModule } from 'src/modules/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Users, Parents]),
|
TypeOrmModule.forFeature([Users, Parents, Children]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
@ -9,11 +10,16 @@ import { UserService } from '../user/user.service';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { RegisterParentDto } from './dto/register-parent.dto';
|
import { RegisterParentDto } from './dto/register-parent.dto';
|
||||||
|
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
|
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { AppConfigService } from 'src/modules/config/config.service';
|
import { AppConfigService } from 'src/modules/config/config.service';
|
||||||
|
|
||||||
@ -28,6 +34,8 @@ export class AuthService {
|
|||||||
private readonly parentsRepo: Repository<Parents>,
|
private readonly parentsRepo: Repository<Parents>,
|
||||||
@InjectRepository(Users)
|
@InjectRepository(Users)
|
||||||
private readonly usersRepo: Repository<Users>,
|
private readonly usersRepo: Repository<Users>,
|
||||||
|
@InjectRepository(Children)
|
||||||
|
private readonly childrenRepo: Repository<Children>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -274,9 +282,192 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
|
||||||
|
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
|
||||||
|
*/
|
||||||
|
async inscrireParentComplet(dto: RegisterParentCompletDto) {
|
||||||
|
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
|
||||||
|
throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dto.enfants || dto.enfants.length === 0) {
|
||||||
|
throw new BadRequestException('Au moins un enfant est requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existe = await this.usersService.findByEmailOrNull(dto.email);
|
||||||
|
if (existe) {
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.co_parent_email) {
|
||||||
|
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
||||||
|
if (coParentExiste) {
|
||||||
|
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||||
|
'password_reset_token_expiry_days',
|
||||||
|
7,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenCreationMdp = crypto.randomUUID();
|
||||||
|
const dateExpiration = new Date();
|
||||||
|
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
|
||||||
|
|
||||||
|
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
|
||||||
|
const parent1 = manager.create(Users, {
|
||||||
|
email: dto.email,
|
||||||
|
prenom: dto.prenom,
|
||||||
|
nom: dto.nom,
|
||||||
|
role: RoleType.PARENT,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
telephone: dto.telephone,
|
||||||
|
adresse: dto.adresse,
|
||||||
|
code_postal: dto.code_postal,
|
||||||
|
ville: dto.ville,
|
||||||
|
profession: dto.profession,
|
||||||
|
situation_familiale: dto.situation_familiale,
|
||||||
|
token_creation_mdp: tokenCreationMdp,
|
||||||
|
token_creation_mdp_expire_le: dateExpiration,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dto.date_naissance) {
|
||||||
|
parent1.date_naissance = new Date(dto.date_naissance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent1Enregistre = await manager.save(Users, parent1);
|
||||||
|
|
||||||
|
let parent2Enregistre: Users | null = null;
|
||||||
|
let tokenCoParent: string | null = null;
|
||||||
|
|
||||||
|
if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
|
||||||
|
tokenCoParent = crypto.randomUUID();
|
||||||
|
const dateExpirationCoParent = new Date();
|
||||||
|
dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken);
|
||||||
|
|
||||||
|
const parent2 = manager.create(Users, {
|
||||||
|
email: dto.co_parent_email,
|
||||||
|
prenom: dto.co_parent_prenom,
|
||||||
|
nom: dto.co_parent_nom,
|
||||||
|
role: RoleType.PARENT,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
telephone: dto.co_parent_telephone,
|
||||||
|
adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
|
||||||
|
code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
|
||||||
|
ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
|
||||||
|
token_creation_mdp: tokenCoParent,
|
||||||
|
token_creation_mdp_expire_le: dateExpirationCoParent,
|
||||||
|
});
|
||||||
|
|
||||||
|
parent2Enregistre = await manager.save(Users, parent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entiteParent = manager.create(Parents, {
|
||||||
|
user_id: parent1Enregistre.id,
|
||||||
|
});
|
||||||
|
entiteParent.user = parent1Enregistre;
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
entiteParent.co_parent = parent2Enregistre;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.save(Parents, entiteParent);
|
||||||
|
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
const entiteCoParent = manager.create(Parents, {
|
||||||
|
user_id: parent2Enregistre.id,
|
||||||
|
});
|
||||||
|
entiteCoParent.user = parent2Enregistre;
|
||||||
|
entiteCoParent.co_parent = parent1Enregistre;
|
||||||
|
|
||||||
|
await manager.save(Parents, entiteCoParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfantsEnregistres: Children[] = [];
|
||||||
|
for (const enfantDto of dto.enfants) {
|
||||||
|
let urlPhoto: string | null = null;
|
||||||
|
|
||||||
|
if (enfantDto.photo_base64 && enfantDto.photo_filename) {
|
||||||
|
urlPhoto = await this.sauvegarderPhotoDepuisBase64(
|
||||||
|
enfantDto.photo_base64,
|
||||||
|
enfantDto.photo_filename,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfant = new Children();
|
||||||
|
enfant.first_name = enfantDto.prenom;
|
||||||
|
enfant.last_name = enfantDto.nom || dto.nom;
|
||||||
|
enfant.gender = enfantDto.genre;
|
||||||
|
enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined;
|
||||||
|
enfant.due_date = enfantDto.date_previsionnelle_naissance
|
||||||
|
? new Date(enfantDto.date_previsionnelle_naissance)
|
||||||
|
: undefined;
|
||||||
|
enfant.photo_url = urlPhoto || undefined;
|
||||||
|
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
|
||||||
|
enfant.consent_photo = false;
|
||||||
|
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
||||||
|
|
||||||
|
const enfantEnregistre = await manager.save(Children, enfant);
|
||||||
|
enfantsEnregistres.push(enfantEnregistre);
|
||||||
|
|
||||||
|
const lienParentEnfant1 = manager.create(ParentsChildren, {
|
||||||
|
parentId: parent1Enregistre.id,
|
||||||
|
enfantId: enfantEnregistre.id,
|
||||||
|
});
|
||||||
|
await manager.save(ParentsChildren, lienParentEnfant1);
|
||||||
|
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
const lienParentEnfant2 = manager.create(ParentsChildren, {
|
||||||
|
parentId: parent2Enregistre.id,
|
||||||
|
enfantId: enfantEnregistre.id,
|
||||||
|
});
|
||||||
|
await manager.save(ParentsChildren, lienParentEnfant2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parent1: parent1Enregistre,
|
||||||
|
parent2: parent2Enregistre,
|
||||||
|
enfants: enfantsEnregistres,
|
||||||
|
tokenCreationMdp,
|
||||||
|
tokenCoParent,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
|
||||||
|
parent_id: resultat.parent1.id,
|
||||||
|
co_parent_id: resultat.parent2?.id,
|
||||||
|
enfants_ids: resultat.enfants.map(e => e.id),
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde une photo depuis base64 vers le système de fichiers
|
||||||
|
*/
|
||||||
|
private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise<string> {
|
||||||
|
const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/);
|
||||||
|
if (!correspondances) {
|
||||||
|
throw new BadRequestException('Format de photo invalide (doit être base64)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = correspondances[1];
|
||||||
|
const tamponImage = Buffer.from(correspondances[2], 'base64');
|
||||||
|
|
||||||
|
const dossierUpload = '/app/uploads/photos';
|
||||||
|
await fs.mkdir(dossierUpload, { recursive: true });
|
||||||
|
|
||||||
|
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
|
||||||
|
const cheminFichier = path.join(dossierUpload, nomFichierUnique);
|
||||||
|
|
||||||
|
await fs.writeFile(cheminFichier, tamponImage);
|
||||||
|
|
||||||
|
return `/uploads/photos/${nomFichierUnique}`;
|
||||||
|
}
|
||||||
|
|
||||||
async logout(userId: string) {
|
async logout(userId: string) {
|
||||||
// Pour le moment envoyer un message clair
|
|
||||||
return { success: true, message: 'Deconnexion'}
|
return { success: true, message: 'Deconnexion'}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
backend/src/routes/auth/dto/enfant-inscription.dto.ts
Normal file
63
backend/src/routes/auth/dto/enfant-inscription.dto.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
IsDateString,
|
||||||
|
IsBoolean,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { GenreType } from 'src/entities/children.entity';
|
||||||
|
|
||||||
|
export class EnfantInscriptionDto {
|
||||||
|
@ApiProperty({ example: 'Emma', required: false, description: 'Prénom de l\'enfant (obligatoire si déjà né)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
|
||||||
|
prenom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN', required: false, description: 'Nom de l\'enfant (hérité des parents si non fourni)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
|
||||||
|
nom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-02-15', required: false, description: 'Date de naissance (si enfant déjà né)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2025-06-15', required: false, description: 'Date prévisionnelle de naissance (si enfant à naître)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_previsionnelle_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: GenreType, example: GenreType.F })
|
||||||
|
@IsEnum(GenreType, { message: 'Le genre doit être H, F ou Autre' })
|
||||||
|
@IsNotEmpty({ message: 'Le genre est requis' })
|
||||||
|
genre: GenreType;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
|
||||||
|
required: false,
|
||||||
|
description: 'Photo de l\'enfant en base64 (obligatoire si déjà né)'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_base64?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'emma_martin.jpg', required: false, description: 'Nom du fichier photo' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_filename?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
grossesse_multiple?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
182
backend/src/routes/auth/dto/register-parent-complet.dto.ts
Normal file
182
backend/src/routes/auth/dto/register-parent-complet.dto.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsBoolean,
|
||||||
|
IsArray,
|
||||||
|
ValidateNested,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { SituationFamilialeType } from 'src/entities/users.entity';
|
||||||
|
import { EnfantInscriptionDto } from './enfant-inscription.dto';
|
||||||
|
|
||||||
|
export class RegisterParentCompletDto {
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 1 : PARENT 1 (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'claire.martin@ptits-pas.fr' })
|
||||||
|
@IsEmail({}, { message: 'Email invalide' })
|
||||||
|
@IsNotEmpty({ message: 'L\'email est requis' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Claire' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le prénom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le nom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
|
||||||
|
nom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '0689567890' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le téléphone est requis' })
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
|
||||||
|
})
|
||||||
|
telephone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '95870', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10)
|
||||||
|
code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bezons', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(150)
|
||||||
|
ville?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Infirmière', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(150)
|
||||||
|
profession?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: SituationFamilialeType, example: SituationFamilialeType.MARIE, required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(SituationFamilialeType)
|
||||||
|
situation_familiale?: SituationFamilialeType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1990-04-03', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_naissance?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 2 : PARENT 2 / CO-PARENT (Optionnel)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail({}, { message: 'Email du co-parent invalide' })
|
||||||
|
co_parent_email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Thomas', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_prenom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_nom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '0678456789', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone du co-parent doit être valide',
|
||||||
|
})
|
||||||
|
co_parent_telephone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Le co-parent habite à la même adresse', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
co_parent_meme_adresse?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_ville?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 3 : ENFANT(S) (Au moins 1 requis)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: [EnfantInscriptionDto],
|
||||||
|
description: 'Liste des enfants (au moins 1 requis)',
|
||||||
|
example: [{
|
||||||
|
prenom: 'Emma',
|
||||||
|
nom: 'MARTIN',
|
||||||
|
date_naissance: '2023-02-15',
|
||||||
|
genre: 'F',
|
||||||
|
photo_base64: 'data:image/jpeg;base64,...',
|
||||||
|
photo_filename: 'emma_martin.jpg'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
@IsArray({ message: 'La liste des enfants doit être un tableau' })
|
||||||
|
@IsNotEmpty({ message: 'Au moins un enfant est requis' })
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EnfantInscriptionDto)
|
||||||
|
enfants: EnfantInscriptionDto[];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 4 : PRÉSENTATION DU DOSSIER (Optionnel)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Nous recherchons une assistante maternelle bienveillante pour nos triplés...',
|
||||||
|
required: false,
|
||||||
|
description: 'Présentation du dossier (max 2000 caractères)'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
|
||||||
|
presentation_dossier?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 5 : ACCEPTATION CGU (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Acceptation des Conditions Générales d\'Utilisation' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: 'L\'acceptation des CGU est requise' })
|
||||||
|
acceptation_cgu: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: 'L\'acceptation de la politique de confidentialité est requise' })
|
||||||
|
acceptation_privacy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
344
docs/99_REGLES-CODAGE.md
Normal file
344
docs/99_REGLES-CODAGE.md
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
# 📐 Règles de Codage - Projet P'titsPas
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : 1er Décembre 2025
|
||||||
|
**Statut** : ✅ Actif
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Langue du Code
|
||||||
|
|
||||||
|
### Principe Général
|
||||||
|
**Tout le code doit être écrit en FRANÇAIS**, sauf les termes techniques qui restent en **ANGLAIS**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Ce qui doit être en FRANÇAIS
|
||||||
|
|
||||||
|
### 1. Noms de variables
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
const utilisateurConnecte = await this.trouverUtilisateur(id);
|
||||||
|
const enfantsEnregistres = [];
|
||||||
|
const tokenCreationMotDePasse = crypto.randomUUID();
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
const loggedUser = await this.findUser(id);
|
||||||
|
const savedChildren = [];
|
||||||
|
const passwordCreationToken = crypto.randomUUID();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Noms de fonctions/méthodes
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
async inscrireParentComplet(dto: DtoInscriptionParentComplet) { }
|
||||||
|
async creerGestionnaire(dto: DtoCreationGestionnaire) { }
|
||||||
|
async validerCompte(idUtilisateur: string) { }
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
async registerParentComplete(dto: RegisterParentCompleteDto) { }
|
||||||
|
async createManager(dto: CreateManagerDto) { }
|
||||||
|
async validateAccount(userId: string) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Noms de classes/interfaces/types
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
export class DtoInscriptionParentComplet { }
|
||||||
|
export class ServiceAuthentification { }
|
||||||
|
export interface OptionsConfiguration { }
|
||||||
|
export type StatutUtilisateur = 'actif' | 'en_attente' | 'suspendu';
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
export class RegisterParentCompleteDto { }
|
||||||
|
export class AuthService { }
|
||||||
|
export interface ConfigOptions { }
|
||||||
|
export type UserStatus = 'active' | 'pending' | 'suspended';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Noms de fichiers
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
inscription-parent-complet.dto.ts
|
||||||
|
service-authentification.ts
|
||||||
|
entite-utilisateurs.ts
|
||||||
|
controleur-configuration.ts
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
register-parent-complete.dto.ts
|
||||||
|
auth.service.ts
|
||||||
|
users.entity.ts
|
||||||
|
config.controller.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Propriétés d'entités/DTOs
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
export class Enfants {
|
||||||
|
@Column({ name: 'prenom' })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@Column({ name: 'date_naissance' })
|
||||||
|
dateNaissance: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'consentement_photo' })
|
||||||
|
consentementPhoto: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
export class Children {
|
||||||
|
@Column({ name: 'first_name' })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Column({ name: 'birth_date' })
|
||||||
|
birthDate: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'consent_photo' })
|
||||||
|
consentPhoto: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Commentaires
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
// Créer Parent 1 + Parent 2 (si existe) + entités parents
|
||||||
|
// Vérifier que l'email n'existe pas déjà
|
||||||
|
// Transaction : Créer utilisateur + entité métier
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
// Create Parent 1 + Parent 2 (if exists) + parent entities
|
||||||
|
// Check if email already exists
|
||||||
|
// Transaction: Create user + business entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Messages d'erreur/succès
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
return { message: 'Inscription réussie. Votre dossier est en attente de validation.' };
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
throw new ConflictException('An account with this email already exists');
|
||||||
|
return { message: 'Registration successful. Your application is pending validation.' };
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Logs
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
this.logger.log('📦 Chargement de 16 configurations en cache');
|
||||||
|
this.logger.error('Erreur lors de la création du parent');
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
this.logger.log('📦 Loading 16 configurations in cache');
|
||||||
|
this.logger.error('Error creating parent');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Ce qui RESTE en ANGLAIS (Termes Techniques)
|
||||||
|
|
||||||
|
### 1. Patterns de conception
|
||||||
|
- `singleton`
|
||||||
|
- `factory`
|
||||||
|
- `repository`
|
||||||
|
- `observer`
|
||||||
|
- `decorator`
|
||||||
|
|
||||||
|
### 2. Architecture/Framework
|
||||||
|
- `backend` / `frontend`
|
||||||
|
- `controller`
|
||||||
|
- `service`
|
||||||
|
- `middleware`
|
||||||
|
- `guard`
|
||||||
|
- `interceptor`
|
||||||
|
- `pipe`
|
||||||
|
- `filter`
|
||||||
|
- `module`
|
||||||
|
- `provider`
|
||||||
|
|
||||||
|
### 3. Concepts techniques
|
||||||
|
- `entity` (TypeORM)
|
||||||
|
- `DTO` (Data Transfer Object)
|
||||||
|
- `API` / `endpoint`
|
||||||
|
- `token` (JWT)
|
||||||
|
- `hash` (bcrypt)
|
||||||
|
- `cache`
|
||||||
|
- `query`
|
||||||
|
- `transaction`
|
||||||
|
- `migration`
|
||||||
|
- `seed`
|
||||||
|
|
||||||
|
### 4. Bibliothèques/Technologies
|
||||||
|
- `NestJS`
|
||||||
|
- `TypeORM`
|
||||||
|
- `PostgreSQL`
|
||||||
|
- `Docker`
|
||||||
|
- `Git`
|
||||||
|
- `JWT`
|
||||||
|
- `bcrypt`
|
||||||
|
- `Multer`
|
||||||
|
- `Nodemailer`
|
||||||
|
|
||||||
|
### 5. Mots-clés TypeScript/JavaScript
|
||||||
|
- `async` / `await`
|
||||||
|
- `const` / `let` / `var`
|
||||||
|
- `function`
|
||||||
|
- `class`
|
||||||
|
- `interface`
|
||||||
|
- `type`
|
||||||
|
- `enum`
|
||||||
|
- `import` / `export`
|
||||||
|
- `return`
|
||||||
|
- `throw`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Exemples Complets
|
||||||
|
|
||||||
|
### Exemple 1 : Service d'authentification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceAuthentification {
|
||||||
|
constructor(
|
||||||
|
private readonly serviceUtilisateurs: ServiceUtilisateurs,
|
||||||
|
private readonly serviceJwt: JwtService,
|
||||||
|
@InjectRepository(Utilisateurs)
|
||||||
|
private readonly depotUtilisateurs: Repository<Utilisateurs>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async inscrireParentComplet(dto: DtoInscriptionParentComplet) {
|
||||||
|
// Vérifier que l'email n'existe pas
|
||||||
|
const existe = await this.serviceUtilisateurs.trouverParEmail(dto.email);
|
||||||
|
if (existe) {
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer le token de création de mot de passe
|
||||||
|
const tokenCreationMdp = crypto.randomUUID();
|
||||||
|
const dateExpiration = new Date();
|
||||||
|
dateExpiration.setDate(dateExpiration.getDate() + 7);
|
||||||
|
|
||||||
|
// Transaction : Créer parent + enfants
|
||||||
|
const resultat = await this.depotUtilisateurs.manager.transaction(async (manager) => {
|
||||||
|
const parent1 = new Utilisateurs();
|
||||||
|
parent1.email = dto.email;
|
||||||
|
parent1.prenom = dto.prenom;
|
||||||
|
parent1.nom = dto.nom;
|
||||||
|
parent1.tokenCreationMdp = tokenCreationMdp;
|
||||||
|
|
||||||
|
const parentEnregistre = await manager.save(Utilisateurs, parent1);
|
||||||
|
|
||||||
|
return { parent: parentEnregistre, token: tokenCreationMdp };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Inscription réussie. Votre dossier est en attente de validation.',
|
||||||
|
idParent: resultat.parent.id,
|
||||||
|
statut: 'en_attente',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple 2 : Entité Enfants
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
@Entity('enfants')
|
||||||
|
export class Enfants {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'prenom', length: 100 })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@Column({ name: 'nom', length: 100 })
|
||||||
|
nom: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: TypeGenre,
|
||||||
|
name: 'genre'
|
||||||
|
})
|
||||||
|
genre: TypeGenre;
|
||||||
|
|
||||||
|
@Column({ type: 'date', name: 'date_naissance', nullable: true })
|
||||||
|
dateNaissance?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'date', name: 'date_prevue_naissance', nullable: true })
|
||||||
|
datePrevueNaissance?: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'photo_url', type: 'text', nullable: true })
|
||||||
|
photoUrl?: string;
|
||||||
|
|
||||||
|
@Column({ name: 'consentement_photo', type: 'boolean', default: false })
|
||||||
|
consentementPhoto: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'est_multiple', type: 'boolean', default: false })
|
||||||
|
estMultiple: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: StatutEnfantType,
|
||||||
|
name: 'statut'
|
||||||
|
})
|
||||||
|
statut: StatutEnfantType;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Migration Progressive
|
||||||
|
|
||||||
|
### Stratégie
|
||||||
|
1. ✅ **Nouveau code** : Appliquer la règle immédiatement
|
||||||
|
2. ⏳ **Code existant** : Migrer progressivement lors des modifications
|
||||||
|
3. ❌ **Ne PAS refactoriser** tout le code d'un coup
|
||||||
|
|
||||||
|
### Priorités de migration
|
||||||
|
1. **Haute priorité** : Nouveaux fichiers, nouvelles fonctionnalités
|
||||||
|
2. **Moyenne priorité** : Fichiers modifiés fréquemment
|
||||||
|
3. **Basse priorité** : Code stable non modifié
|
||||||
|
|
||||||
|
### Exemple de migration progressive
|
||||||
|
```typescript
|
||||||
|
// Avant (ancien code - OK pour l'instant)
|
||||||
|
export class Children { }
|
||||||
|
|
||||||
|
// Après modification (nouveau code - appliquer la règle)
|
||||||
|
export class Enfants { }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Exceptions
|
||||||
|
|
||||||
|
### Cas où l'anglais est toléré
|
||||||
|
1. **Noms de colonnes en BDD** : Si la BDD existe déjà (ex: `first_name` en BDD → `prenom` en TypeScript)
|
||||||
|
2. **APIs externes** : Noms imposés par des bibliothèques tierces
|
||||||
|
3. **Standards** : `id`, `uuid`, `url`, `email`, `password` (termes universels)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Avant Commit
|
||||||
|
|
||||||
|
- [ ] Noms de variables en français
|
||||||
|
- [ ] Noms de fonctions/méthodes en français
|
||||||
|
- [ ] Noms de classes/interfaces en français
|
||||||
|
- [ ] Noms de fichiers en français
|
||||||
|
- [ ] Propriétés d'entités/DTOs en français
|
||||||
|
- [ ] Commentaires en français
|
||||||
|
- [ ] Messages d'erreur/succès en français
|
||||||
|
- [ ] Termes techniques restent en anglais
|
||||||
|
- [ ] Pas de `console.log` (utiliser `this.logger`)
|
||||||
|
- [ ] Pas de code commenté
|
||||||
|
- [ ] Types TypeScript corrects (pas de `any`)
|
||||||
|
- [ ] Imports propres (pas d'imports inutilisés)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 1er Décembre 2025
|
||||||
|
**Auteur** : Équipe P'titsPas
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user