Compare commits
No commits in common. "master" and "feature/7-tables-documents-legaux" have entirely different histories.
master
...
feature/7-
@ -32,9 +32,6 @@ 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
|
||||
|
||||
@ -37,7 +37,6 @@
|
||||
"class-validator": "^0.14.2",
|
||||
"joi": "^18.0.0",
|
||||
"mapped-types": "^0.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.16.3",
|
||||
@ -55,7 +54,6 @@
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
|
||||
@ -15,7 +15,6 @@ import { SentryGlobalFilter } from '@sentry/nestjs/setup';
|
||||
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
|
||||
import { EnfantsModule } from './routes/enfants/enfants.module';
|
||||
import { AppConfigModule } from './modules/config/config.module';
|
||||
import { DocumentsLegauxModule } from './modules/documents-legaux';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -52,7 +51,6 @@ import { DocumentsLegauxModule } from './modules/documents-legaux';
|
||||
EnfantsModule,
|
||||
AuthModule,
|
||||
AppConfigModule,
|
||||
DocumentsLegauxModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Users } from './users.entity';
|
||||
import { DocumentLegal } from './document-legal.entity';
|
||||
|
||||
@Entity('acceptations_documents')
|
||||
export class AcceptationDocument {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ManyToOne(() => Users, { nullable: false, onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'id_utilisateur' })
|
||||
utilisateur: Users;
|
||||
|
||||
@ManyToOne(() => DocumentLegal, { nullable: true })
|
||||
@JoinColumn({ name: 'id_document' })
|
||||
document: DocumentLegal | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
type_document: 'cgu' | 'privacy';
|
||||
|
||||
@Column({ type: 'integer', nullable: false })
|
||||
version_document: number;
|
||||
|
||||
@CreateDateColumn({ name: 'accepte_le', type: 'timestamptz' })
|
||||
accepteLe: Date;
|
||||
|
||||
@Column({ type: 'inet', nullable: true })
|
||||
ip_address: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
user_agent: string | null;
|
||||
}
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Users } from './users.entity';
|
||||
|
||||
@Entity('documents_legaux')
|
||||
export class DocumentLegal {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
type: 'cgu' | 'privacy';
|
||||
|
||||
@Column({ type: 'integer', nullable: false })
|
||||
version: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
fichier_nom: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 500, nullable: false })
|
||||
fichier_path: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 64, nullable: false })
|
||||
fichier_hash: string;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
actif: boolean;
|
||||
|
||||
@ManyToOne(() => Users, { nullable: true })
|
||||
@JoinColumn({ name: 'televerse_par' })
|
||||
televersePar: Users | null;
|
||||
|
||||
@CreateDateColumn({ name: 'televerse_le', type: 'timestamptz' })
|
||||
televerseLe: Date;
|
||||
|
||||
@Column({ name: 'active_le', type: 'timestamptz', nullable: true })
|
||||
activeLe: Date | null;
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@ export class Users {
|
||||
@Column({ unique: true, name: 'email' })
|
||||
email: string;
|
||||
|
||||
@Column({ name: 'password', nullable: true })
|
||||
password?: string;
|
||||
@Column({ name: 'password' })
|
||||
password: string;
|
||||
|
||||
@Column({ name: 'prenom', nullable: true })
|
||||
prenom?: string;
|
||||
@ -96,6 +96,12 @@ export class Users {
|
||||
@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;
|
||||
|
||||
@ -111,12 +117,6 @@ export class Users {
|
||||
@Column({ default: false, name: 'changement_mdp_obligatoire' })
|
||||
changement_mdp_obligatoire: boolean;
|
||||
|
||||
@Column({ nullable: true, name: 'token_creation_mdp', length: 255 })
|
||||
token_creation_mdp?: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true, name: 'token_creation_mdp_expire_le' })
|
||||
token_creation_mdp_expire_le?: Date;
|
||||
|
||||
@Column({ nullable: true, name: 'ville' })
|
||||
ville?: string;
|
||||
|
||||
|
||||
@ -1,202 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Param,
|
||||
Body,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
Res,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
ParseUUIDPipe,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import { DocumentsLegauxService } from './documents-legaux.service';
|
||||
import { UploadDocumentDto } from './dto/upload-document.dto';
|
||||
import { DocumentsActifsResponseDto } from './dto/documents-actifs.dto';
|
||||
import { DocumentVersionDto } from './dto/document-version.dto';
|
||||
|
||||
@Controller('documents-legaux')
|
||||
export class DocumentsLegauxController {
|
||||
constructor(private readonly documentsService: DocumentsLegauxService) {}
|
||||
|
||||
/**
|
||||
* GET /api/v1/documents-legaux/actifs
|
||||
* Récupérer les documents actifs (CGU + Privacy)
|
||||
* PUBLIC
|
||||
*/
|
||||
@Get('actifs')
|
||||
async getDocumentsActifs(): Promise<DocumentsActifsResponseDto> {
|
||||
const { cgu, privacy } = await this.documentsService.getDocumentsActifs();
|
||||
|
||||
return {
|
||||
cgu: {
|
||||
id: cgu.id,
|
||||
type: cgu.type,
|
||||
version: cgu.version,
|
||||
url: `/api/v1/documents-legaux/${cgu.id}/download`,
|
||||
activeLe: cgu.activeLe,
|
||||
},
|
||||
privacy: {
|
||||
id: privacy.id,
|
||||
type: privacy.type,
|
||||
version: privacy.version,
|
||||
url: `/api/v1/documents-legaux/${privacy.id}/download`,
|
||||
activeLe: privacy.activeLe,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/documents-legaux/:type/versions
|
||||
* Lister toutes les versions d'un type de document
|
||||
* ADMIN ONLY
|
||||
*/
|
||||
@Get(':type/versions')
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('super_admin', 'administrateur')
|
||||
async listerVersions(@Param('type') type: string): Promise<DocumentVersionDto[]> {
|
||||
if (type !== 'cgu' && type !== 'privacy') {
|
||||
throw new BadRequestException('Le type doit être "cgu" ou "privacy"');
|
||||
}
|
||||
|
||||
const documents = await this.documentsService.listerVersions(type as 'cgu' | 'privacy');
|
||||
|
||||
return documents.map((doc) => ({
|
||||
id: doc.id,
|
||||
version: doc.version,
|
||||
fichier_nom: doc.fichier_nom,
|
||||
actif: doc.actif,
|
||||
televersePar: doc.televersePar
|
||||
? {
|
||||
id: doc.televersePar.id,
|
||||
prenom: doc.televersePar.prenom,
|
||||
nom: doc.televersePar.nom,
|
||||
}
|
||||
: null,
|
||||
televerseLe: doc.televerseLe,
|
||||
activeLe: doc.activeLe,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/documents-legaux
|
||||
* Upload une nouvelle version d'un document
|
||||
* ADMIN ONLY
|
||||
*/
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('super_admin', 'administrateur')
|
||||
async uploadDocument(
|
||||
@Body() uploadDto: UploadDocumentDto,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
// @CurrentUser() user: any, // TODO: Décommenter quand le guard sera implémenté
|
||||
) {
|
||||
if (!file) {
|
||||
throw new BadRequestException('Aucun fichier fourni');
|
||||
}
|
||||
|
||||
// TODO: Récupérer l'ID utilisateur depuis le guard
|
||||
const userId = '00000000-0000-0000-0000-000000000000'; // Temporaire
|
||||
|
||||
const document = await this.documentsService.uploadNouvelleVersion(
|
||||
uploadDto.type,
|
||||
file,
|
||||
userId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
version: document.version,
|
||||
fichier_nom: document.fichier_nom,
|
||||
actif: document.actif,
|
||||
televerseLe: document.televerseLe,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/documents-legaux/:id/activer
|
||||
* Activer une version d'un document
|
||||
* ADMIN ONLY
|
||||
*/
|
||||
@Patch(':id/activer')
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('super_admin', 'administrateur')
|
||||
async activerVersion(@Param('id', ParseUUIDPipe) documentId: string) {
|
||||
await this.documentsService.activerVersion(documentId);
|
||||
|
||||
// Récupérer le document pour retourner les infos
|
||||
const documents = await this.documentsService.listerVersions('cgu');
|
||||
const document = documents.find((d) => d.id === documentId);
|
||||
|
||||
if (!document) {
|
||||
const documentsPrivacy = await this.documentsService.listerVersions('privacy');
|
||||
const docPrivacy = documentsPrivacy.find((d) => d.id === documentId);
|
||||
|
||||
if (!docPrivacy) {
|
||||
throw new BadRequestException('Document non trouvé');
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Document activé avec succès',
|
||||
documentId: docPrivacy.id,
|
||||
type: docPrivacy.type,
|
||||
version: docPrivacy.version,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Document activé avec succès',
|
||||
documentId: document.id,
|
||||
type: document.type,
|
||||
version: document.version,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/documents-legaux/:id/download
|
||||
* Télécharger un document
|
||||
* PUBLIC
|
||||
*/
|
||||
@Get(':id/download')
|
||||
async telechargerDocument(
|
||||
@Param('id', ParseUUIDPipe) documentId: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { stream, filename } = await this.documentsService.telechargerDocument(documentId);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).send(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/documents-legaux/:id/verifier-integrite
|
||||
* Vérifier l'intégrité d'un document (hash SHA-256)
|
||||
* ADMIN ONLY
|
||||
*/
|
||||
@Get(':id/verifier-integrite')
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('super_admin', 'administrateur')
|
||||
async verifierIntegrite(@Param('id', ParseUUIDPipe) documentId: string) {
|
||||
const integre = await this.documentsService.verifierIntegrite(documentId);
|
||||
|
||||
return {
|
||||
documentId,
|
||||
integre,
|
||||
message: integre
|
||||
? 'Le document est intègre (hash valide)'
|
||||
: 'ALERTE : Le document a été modifié (hash invalide)',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DocumentLegal } from '../../entities/document-legal.entity';
|
||||
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
|
||||
import { DocumentsLegauxService } from './documents-legaux.service';
|
||||
import { DocumentsLegauxController } from './documents-legaux.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])],
|
||||
providers: [DocumentsLegauxService],
|
||||
controllers: [DocumentsLegauxController],
|
||||
exports: [DocumentsLegauxService],
|
||||
})
|
||||
export class DocumentsLegauxModule {}
|
||||
|
||||
@ -1,209 +0,0 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { DocumentLegal } from '../../entities/document-legal.entity';
|
||||
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentsLegauxService {
|
||||
private readonly UPLOAD_DIR = '/app/documents/legaux';
|
||||
|
||||
constructor(
|
||||
@InjectRepository(DocumentLegal)
|
||||
private docRepo: Repository<DocumentLegal>,
|
||||
@InjectRepository(AcceptationDocument)
|
||||
private acceptationRepo: Repository<AcceptationDocument>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Récupérer les documents actifs (CGU + Privacy)
|
||||
*/
|
||||
async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> {
|
||||
const cgu = await this.docRepo.findOne({
|
||||
where: { type: 'cgu', actif: true },
|
||||
});
|
||||
|
||||
const privacy = await this.docRepo.findOne({
|
||||
where: { type: 'privacy', actif: true },
|
||||
});
|
||||
|
||||
if (!cgu || !privacy) {
|
||||
throw new NotFoundException('Documents légaux manquants');
|
||||
}
|
||||
|
||||
return { cgu, privacy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploader une nouvelle version d'un document
|
||||
*/
|
||||
async uploadNouvelleVersion(
|
||||
type: 'cgu' | 'privacy',
|
||||
file: Express.Multer.File,
|
||||
userId: string,
|
||||
): Promise<DocumentLegal> {
|
||||
// Validation du type de fichier
|
||||
if (file.mimetype !== 'application/pdf') {
|
||||
throw new BadRequestException('Seuls les fichiers PDF sont acceptés');
|
||||
}
|
||||
|
||||
// Validation de la taille (max 10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
throw new BadRequestException('Le fichier ne doit pas dépasser 10MB');
|
||||
}
|
||||
|
||||
// 1. Calculer la prochaine version
|
||||
const lastDoc = await this.docRepo.findOne({
|
||||
where: { type },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
const nouvelleVersion = (lastDoc?.version || 0) + 1;
|
||||
|
||||
// 2. Calculer le hash SHA-256 du fichier
|
||||
const fileBuffer = file.buffer;
|
||||
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||
|
||||
// 3. Générer le nom de fichier unique
|
||||
const timestamp = Date.now();
|
||||
const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`;
|
||||
const filePath = path.join(this.UPLOAD_DIR, fileName);
|
||||
|
||||
// 4. Créer le répertoire si nécessaire et sauvegarder le fichier
|
||||
await fs.mkdir(this.UPLOAD_DIR, { recursive: true });
|
||||
await fs.writeFile(filePath, fileBuffer);
|
||||
|
||||
// 5. Créer l'entrée en BDD
|
||||
const document = this.docRepo.create({
|
||||
type,
|
||||
version: nouvelleVersion,
|
||||
fichier_nom: file.originalname,
|
||||
fichier_path: filePath,
|
||||
fichier_hash: hash,
|
||||
actif: false, // Pas actif par défaut
|
||||
televersePar: { id: userId } as any,
|
||||
televerseLe: new Date(),
|
||||
});
|
||||
|
||||
return await this.docRepo.save(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activer une version (désactive automatiquement l'ancienne)
|
||||
*/
|
||||
async activerVersion(documentId: string): Promise<void> {
|
||||
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundException('Document non trouvé');
|
||||
}
|
||||
|
||||
// Transaction : désactiver l'ancienne version, activer la nouvelle
|
||||
await this.docRepo.manager.transaction(async (manager) => {
|
||||
// Désactiver toutes les versions de ce type
|
||||
await manager.update(
|
||||
DocumentLegal,
|
||||
{ type: document.type, actif: true },
|
||||
{ actif: false },
|
||||
);
|
||||
|
||||
// Activer la nouvelle version
|
||||
await manager.update(
|
||||
DocumentLegal,
|
||||
{ id: documentId },
|
||||
{ actif: true, activeLe: new Date() },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lister toutes les versions d'un type de document
|
||||
*/
|
||||
async listerVersions(type: 'cgu' | 'privacy'): Promise<DocumentLegal[]> {
|
||||
return await this.docRepo.find({
|
||||
where: { type },
|
||||
order: { version: 'DESC' },
|
||||
relations: ['televersePar'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharger un document (retourne le buffer et le nom)
|
||||
*/
|
||||
async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> {
|
||||
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundException('Document non trouvé');
|
||||
}
|
||||
|
||||
try {
|
||||
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||
|
||||
return {
|
||||
stream: fileBuffer,
|
||||
filename: document.fichier_nom,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new NotFoundException('Fichier introuvable sur le système de fichiers');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier l'intégrité d'un document (hash SHA-256)
|
||||
*/
|
||||
async verifierIntegrite(documentId: string): Promise<boolean> {
|
||||
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundException('Document non trouvé');
|
||||
}
|
||||
|
||||
try {
|
||||
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||
|
||||
return hash === document.fichier_hash;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistrer une acceptation de document (lors de l'inscription)
|
||||
*/
|
||||
async enregistrerAcceptation(
|
||||
userId: string,
|
||||
documentId: string,
|
||||
typeDocument: 'cgu' | 'privacy',
|
||||
versionDocument: number,
|
||||
ipAddress: string | null,
|
||||
userAgent: string | null,
|
||||
): Promise<AcceptationDocument> {
|
||||
const acceptation = this.acceptationRepo.create({
|
||||
utilisateur: { id: userId } as any,
|
||||
document: { id: documentId } as any,
|
||||
type_document: typeDocument,
|
||||
version_document: versionDocument,
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent,
|
||||
});
|
||||
|
||||
return await this.acceptationRepo.save(acceptation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer l'historique des acceptations d'un utilisateur
|
||||
*/
|
||||
async getAcceptationsUtilisateur(userId: string): Promise<AcceptationDocument[]> {
|
||||
return await this.acceptationRepo.find({
|
||||
where: { utilisateur: { id: userId } },
|
||||
order: { accepteLe: 'DESC' },
|
||||
relations: ['document'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
export class DocumentVersionDto {
|
||||
id: string;
|
||||
version: number;
|
||||
fichier_nom: string;
|
||||
actif: boolean;
|
||||
televersePar: {
|
||||
id: string;
|
||||
prenom?: string;
|
||||
nom?: string;
|
||||
} | null;
|
||||
televerseLe: Date;
|
||||
activeLe: Date | null;
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
export class DocumentActifDto {
|
||||
id: string;
|
||||
type: 'cgu' | 'privacy';
|
||||
version: number;
|
||||
url: string;
|
||||
activeLe: Date | null;
|
||||
}
|
||||
|
||||
export class DocumentsActifsResponseDto {
|
||||
cgu: DocumentActifDto;
|
||||
privacy: DocumentActifDto;
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class UploadDocumentDto {
|
||||
@IsEnum(['cgu', 'privacy'], { message: 'Le type doit être "cgu" ou "privacy"' })
|
||||
@IsNotEmpty({ message: 'Le type est requis' })
|
||||
type: 'cgu' | 'privacy';
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export * from './documents-legaux.module';
|
||||
export * from './documents-legaux.service';
|
||||
|
||||
@ -3,8 +3,6 @@ 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 { 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';
|
||||
@ -32,34 +30,11 @@ export class AuthController {
|
||||
|
||||
@Public()
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: 'Inscription (OBSOLÈTE - utiliser /register/parent)' })
|
||||
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||
@ApiOperation({ summary: 'Inscription' })
|
||||
async register(@Body() dto: RegisterDto) {
|
||||
return this.authService.register(dto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('register/parent')
|
||||
@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 registerParentLegacy(@Body() dto: RegisterParentDto) {
|
||||
return this.authService.registerParent(dto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
@ApiBearerAuth('refresh_token')
|
||||
|
||||
@ -1,20 +1,13 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
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';
|
||||
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, Children]),
|
||||
forwardRef(() => UserModule),
|
||||
AppConfigModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
|
||||
@ -2,26 +2,14 @@ import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
} 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 * 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';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -29,13 +17,6 @@ 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>,
|
||||
@InjectRepository(Children)
|
||||
private readonly childrenRepo: Repository<Children>,
|
||||
) { }
|
||||
|
||||
/**
|
||||
@ -62,37 +43,29 @@ export class AuthService {
|
||||
* Connexion utilisateur
|
||||
*/
|
||||
async login(dto: LoginDto) {
|
||||
const user = await this.usersService.findByEmailOrNull(dto.email);
|
||||
try {
|
||||
const user = await this.usersService.findByEmailOrNull(dto.email);
|
||||
|
||||
if (!user) {
|
||||
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');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,8 +89,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription utilisateur OBSOLÈTE - Utiliser registerParent() ou registerAM()
|
||||
* @deprecated
|
||||
* Inscription utilisateur lambda (parent ou assistante maternelle)
|
||||
*/
|
||||
async register(registerDto: RegisterDto) {
|
||||
const exists = await this.usersService.findByEmailOrNull(registerDto.email);
|
||||
@ -157,305 +129,9 @@ 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
|
||||
*/
|
||||
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,
|
||||
token_creation_mdp: tokenCreationMdp,
|
||||
token_creation_mdp_expire_le: dateExpiration,
|
||||
});
|
||||
|
||||
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) {
|
||||
// Pour le moment envoyer un message clair
|
||||
return { success: true, message: 'Deconnexion'}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
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;
|
||||
|
||||
// ============================================
|
||||
// É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;
|
||||
}
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { SituationFamilialeType } from 'src/entities/users.entity';
|
||||
|
||||
export class RegisterParentDto {
|
||||
// === Informations obligatoires ===
|
||||
@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;
|
||||
|
||||
// === Informations optionnelles ===
|
||||
@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;
|
||||
|
||||
// === Informations 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: '0612345678', 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()
|
||||
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;
|
||||
}
|
||||
|
||||
@ -29,10 +29,10 @@ export class CreateEnfantsDto {
|
||||
@MaxLength(100)
|
||||
last_name?: string;
|
||||
|
||||
@ApiProperty({ enum: GenreType })
|
||||
@ApiProperty({ enum: GenreType, required: false })
|
||||
@IsOptional()
|
||||
@IsEnum(GenreType)
|
||||
@IsNotEmpty()
|
||||
gender: GenreType;
|
||||
gender?: GenreType;
|
||||
|
||||
@ApiProperty({ example: '2018-06-24', required: false })
|
||||
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
|
||||
|
||||
@ -8,13 +8,8 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
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';
|
||||
@ -33,34 +28,8 @@ export class EnfantsController {
|
||||
|
||||
@Roles(RoleType.PARENT)
|
||||
@Post()
|
||||
@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);
|
||||
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) {
|
||||
return this.enfantsService.create(dto, currentUser);
|
||||
}
|
||||
|
||||
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
|
||||
|
||||
@ -24,11 +24,10 @@ export class EnfantsService {
|
||||
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||
) { }
|
||||
|
||||
// Création d'un enfant
|
||||
async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise<Children> {
|
||||
// Création d’un enfant
|
||||
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> {
|
||||
const parent = await this.parentsRepository.findOne({
|
||||
where: { user_id: currentUser.id },
|
||||
relations: ['co_parent'],
|
||||
});
|
||||
if (!parent) throw new NotFoundException('Parent introuvable');
|
||||
|
||||
@ -47,34 +46,17 @@ 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 (Parent 1)
|
||||
// Lien parent-enfant
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -58,34 +58,32 @@ Ajouter un champ pour stocker la présentation du dossier parent (étape 4 de l'
|
||||
|
||||
---
|
||||
|
||||
### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe ✅
|
||||
### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `security`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
|
||||
**Labels** : `bdd`, `p0-bloquant`, `security`
|
||||
|
||||
**Description** :
|
||||
Ajouter les champs nécessaires pour gérer les tokens de création de mot de passe (workflow sans MDP lors inscription).
|
||||
|
||||
**Tâches** :
|
||||
- [x] Ajouter `password_reset_token` UUID dans `utilisateurs`
|
||||
- [x] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs`
|
||||
- [x] Créer migration Prisma
|
||||
- [x] Tester migration
|
||||
- [ ] Ajouter `password_reset_token` UUID dans `utilisateurs`
|
||||
- [ ] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs`
|
||||
- [ ] Créer migration Prisma
|
||||
- [ ] Tester migration
|
||||
|
||||
---
|
||||
|
||||
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants ✅
|
||||
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cdc`
|
||||
|
||||
**Description** :
|
||||
Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`.
|
||||
|
||||
**Tâches** :
|
||||
- [x] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants`
|
||||
- [x] Créer migration Prisma
|
||||
- [x] Tester migration
|
||||
- [ ] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants`
|
||||
- [ ] Créer migration Prisma
|
||||
- [ ] Tester migration
|
||||
|
||||
---
|
||||
|
||||
@ -124,10 +122,9 @@ Créer la table `configuration` pour stocker les paramètres système (SMTP, app
|
||||
|
||||
---
|
||||
|
||||
### Ticket #7 : [BDD] Tables documents légaux & acceptations ✅
|
||||
### Ticket #7 : [BDD] Tables documents légaux & acceptations
|
||||
**Estimation** : 2h
|
||||
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-30 - Ticket #68 sur Gitea)
|
||||
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
|
||||
|
||||
**Description** :
|
||||
Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) et tracer les acceptations utilisateurs.
|
||||
@ -337,13 +334,12 @@ Ajouter la gestion du co-parent (Parent 2) dans l'endpoint d'inscription.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #18 : [Backend] API Inscription Parent - REFONTE (Workflow complet 6 étapes) ✅
|
||||
### Ticket #18 : [Backend] API Inscription Parent (étape 3 - Enfants)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
|
||||
|
||||
**Description** :
|
||||
Refonte complète de l'API d'inscription parent pour gérer le workflow complet en 6 étapes dans une seule transaction.
|
||||
Créer l'endpoint pour ajouter des enfants lors de l'inscription parent.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Endpoint `POST /api/v1/enfants`
|
||||
@ -356,13 +352,12 @@ Refonte complète de l'API d'inscription parent pour gérer le workflow complet
|
||||
|
||||
---
|
||||
|
||||
### Ticket #19 : [Backend] API Inscription Parent (étape 2 - Parent 2) ✅
|
||||
### Ticket #19 : [Backend] API Inscription Parent (étape 4-6 - Finalisation)
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
|
||||
**Description** :
|
||||
Gestion du co-parent (Parent 2) dans l'endpoint d'inscription (intégré dans la refonte #18).
|
||||
Finaliser l'inscription parent (présentation, CGU, récapitulatif).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Enregistrement présentation dossier
|
||||
@ -372,13 +367,12 @@ Gestion du co-parent (Parent 2) dans l'endpoint d'inscription (intégré dans la
|
||||
|
||||
---
|
||||
|
||||
### Ticket #20 : [Backend] API Inscription Parent (étape 3 - Enfants) ✅
|
||||
### Ticket #20 : [Backend] API Inscription AM (panneau 1 - Identité)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
|
||||
|
||||
**Description** :
|
||||
Gestion des enfants dans l'endpoint d'inscription (intégré dans la refonte #18).
|
||||
Créer l'endpoint d'inscription Assistante Maternelle (panneau 1/5 : identité).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Endpoint `POST /api/v1/auth/register/am`
|
||||
@ -392,13 +386,12 @@ Gestion des enfants dans l'endpoint d'inscription (intégré dans la refonte #18
|
||||
|
||||
---
|
||||
|
||||
### Ticket #21 : [Backend] API Inscription Parent (étape 4-6 - Finalisation) ✅
|
||||
### Ticket #21 : [Backend] API Inscription AM (panneau 2 - Infos pro)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
|
||||
**Description** :
|
||||
Finalisation de l'inscription parent (présentation, CGU, récapitulatif - intégré dans la refonte #18).
|
||||
Ajouter les informations professionnelles de l'AM (panneau 2/5).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Validation NIR (15 chiffres obligatoire)
|
||||
@ -624,33 +617,22 @@ Créer l'écran de création de gestionnaire (super admin uniquement).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #34 : [Réservé - Non utilisé]
|
||||
|
||||
---
|
||||
|
||||
### Ticket #35 : [Réservé - Non utilisé]
|
||||
|
||||
---
|
||||
|
||||
### Ticket #36 : [Frontend] Inscription Parent - Étape 1 (Parent 1) ✅
|
||||
### Ticket #34 : [Frontend] Inscription Parent - Étape 1 (Parent 1)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (PR #73 mergée le 2025-12-01)
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
**Description** :
|
||||
Créer le formulaire d'inscription parent - étape 1/6 (informations Parent 1).
|
||||
|
||||
**Tâches** :
|
||||
- [x] Formulaire identité Parent 1
|
||||
- [x] Validation côté client
|
||||
- [x] Pas de champ mot de passe
|
||||
- [x] Navigation vers étape 2
|
||||
- [x] Améliorations visuelles (labels 22px, champs 20px, espacement 32px)
|
||||
- [x] Correction indicateur étape 1/6
|
||||
- [ ] Formulaire identité Parent 1
|
||||
- [ ] Validation côté client
|
||||
- [ ] Pas de champ mot de passe
|
||||
- [ ] Navigation vers étape 2
|
||||
|
||||
---
|
||||
|
||||
### Ticket #37 : [Frontend] Inscription Parent - Étape 2 (Parent 2)
|
||||
### Ticket #35 : [Frontend] Inscription Parent - Étape 2 (Parent 2)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
@ -662,13 +644,10 @@ Créer le formulaire d'inscription parent - étape 2/6 (informations Parent 2 op
|
||||
- [ ] Formulaire identité Parent 2 (conditionnel)
|
||||
- [ ] Checkbox "Même adresse"
|
||||
- [ ] Navigation vers étape 3
|
||||
- [ ] Pas de champ mot de passe
|
||||
- [ ] Améliorations visuelles (mêmes que Step1)
|
||||
- [ ] Correction indicateur étape 2/6
|
||||
|
||||
---
|
||||
|
||||
### Ticket #38 : [Frontend] Inscription Parent - Étape 3 (Enfants)
|
||||
### Ticket #36 : [Frontend] Inscription Parent - Étape 3 (Enfants)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
|
||||
|
||||
@ -685,7 +664,7 @@ Créer le formulaire d'inscription parent - étape 3/6 (informations enfants).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #39 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation)
|
||||
### Ticket #37 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
@ -702,7 +681,7 @@ Créer les étapes finales de l'inscription parent (présentation, CGU, récapit
|
||||
|
||||
---
|
||||
|
||||
### Ticket #40 : [Frontend] Inscription AM - Panneau 1 (Identité)
|
||||
### Ticket #38 : [Frontend] Inscription AM - Panneau 1 (Identité)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
|
||||
|
||||
@ -718,7 +697,7 @@ Créer le formulaire d'inscription AM - panneau 1/5 (identité).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #41 : [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
||||
### Ticket #39 : [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
@ -734,7 +713,7 @@ Créer le formulaire d'inscription AM - panneau 2/5 (informations professionnell
|
||||
|
||||
---
|
||||
|
||||
### Ticket #42 : [Frontend] Inscription AM - Finalisation
|
||||
### Ticket #40 : [Frontend] Inscription AM - Finalisation
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
@ -750,7 +729,7 @@ Créer les étapes finales de l'inscription AM (présentation, CGU, récapitulat
|
||||
|
||||
---
|
||||
|
||||
### Ticket #43 : [Frontend] Écran Création Mot de Passe
|
||||
### Ticket #41 : [Frontend] Écran Création Mot de Passe
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`
|
||||
|
||||
@ -767,7 +746,7 @@ Créer l'écran de création de mot de passe (lien reçu par email).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #44 : [Frontend] Dashboard Gestionnaire - Structure
|
||||
### Ticket #42 : [Frontend] Dashboard Gestionnaire - Structure
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `gestionnaire`
|
||||
|
||||
@ -781,7 +760,7 @@ Créer la structure du dashboard gestionnaire avec 2 onglets.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #45 : [Frontend] Dashboard Gestionnaire - Liste Parents
|
||||
### Ticket #43 : [Frontend] Dashboard Gestionnaire - Liste Parents
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `gestionnaire`
|
||||
|
||||
@ -797,7 +776,7 @@ Créer la liste des parents en attente de validation.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #46 : [Frontend] Dashboard Gestionnaire - Liste AM
|
||||
### Ticket #44 : [Frontend] Dashboard Gestionnaire - Liste AM
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `gestionnaire`
|
||||
|
||||
@ -814,7 +793,7 @@ Créer la liste des assistantes maternelles en attente de validation.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #47 : [Frontend] Écran Changement MDP Obligatoire
|
||||
### Ticket #45 : [Frontend] Écran Changement MDP Obligatoire
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `security`
|
||||
|
||||
@ -830,7 +809,7 @@ Créer l'écran de changement de mot de passe obligatoire (première connexion g
|
||||
|
||||
---
|
||||
|
||||
### Ticket #48 : [Frontend] Gestion Erreurs & Messages
|
||||
### Ticket #46 : [Frontend] Gestion Erreurs & Messages
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `ux`
|
||||
|
||||
@ -844,7 +823,7 @@ Créer un système de gestion des erreurs et messages utilisateur.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #49 : [Frontend] Écran Gestion Documents Légaux (Admin)
|
||||
### Ticket #47 : [Frontend] Écran Gestion Documents Légaux (Admin)
|
||||
**Estimation** : 5h
|
||||
**Labels** : `frontend`, `p3`, `juridique`, `admin`
|
||||
|
||||
@ -863,7 +842,7 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #50 : [Frontend] Affichage dynamique CGU lors inscription
|
||||
### Ticket #48 : [Frontend] Affichage dynamique CGU lors inscription
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `juridique`
|
||||
|
||||
@ -877,24 +856,9 @@ Afficher dynamiquement les CGU/Privacy lors de l'inscription (avec numéro de ve
|
||||
|
||||
---
|
||||
|
||||
### Ticket #51 : [Frontend] Écran Logs Admin (optionnel v1.1)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `admin`, `logs`
|
||||
|
||||
**Description** :
|
||||
Créer l'écran de consultation des logs système (optionnel pour v1.1).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Appel API logs
|
||||
- [ ] Filtres (date, niveau, utilisateur)
|
||||
- [ ] Pagination
|
||||
- [ ] Export CSV
|
||||
|
||||
---
|
||||
|
||||
## 🔵 PRIORITÉ 4 : Tests & Documentation
|
||||
|
||||
### Ticket #52 : [Tests] Tests unitaires Backend
|
||||
### Ticket #49 : [Tests] Tests unitaires Backend
|
||||
**Estimation** : 8h
|
||||
**Labels** : `tests`, `p4`, `backend`
|
||||
|
||||
|
||||
@ -1,344 +0,0 @@
|
||||
# 📐 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
|
||||
|
||||
@ -22,9 +22,11 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _addressController = TextEditingController(); // Rue seule
|
||||
final _postalCodeController = TextEditingController(); // Restauré
|
||||
final _cityController = TextEditingController(); // Restauré
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -46,6 +48,8 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
_lastNameController.text = genLastName;
|
||||
_phoneController.text = DataGenerator.phone();
|
||||
_emailController.text = DataGenerator.email(genFirstName, genLastName);
|
||||
_passwordController.text = DataGenerator.password();
|
||||
_confirmPasswordController.text = _passwordController.text;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -54,6 +58,8 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
@ -82,9 +88,9 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicateur d'étape
|
||||
// Indicateur d'étape (à rendre dynamique)
|
||||
Text(
|
||||
'Étape 1/6',
|
||||
'Étape 1/5',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@ -115,43 +121,54 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Mot de passe requis';
|
||||
if (value.length < 6) return '6 caractères minimum';
|
||||
return null;
|
||||
})),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Confirmation requise';
|
||||
if (value != _passwordController.text) return 'Ne correspond pas';
|
||||
return null;
|
||||
})),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomAppTextField(
|
||||
controller: _addressController,
|
||||
labelText: 'Adresse (N° et Rue)',
|
||||
hintText: 'Numéro et nom de votre rue',
|
||||
style: CustomAppTextFieldStyle.beige,
|
||||
fieldWidth: double.infinity,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -188,12 +205,12 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
ParentData(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
address: _addressController.text,
|
||||
postalCode: _postalCodeController.text,
|
||||
city: _cityController.text,
|
||||
address: _addressController.text, // Rue
|
||||
postalCode: _postalCodeController.text, // Ajout
|
||||
city: _cityController.text, // Ajout
|
||||
phone: _phoneController.text,
|
||||
email: _emailController.text,
|
||||
password: '', // Pas de mot de passe à cette étape
|
||||
password: _passwordController.text,
|
||||
)
|
||||
);
|
||||
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
|
||||
|
||||
@ -22,14 +22,16 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
|
||||
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
|
||||
|
||||
// Contrôleurs pour les champs du parent 2
|
||||
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _addressController = TextEditingController(); // Rue seule
|
||||
final _postalCodeController = TextEditingController(); // Restauré
|
||||
final _cityController = TextEditingController(); // Restauré
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -47,13 +49,17 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
_lastNameController.text = genLastName;
|
||||
_phoneController.text = DataGenerator.phone();
|
||||
_emailController.text = DataGenerator.email(genFirstName, genLastName);
|
||||
_passwordController.text = DataGenerator.password();
|
||||
_confirmPasswordController.text = _passwordController.text;
|
||||
|
||||
_sameAddressAsParent1 = DataGenerator.boolean();
|
||||
if (!_sameAddressAsParent1) {
|
||||
// Générer adresse, CP, Ville séparément
|
||||
_addressController.text = DataGenerator.address();
|
||||
_postalCodeController.text = DataGenerator.postalCode();
|
||||
_cityController.text = DataGenerator.city();
|
||||
} else {
|
||||
// Vider les champs si même adresse (seront désactivés)
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
@ -66,6 +72,8 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
@ -90,7 +98,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Étape 2/6', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Informations du Deuxième Parent (Optionnel)',
|
||||
@ -109,9 +117,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -150,33 +156,40 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
]),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 25),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)),
|
||||
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -212,7 +225,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
|
||||
phone: _phoneController.text,
|
||||
email: _emailController.text,
|
||||
password: '', // Pas de mot de passe à cette étape
|
||||
password: _passwordController.text,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
@ -231,10 +244,8 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
|
||||
void _clearParent2Fields() {
|
||||
_formKey.currentState?.reset();
|
||||
_lastNameController.clear();
|
||||
_firstNameController.clear();
|
||||
_phoneController.clear();
|
||||
_emailController.clear();
|
||||
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
|
||||
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user