fix(uploads): validation YAML + logging erreurs 5xx détaillé

- Validation syntaxique js-yaml avant import (BadRequestException 400 avec message précis)
- Logger dans UploadsController : log du fichier principal détecté pour multi-upload
- AllExceptionsFilter : log complet avec stack trace pour toutes les erreurs 5xx
- @asyncapi/bundler version corrigée (0.9.0 → ^1.0.1, version inexistante)
- @types/js-yaml ajouté en devDependencies

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
z3n
2026-06-08 12:50:50 +00:00
parent b975e01e15
commit a0677df5ec
6 changed files with 11453 additions and 93 deletions

View File

@@ -29,7 +29,8 @@
"class-validator": "^0.14.0",
"class-transformer": "^0.5.0",
"@asyncapi/cli": "6.0.0",
"@asyncapi/bundler": "0.9.0",
"@asyncapi/bundler": "^1.0.1",
"js-yaml": "^4.1.0",
"@asyncapi/generator": "3.0.1",
"@asyncapi/html-template": "3.5.6",
"@redocly/cli": "latest"
@@ -40,6 +41,7 @@
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.0.0",
"@types/js-yaml": "^4.0.9",
"@types/uuid": "^10.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"

View File

@@ -1,8 +1,10 @@
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
@@ -18,6 +20,14 @@ export class AllExceptionsFilter implements ExceptionFilter {
? exception.getResponse()
: 'Internal server error';
/* Logguer toutes les erreurs 5xx avec stack trace complète */
if (status >= 500) {
this.logger.error(
`[${request.method} ${request.url}] ${String(exception)}`,
exception instanceof Error ? exception.stack : undefined,
);
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),

View File

@@ -5,6 +5,7 @@ import {
UploadedFiles,
Body,
BadRequestException,
Logger,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
@@ -12,6 +13,7 @@ import * as fs from 'fs';
import * as fsPromises from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import * as yaml from 'js-yaml';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { randomUUID } from 'crypto';
@@ -30,6 +32,8 @@ import { ApiType } from '@datacat/shared';
@Controller('uploads')
export class UploadsController {
private readonly logger = new Logger(UploadsController.name);
constructor(
private readonly apisService: ApisService,
private readonly docsService: DocsGenerationService,
@@ -78,13 +82,19 @@ export class UploadsController {
let detectedType: ApiType | null = null;
if (files.length === 1) {
/* Cas simple : un seul fichier, comportement identique à l'ancien */
/* Cas simple : un seul fichier */
yamlContent = fs.readFileSync(files[0].path, 'utf-8');
/* Validation syntaxique YAML */
this.validateYamlSyntax(yamlContent, files[0].originalname);
detectedType = this.detectTypeFromContent(yamlContent);
} else {
/* Cas multi-fichiers : détecter le fichier principal et bundler */
const { mainFile, type } = this.detectMainFile(files);
detectedType = type;
this.logger.log(`Multi-file upload: main=${mainFile.originalname}, type=${type}, total=${files.length} files`);
yamlContent = await this.bundleFiles(files, mainFile, type);
}
@@ -115,6 +125,21 @@ export class UploadsController {
return entry;
}
/** Valide la syntaxe YAML et lève une BadRequestException si invalide */
private validateYamlSyntax(content: string, filename: string): void {
try {
const parsed = yaml.load(content);
if (parsed === null || typeof parsed !== 'object') {
throw new BadRequestException(`${filename}: le fichier YAML est vide ou invalide`);
}
} catch (err) {
if (err instanceof BadRequestException) throw err;
const msg = err instanceof Error ? err.message : String(err);
this.logger.warn(`Validation YAML échouée pour ${filename}: ${msg}`);
throw new BadRequestException(`${filename}: erreur de syntaxe YAML — ${msg}`);
}
}
private detectTypeFromContent(content: string): ApiType | null {
/* Cherche la clé racine openapi: ou asyncapi: sans dépendance à js-yaml */
if (/^openapi\s*:/m.test(content)) return 'OPENAPI';

View File

@@ -65,7 +65,8 @@
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "proxy.conf.json"
"proxyConfig": "proxy.conf.json",
"allowedHosts": ["datacat.dev.chmod777.dev"]
}
}
}

11498
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,7 @@ if command -v asyncapi &>/dev/null; then
ok "asyncapi CLI déjà installé ($(asyncapi --version 2>/dev/null || echo 'version inconnue'))"
else
info "Installation de @asyncapi/cli@6.0.0..."
npm install -g @asyncapi/cli@6.0.0
npm install -g @asyncapi/cli@6.0.0 --legacy-peer-deps
ok "asyncapi CLI installé"
fi