fix(api): inscription parent — body 15mb, uploads persistants, 409 si unique PG

- NestExpressApplication + useBodyParser json/urlencoded 15mb
- UPLOAD_PHOTOS_DIR: résolution chemin absolu/relatif + doc .env.example / proxy
- docker-compose: volume backend_uploads + UPLOAD_PHOTOS_DIR par défaut
- Map erreur PostgreSQL 23505 → ConflictException (parent + AM)
- Logs Nest si échec SMTP après inscription (email déjà géré)

Made-with: Cursor
This commit is contained in:
2026-04-10 17:40:23 +02:00
parent 077a1ca3ed
commit 2b3128ce4a
4 changed files with 70 additions and 11 deletions
+51 -8
View File
@@ -1,12 +1,13 @@
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { QueryFailedError, Repository } from 'typeorm';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
@@ -32,6 +33,8 @@ import { MailService } from 'src/modules/mail/mail.service';
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private readonly usersService: UserService,
private readonly jwtService: JwtService,
@@ -213,7 +216,16 @@ export class AuthService {
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
let resultat: {
parent1: Users;
parent2: Users | null;
enfants: Children[];
tokenCreationMdp: string;
tokenCoParent: string | null;
};
try {
resultat = await this.usersRepo.manager.transaction(async (manager) => {
const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);
const parent1 = manager.create(Users, {
@@ -331,6 +343,14 @@ export class AuthService {
tokenCoParent,
};
});
} catch (err) {
if (this.isPostgresUniqueViolation(err)) {
throw new ConflictException(
'Un compte avec cet email existe déjà (contrainte unique en base).',
);
}
throw err;
}
const numeroDossier = resultat.parent1.numero_dossier ?? '';
@@ -341,7 +361,7 @@ export class AuthService {
resultat.parent1.nom ?? '',
numeroDossier,
);
if (resultat.parent2) {
if (resultat.parent2) {
await this.mailService.sendParentRegistrationPendingEmail(
resultat.parent2.email,
resultat.parent2.prenom ?? '',
@@ -350,7 +370,10 @@ export class AuthService {
);
}
} catch (err) {
console.error("[inscrireParentComplet] Échec envoi email de confirmation d'inscription", err);
this.logger.error(
"[inscrireParentComplet] Échec envoi email d'accusé de réception (inscription conservée)",
err instanceof Error ? err.stack : String(err),
);
}
return {
@@ -428,7 +451,9 @@ export class AuthService {
const dateConsentementPhoto =
dto.consentement_photo ? new Date() : undefined;
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
let resultat: { user: Users };
try {
resultat = await this.usersRepo.manager.transaction(async (manager) => {
const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);
const user = manager.create(Users, {
@@ -467,6 +492,12 @@ export class AuthService {
return { user: userEnregistre };
});
} catch (err) {
if (this.isPostgresUniqueViolation(err)) {
throw new ConflictException('Un compte avec cet email existe déjà (contrainte unique en base).');
}
throw err;
}
return {
message:
@@ -488,9 +519,12 @@ export class AuthService {
const extension = correspondances[1];
const tamponImage = Buffer.from(correspondances[2], 'base64');
const dossierUpload =
(process.env.UPLOAD_PHOTOS_DIR && process.env.UPLOAD_PHOTOS_DIR.trim()) ||
path.join(process.cwd(), 'uploads', 'photos');
const rawDir = process.env.UPLOAD_PHOTOS_DIR?.trim();
const dossierUpload = rawDir
? path.isAbsolute(rawDir)
? rawDir
: path.resolve(process.cwd(), rawDir)
: path.join(process.cwd(), 'uploads', 'photos');
await fs.mkdir(dossierUpload, { recursive: true });
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
@@ -601,4 +635,13 @@ export class AuthService {
token: user.token_reprise,
};
}
/** Violation unique PostgreSQL (ex. email déjà présent malgré course entre requêtes). */
private isPostgresUniqueViolation(err: unknown): boolean {
if (!(err instanceof QueryFailedError)) {
return false;
}
const code = (err.driverError as { code?: string } | undefined)?.code;
return code === '23505';
}
}