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:
@@ -29,7 +29,8 @@
|
|||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"class-transformer": "^0.5.0",
|
"class-transformer": "^0.5.0",
|
||||||
"@asyncapi/cli": "6.0.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/generator": "3.0.1",
|
||||||
"@asyncapi/html-template": "3.5.6",
|
"@asyncapi/html-template": "3.5.6",
|
||||||
"@redocly/cli": "latest"
|
"@redocly/cli": "latest"
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
|
|||||||
@@ -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';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
export class AllExceptionsFilter implements ExceptionFilter {
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger('ExceptionFilter');
|
||||||
|
|
||||||
catch(exception: unknown, host: ArgumentsHost) {
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
@@ -18,6 +20,14 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|||||||
? exception.getResponse()
|
? exception.getResponse()
|
||||||
: 'Internal server error';
|
: '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({
|
response.status(status).json({
|
||||||
statusCode: status,
|
statusCode: status,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
UploadedFiles,
|
UploadedFiles,
|
||||||
Body,
|
Body,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
@@ -12,6 +13,7 @@ import * as fs from 'fs';
|
|||||||
import * as fsPromises from 'fs/promises';
|
import * as fsPromises from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
@@ -30,6 +32,8 @@ import { ApiType } from '@datacat/shared';
|
|||||||
|
|
||||||
@Controller('uploads')
|
@Controller('uploads')
|
||||||
export class UploadsController {
|
export class UploadsController {
|
||||||
|
private readonly logger = new Logger(UploadsController.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly apisService: ApisService,
|
private readonly apisService: ApisService,
|
||||||
private readonly docsService: DocsGenerationService,
|
private readonly docsService: DocsGenerationService,
|
||||||
@@ -78,13 +82,19 @@ export class UploadsController {
|
|||||||
let detectedType: ApiType | null = null;
|
let detectedType: ApiType | null = null;
|
||||||
|
|
||||||
if (files.length === 1) {
|
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');
|
yamlContent = fs.readFileSync(files[0].path, 'utf-8');
|
||||||
|
|
||||||
|
/* Validation syntaxique YAML */
|
||||||
|
this.validateYamlSyntax(yamlContent, files[0].originalname);
|
||||||
|
|
||||||
detectedType = this.detectTypeFromContent(yamlContent);
|
detectedType = this.detectTypeFromContent(yamlContent);
|
||||||
} else {
|
} else {
|
||||||
/* Cas multi-fichiers : détecter le fichier principal et bundler */
|
/* Cas multi-fichiers : détecter le fichier principal et bundler */
|
||||||
const { mainFile, type } = this.detectMainFile(files);
|
const { mainFile, type } = this.detectMainFile(files);
|
||||||
detectedType = type;
|
detectedType = type;
|
||||||
|
|
||||||
|
this.logger.log(`Multi-file upload: main=${mainFile.originalname}, type=${type}, total=${files.length} files`);
|
||||||
yamlContent = await this.bundleFiles(files, mainFile, type);
|
yamlContent = await this.bundleFiles(files, mainFile, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +125,21 @@ export class UploadsController {
|
|||||||
return entry;
|
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 {
|
private detectTypeFromContent(content: string): ApiType | null {
|
||||||
/* Cherche la clé racine openapi: ou asyncapi: sans dépendance à js-yaml */
|
/* Cherche la clé racine openapi: ou asyncapi: sans dépendance à js-yaml */
|
||||||
if (/^openapi\s*:/m.test(content)) return 'OPENAPI';
|
if (/^openapi\s*:/m.test(content)) return 'OPENAPI';
|
||||||
|
|||||||
@@ -65,7 +65,8 @@
|
|||||||
},
|
},
|
||||||
"defaultConfiguration": "development",
|
"defaultConfiguration": "development",
|
||||||
"options": {
|
"options": {
|
||||||
"proxyConfig": "proxy.conf.json"
|
"proxyConfig": "proxy.conf.json",
|
||||||
|
"allowedHosts": ["datacat.dev.chmod777.dev"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11498
pnpm-lock.yaml
generated
11498
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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'))"
|
ok "asyncapi CLI déjà installé ($(asyncapi --version 2>/dev/null || echo 'version inconnue'))"
|
||||||
else
|
else
|
||||||
info "Installation de @asyncapi/cli@6.0.0..."
|
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é"
|
ok "asyncapi CLI installé"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user