feat(#90): API Inscription AM - POST /auth/register/am

- DTO RegisterAMCompletDto (identité, photo, infos pro, CGU)
- Endpoint POST /auth/register/am + inscrireAMComplet() (transaction User + AssistanteMaternelle)
- Photo base64, token création MDP, consentement photo
- Suppression legacy: route register/parent/legacy, registerParent(), RegisterParentDto
- Frontend: ApiConfig.registerAM pour ticket #91

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-16 00:05:23 +01:00
co-authored by Cursor
parent 111935e451
commit c94f2cf0d5
6 changed files with 249 additions and 233 deletions
+79 -121
View File
@@ -13,13 +13,14 @@ 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 { RegisterAMCompletDto } from './dto/register-am-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 { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { LoginDto } from './dto/login.dto';
import { AppConfigService } from 'src/modules/config/config.service';
@@ -116,7 +117,7 @@ export class AuthService {
}
/**
* Inscription utilisateur OBSOLÈTE - Utiliser registerParent() ou registerAM()
* Inscription utilisateur OBSOLÈTE - Utiliser inscrireParentComplet() ou registerAM()
* @deprecated
*/
async register(registerDto: RegisterDto) {
@@ -157,125 +158,6 @@ export class AuthService {
};
}
/**
* Inscription Parent (étape 1/6 du workflow CDC)
* SANS mot de passe - Token de création MDP généré
*/
async registerParent(dto: RegisterParentDto) {
// 1. Vérifier que l'email n'existe pas
const exists = await this.usersService.findByEmailOrNull(dto.email);
if (exists) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
// 2. Vérifier l'email du co-parent s'il existe
if (dto.co_parent_email) {
const coParentExists = await this.usersService.findByEmailOrNull(dto.co_parent_email);
if (coParentExists) {
throw new ConflictException('L\'email du co-parent est déjà utilisé');
}
}
// 3. Récupérer la durée d'expiration du token depuis la config
const tokenExpiryDays = await this.appConfigService.get<number>(
'password_reset_token_expiry_days',
7,
);
// 4. Générer les tokens de création de mot de passe
const tokenCreationMdp = crypto.randomUUID();
const tokenExpiration = new Date();
tokenExpiration.setDate(tokenExpiration.getDate() + tokenExpiryDays);
// 5. Transaction : Créer Parent 1 + Parent 2 (si existe) + entités parents
const result = await this.usersRepo.manager.transaction(async (manager) => {
// Créer Parent 1
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,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: tokenExpiration,
});
const savedParent1 = await manager.save(Users, parent1);
// Créer Parent 2 si renseigné
let savedParent2: 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 tokenExpirationCoParent = new Date();
tokenExpirationCoParent.setDate(tokenExpirationCoParent.getDate() + tokenExpiryDays);
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: tokenExpirationCoParent,
});
savedParent2 = await manager.save(Users, parent2);
}
// Créer l'entité métier Parents pour Parent 1
const parentEntity = manager.create(Parents, {
user_id: savedParent1.id,
});
parentEntity.user = savedParent1;
if (savedParent2) {
parentEntity.co_parent = savedParent2;
}
await manager.save(Parents, parentEntity);
// Créer l'entité métier Parents pour Parent 2 (si existe)
if (savedParent2) {
const coParentEntity = manager.create(Parents, {
user_id: savedParent2.id,
});
coParentEntity.user = savedParent2;
coParentEntity.co_parent = savedParent1;
await manager.save(Parents, coParentEntity);
}
return {
parent1: savedParent1,
parent2: savedParent2,
tokenCreationMdp,
tokenCoParent,
};
});
// 6. TODO: Envoyer email avec lien de création de MDP
// await this.mailService.sendPasswordCreationEmail(result.parent1, result.tokenCreationMdp);
// if (result.parent2 && result.tokenCoParent) {
// await this.mailService.sendPasswordCreationEmail(result.parent2, result.tokenCoParent);
// }
return {
message: 'Inscription réussie. Un email de validation vous a été envoyé.',
parent_id: result.parent1.id,
co_parent_id: result.parent2?.id,
statut: StatutUtilisateurType.EN_ATTENTE,
};
}
/**
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
@@ -432,6 +314,82 @@ export class AuthService {
};
}
/**
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
*/
async inscrireAMComplet(dto: RegisterAMCompletDto) {
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
throw new BadRequestException(
"L'acceptation des CGU et de la politique de confidentialité est obligatoire",
);
}
const existe = await this.usersService.findByEmailOrNull(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
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);
let urlPhoto: string | null = null;
if (dto.photo_base64 && dto.photo_filename) {
urlPhoto = await this.sauvegarderPhotoDepuisBase64(dto.photo_base64, dto.photo_filename);
}
const dateConsentementPhoto =
dto.consentement_photo ? new Date() : undefined;
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const user = manager.create(Users, {
email: dto.email,
prenom: dto.prenom,
nom: dto.nom,
role: RoleType.ASSISTANTE_MATERNELLE,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.telephone,
adresse: dto.adresse,
code_postal: dto.code_postal,
ville: dto.ville,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: dateExpiration,
photo_url: urlPhoto ?? undefined,
consentement_photo: dto.consentement_photo,
date_consentement_photo: dateConsentementPhoto,
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
});
const userEnregistre = await manager.save(Users, user);
const amRepo = manager.getRepository(AssistanteMaternelle);
const am = amRepo.create({
user_id: userEnregistre.id,
approval_number: dto.numero_agrement,
nir: dto.nir,
max_children: dto.capacite_accueil,
biography: dto.biographie,
residence_city: dto.ville ?? undefined,
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
available: true,
});
await amRepo.save(am);
return { user: userEnregistre };
});
return {
message:
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
user_id: resultat.user.id,
statut: StatutUtilisateurType.EN_ATTENTE,
};
}
/**
* Sauvegarde une photo depuis base64 vers le système de fichiers
*/