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:
parent
077a1ca3ed
commit
2b3128ce4a
@ -22,8 +22,13 @@ JWT_EXPIRATION_TIME=7d
|
||||
# Environnement
|
||||
NODE_ENV=development
|
||||
|
||||
# Photos (inscription). Defaut : ./uploads/photos sous le backend. Docker : UPLOAD_PHOTOS_DIR=/app/uploads/photos
|
||||
# Photos inscription (fichiers écrits depuis base64). Préférer un chemin ABSOLU.
|
||||
# Local : laisser vide → ./uploads/photos (relatif au cwd du processus).
|
||||
# Docker (docker-compose) : UPLOAD_PHOTOS_DIR=/app/uploads/photos + volume nommé (voir docker-compose.yml).
|
||||
# UPLOAD_PHOTOS_DIR=
|
||||
#
|
||||
# Reverse proxy : si Nginx devant l’API, augmenter la taille du corps (ex. client_max_body_size 16m;).
|
||||
# Traefik en reverse proxy simple ne limite en général pas le corps ; si middleware buffering, prévoir ~16 Mo+.
|
||||
|
||||
# Log de chaque appel API (mode debug) — mettre à true pour tracer les requêtes front
|
||||
# LOG_API_REQUESTS=true
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { AppModule } from './app.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
|
||||
@ -7,8 +8,13 @@ import { ValidationPipe } from '@nestjs/common';
|
||||
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule,
|
||||
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
});
|
||||
|
||||
// Photos inscription (base64) : limite Express par défaut ~100 ko → 413/500 sans ceci
|
||||
app.useBodyParser('json', { limit: '15mb' });
|
||||
app.useBodyParser('urlencoded', { extended: true, limit: '15mb' });
|
||||
|
||||
// Log de chaque appel API si LOG_API_REQUESTS=true (mode debug)
|
||||
app.useGlobalInterceptors(new LogRequestInterceptor());
|
||||
|
||||
@ -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 ?? '';
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,10 @@ services:
|
||||
NODE_ENV: ${NODE_ENV}
|
||||
LOG_API_REQUESTS: ${LOG_API_REQUESTS:-false}
|
||||
CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY}
|
||||
# Photos inscription (base64) — chemin absolu dans le conteneur (voir volume backend_uploads)
|
||||
UPLOAD_PHOTOS_DIR: ${UPLOAD_PHOTOS_DIR:-/app/uploads/photos}
|
||||
volumes:
|
||||
- backend_uploads:/app/uploads/photos
|
||||
depends_on:
|
||||
- database
|
||||
labels:
|
||||
@ -94,6 +98,7 @@ services:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
backend_uploads:
|
||||
|
||||
networks:
|
||||
ptitspas_network:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user