import { Body, Controller, HttpCode, Post, UploadedFiles, UseInterceptors } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import type { UploadedFileLike } from '../auth/types/auth-user'; import { UseModule } from '../module-registry/module.guard'; import { analyzeWorkingSet } from './cert-analyze'; import { buildOutput } from './cert-output'; import { type AnalysisResult, type BuildResult, certError } from './cert-types'; import { BuildOutputDto } from './dto/cert-build.dto'; /** Obergrenzen (D-17): je Datei 5 MiB, alle Dateien zusammen 20 MiB, hoechstens 30 Dateien. */ export const CERT_MAX_FILES = 30; export const CERT_MAX_FILE_BYTES = 5 * 1024 * 1024; export const CERT_MAX_TOTAL_BYTES = 20 * 1024 * 1024; /** Das Feld `passwords`: ein JSON-Array von Zeichenketten, je Datei eines (Task 4). */ export const CERT_MAX_PASSWORDS = 30; export const CERT_MAX_PASSWORD_CHARS = 1024; /** * Liest das multipart-Feld `passwords`. Fehlt es, gibt es keine Passwoerter. Alles andere als ein * JSON-Array aus hoechstens 30 Zeichenketten zu je hoechstens 1024 Zeichen: 400 invalidInput. * Der Inhalt erscheint nie in einer Fehlermeldung. */ export function parsePasswords(raw: unknown): string[] { if (raw === undefined || raw === null || raw === '') return []; let value: unknown = raw; if (typeof raw === 'string') { try { value = JSON.parse(raw); } catch { certError('invalidInput', 400, 'passwords must be a JSON array of strings'); } } if ( !Array.isArray(value) || value.length > CERT_MAX_PASSWORDS || value.some((p) => typeof p !== 'string' || p.length > CERT_MAX_PASSWORD_CHARS) ) { certError('invalidInput', 400, 'passwords must be a JSON array of strings'); } return value as string[]; } /** * multer liest Dateinamen als Latin-1. Waren es UTF-8-Bytes (Umlaute), den Namen zurueckgewinnen; * ist das Ergebnis kein gueltiges UTF-8, bleibt der Name wie er ist. */ export function repairFileName(name: string): string { const utf8 = Buffer.from(name, 'latin1').toString('utf8'); return utf8.includes('�') ? name : utf8; } /** * CertManagerController: Zertifikat-Manager (quick-261009-ikt, D-14). * * Zustandslos: nichts wird gespeichert, Passwoerter und Schluessel kommen nie in ein Log * (kein Logger-Aufruf mit Anfrageinhalt; das Request-Log kennt nur Pfad und Status). * Geschuetzt durch den globalen JwtAuthGuard, den TenantGuard und @UseModule('cert-manager'). * * Routen (alle POST, 200): * - analyze Task 1 mehrere Dateien (multipart) erkennen und zusammenfassen; Task 4: optionales Feld `passwords` * - build Task 2 Ausgabe bauen (JSON, eigene Grenze 512 KiB, siehe cert-json-body.ts), ab Task 5/6 erweitert * - fetch-issuer Task 7 fehlendes Zwischenzertifikat nur auf Knopfdruck holen */ @Controller('modules/cert-manager') @UseModule('cert-manager') export class CertManagerController { @Post('analyze') @HttpCode(200) @UseInterceptors( FilesInterceptor('files', CERT_MAX_FILES, { limits: { fileSize: CERT_MAX_FILE_BYTES } }), ) analyze( @UploadedFiles() files: UploadedFileLike[] | undefined, @Body('passwords') rawPasswords?: unknown, ): AnalysisResult { if (!files || files.length === 0) { certError('invalidInput', 400, 'No files provided'); } const total = files.reduce((sum, f) => sum + f.buffer.length, 0); if (total > CERT_MAX_TOTAL_BYTES) { certError('tooLarge', 413, 'Files together exceed 20 MiB'); } const passwords = parsePasswords(rawPasswords); return analyzeWorkingSet( files.map((f) => ({ originalname: repairFileName(f.originalname), buffer: f.buffer })), passwords, ); } @Post('build') @HttpCode(200) build(@Body() dto: BuildOutputDto): BuildResult { return buildOutput(dto); } }