feat(backend): endpoint inscription parent + nettoyage code #9

🧹 NETTOYAGE CODE ÉTUDIANT:
- Suppression console.log/error (4 occurrences)
- Suppression code commenté inutile
- Suppression champs obsolètes (mobile, telephone_fixe)
- Correction password nullable dans Users entity

 NOUVELLES FONCTIONNALITÉS:
- Ajout champs token_creation_mdp dans Users entity
- Création RegisterParentDto (validation complète)
- Endpoint POST /auth/register/parent
- Méthode registerParent() avec transaction
- Gestion Parent 1 + Parent 2 (co-parent optionnel)
- Génération tokens UUID pour création MDP
- Lecture durée token depuis AppConfigService
- Création automatique entités Parents
- Statut EN_ATTENTE par défaut
- Intégration AppConfigModule dans AuthModule
- Amélioration méthode login() (vérif statut + password null)

📋 WORKFLOW CDC CONFORME:
- Inscription SANS mot de passe
- Token envoyé par email (TODO)
- Validation gestionnaire requise
- Support co-parent avec même adresse

Réf: docs/20_WORKFLOW-CREATION-COMPTE.md
Ticket: #9 (ou #16)
This commit is contained in:
2025-11-30 16:09:54 +01:00
parent 61b45cd830
commit 40c7f40d12
5 changed files with 313 additions and 30 deletions
+166 -21
View File
@@ -3,13 +3,19 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import { RegisterDto } from './dto/register.dto';
import { RegisterParentDto } from './dto/register-parent.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity';
import { LoginDto } from './dto/login.dto';
import { AppConfigService } from 'src/modules/config/config.service';
@Injectable()
export class AuthService {
@@ -17,6 +23,11 @@ export class AuthService {
private readonly usersService: UserService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly appConfigService: AppConfigService,
@InjectRepository(Parents)
private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users)
private readonly usersRepo: Repository<Users>,
) { }
/**
@@ -43,29 +54,37 @@ export class AuthService {
* Connexion utilisateur
*/
async login(dto: LoginDto) {
try {
const user = await this.usersService.findByEmailOrNull(dto.email);
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);
if (!user) {
throw new UnauthorizedException('Identifiants invalides');
}
// Vérifier que le mot de passe existe (compte activé)
if (!user.password) {
throw new UnauthorizedException(
'Compte non activé. Veuillez créer votre mot de passe via le lien reçu par email.',
);
}
// Vérifier le mot de passe
const isMatch = await bcrypt.compare(dto.password, user.password);
if (!isMatch) {
throw new UnauthorizedException('Identifiants invalides');
}
// Vérifier le statut du compte
if (user.statut === StatutUtilisateurType.EN_ATTENTE) {
throw new UnauthorizedException(
'Votre compte est en attente de validation par un gestionnaire.',
);
}
if (user.statut === StatutUtilisateurType.SUSPENDU) {
throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.');
}
return this.generateTokens(user.id, user.email, user.role);
}
/**
@@ -89,7 +108,8 @@ export class AuthService {
}
/**
* Inscription utilisateur lambda (parent ou assistante maternelle)
* Inscription utilisateur OBSOLÈTE - Utiliser registerParent() ou registerAM()
* @deprecated
*/
async register(registerDto: RegisterDto) {
const exists = await this.usersService.findByEmailOrNull(registerDto.email);
@@ -129,6 +149,131 @@ 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,
profession: dto.profession,
situation_familiale: dto.situation_familiale,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: tokenExpiration,
});
if (dto.date_naissance) {
parent1.date_naissance = new Date(dto.date_naissance);
}
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,
};
}
async logout(userId: string) {
// Pour le moment envoyer un message clair
return { success: true, message: 'Deconnexion'}