From 579b6cae90565530024891b43028629b06ce8a34 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Mon, 1 Dec 2025 21:43:36 +0000 Subject: [PATCH] [Backend] API Inscription Parent - REFONTE Workflow 6 etapes (#72) Co-authored-by: Julien Martin Co-committed-by: Julien Martin --- backend/Dockerfile | 3 + backend/src/routes/auth/auth.controller.ts | 18 +- backend/src/routes/auth/auth.module.ts | 3 +- backend/src/routes/auth/auth.service.ts | 195 +++++++++- .../routes/auth/dto/enfant-inscription.dto.ts | 63 ++++ .../auth/dto/register-parent-complet.dto.ts | 182 +++++++++ .../routes/enfants/dto/create_enfants.dto.ts | 6 +- .../src/routes/enfants/enfants.controller.ts | 37 +- backend/src/routes/enfants/enfants.service.ts | 24 +- docs/99_REGLES-CODAGE.md | 344 ++++++++++++++++++ 10 files changed, 861 insertions(+), 14 deletions(-) create mode 100644 backend/src/routes/auth/dto/enfant-inscription.dto.ts create mode 100644 backend/src/routes/auth/dto/register-parent-complet.dto.ts create mode 100644 docs/99_REGLES-CODAGE.md diff --git a/backend/Dockerfile b/backend/Dockerfile index c41f409..b1d64fd 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -32,6 +32,9 @@ COPY --from=builder /app/dist ./dist RUN addgroup -g 1001 -S nodejs RUN adduser -S nestjs -u 1001 +# Créer le dossier uploads et donner les permissions +RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads + USER nestjs EXPOSE 3000 diff --git a/backend/src/routes/auth/auth.controller.ts b/backend/src/routes/auth/auth.controller.ts index 3bbd23e..a68ea36 100644 --- a/backend/src/routes/auth/auth.controller.ts +++ b/backend/src/routes/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { AuthService } from './auth.service'; import { Public } from 'src/common/decorators/public.decorator'; import { RegisterDto } from './dto/register.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 { AuthGuard } from 'src/common/guards/auth.guard'; import type { Request } from 'express'; @@ -39,10 +40,23 @@ export class AuthController { @Public() @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: 409, description: 'Email déjà utilisé' }) - async registerParent(@Body() dto: RegisterParentDto) { + async registerParentLegacy(@Body() dto: RegisterParentDto) { return this.authService.registerParent(dto); } diff --git a/backend/src/routes/auth/auth.module.ts b/backend/src/routes/auth/auth.module.ts index ad6e1b5..6554be7 100644 --- a/backend/src/routes/auth/auth.module.ts +++ b/backend/src/routes/auth/auth.module.ts @@ -7,11 +7,12 @@ import { JwtModule } from '@nestjs/jwt'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Users } from 'src/entities/users.entity'; import { Parents } from 'src/entities/parents.entity'; +import { Children } from 'src/entities/children.entity'; import { AppConfigModule } from 'src/modules/config'; @Module({ imports: [ - TypeOrmModule.forFeature([Users, Parents]), + TypeOrmModule.forFeature([Users, Parents, Children]), forwardRef(() => UserModule), AppConfigModule, JwtModule.registerAsync({ diff --git a/backend/src/routes/auth/auth.service.ts b/backend/src/routes/auth/auth.service.ts index fad8a61..4184fd0 100644 --- a/backend/src/routes/auth/auth.service.ts +++ b/backend/src/routes/auth/auth.service.ts @@ -2,6 +2,7 @@ import { ConflictException, Injectable, UnauthorizedException, + BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -9,11 +10,16 @@ import { UserService } from '../user/user.service'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as path from 'path'; import { RegisterDto } from './dto/register.dto'; import { RegisterParentDto } from './dto/register-parent.dto'; +import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; import { ConfigService } from '@nestjs/config'; import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.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 { AppConfigService } from 'src/modules/config/config.service'; @@ -28,6 +34,8 @@ export class AuthService { private readonly parentsRepo: Repository, @InjectRepository(Users) private readonly usersRepo: Repository, + @InjectRepository(Children) + private readonly childrenRepo: Repository, ) { } /** @@ -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( + '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 { + 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) { - // Pour le moment envoyer un message clair return { success: true, message: 'Deconnexion'} - } } diff --git a/backend/src/routes/auth/dto/enfant-inscription.dto.ts b/backend/src/routes/auth/dto/enfant-inscription.dto.ts new file mode 100644 index 0000000..9ee120c --- /dev/null +++ b/backend/src/routes/auth/dto/enfant-inscription.dto.ts @@ -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; +} + diff --git a/backend/src/routes/auth/dto/register-parent-complet.dto.ts b/backend/src/routes/auth/dto/register-parent-complet.dto.ts new file mode 100644 index 0000000..bfc062a --- /dev/null +++ b/backend/src/routes/auth/dto/register-parent-complet.dto.ts @@ -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; +} + diff --git a/backend/src/routes/enfants/dto/create_enfants.dto.ts b/backend/src/routes/enfants/dto/create_enfants.dto.ts index 7b55282..3ede09d 100644 --- a/backend/src/routes/enfants/dto/create_enfants.dto.ts +++ b/backend/src/routes/enfants/dto/create_enfants.dto.ts @@ -29,10 +29,10 @@ export class CreateEnfantsDto { @MaxLength(100) last_name?: string; - @ApiProperty({ enum: GenreType, required: false }) - @IsOptional() + @ApiProperty({ enum: GenreType }) @IsEnum(GenreType) - gender?: GenreType; + @IsNotEmpty() + gender: GenreType; @ApiProperty({ example: '2018-06-24', required: false }) @ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE) diff --git a/backend/src/routes/enfants/enfants.controller.ts b/backend/src/routes/enfants/enfants.controller.ts index c76dcd5..e112972 100644 --- a/backend/src/routes/enfants/enfants.controller.ts +++ b/backend/src/routes/enfants/enfants.controller.ts @@ -8,8 +8,13 @@ import { Patch, Post, UseGuards, + UseInterceptors, + UploadedFile, } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; import { EnfantsService } from './enfants.service'; import { CreateEnfantsDto } from './dto/create_enfants.dto'; import { UpdateEnfantsDto } from './dto/update_enfants.dto'; @@ -28,8 +33,34 @@ export class EnfantsController { @Roles(RoleType.PARENT) @Post() - create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) { - return this.enfantsService.create(dto, currentUser); + @ApiConsumes('multipart/form-data') + @UseInterceptors( + FileInterceptor('photo', { + storage: diskStorage({ + destination: './uploads/photos', + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = extname(file.originalname); + cb(null, `enfant-${uniqueSuffix}${ext}`); + }, + }), + fileFilter: (req, file, cb) => { + if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) { + return cb(new Error('Seules les images sont autorisées'), false); + } + cb(null, true); + }, + limits: { + fileSize: 5 * 1024 * 1024, + }, + }), + ) + create( + @Body() dto: CreateEnfantsDto, + @UploadedFile() photo: Express.Multer.File, + @User() currentUser: Users, + ) { + return this.enfantsService.create(dto, currentUser, photo); } @Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN) diff --git a/backend/src/routes/enfants/enfants.service.ts b/backend/src/routes/enfants/enfants.service.ts index 952f2b1..bfefedb 100644 --- a/backend/src/routes/enfants/enfants.service.ts +++ b/backend/src/routes/enfants/enfants.service.ts @@ -24,10 +24,11 @@ export class EnfantsService { private readonly parentsChildrenRepository: Repository, ) { } - // Création d’un enfant - async create(dto: CreateEnfantsDto, currentUser: Users): Promise { + // Création d'un enfant + async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise { const parent = await this.parentsRepository.findOne({ where: { user_id: currentUser.id }, + relations: ['co_parent'], }); if (!parent) throw new NotFoundException('Parent introuvable'); @@ -46,17 +47,34 @@ export class EnfantsService { }); if (exist) throw new ConflictException('Cet enfant existe déjà'); + // Gestion de la photo uploadée + if (photoFile) { + dto.photo_url = `/uploads/photos/${photoFile.filename}`; + if (dto.consent_photo) { + dto.consent_photo_at = new Date().toISOString(); + } + } + // Création const child = this.childrenRepository.create(dto); await this.childrenRepository.save(child); - // Lien parent-enfant + // Lien parent-enfant (Parent 1) const parentLink = this.parentsChildrenRepository.create({ parentId: parent.user_id, enfantId: child.id, }); await this.parentsChildrenRepository.save(parentLink); + // Rattachement automatique au co-parent s'il existe + if (parent.co_parent) { + const coParentLink = this.parentsChildrenRepository.create({ + parentId: parent.co_parent.id, + enfantId: child.id, + }); + await this.parentsChildrenRepository.save(coParentLink); + } + return this.findOne(child.id, currentUser); } diff --git a/docs/99_REGLES-CODAGE.md b/docs/99_REGLES-CODAGE.md new file mode 100644 index 0000000..04e10af --- /dev/null +++ b/docs/99_REGLES-CODAGE.md @@ -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, + ) {} + + 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 +