import { BadRequestException, Body, Controller, Post, UploadedFile, UploadedFiles, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; import { UseModule } from '../module-registry/module.guard'; import type { UploadedFileLike } from '../auth/types/auth-user'; import { analyzeBundle, type BundleExportFormat, type BundleItemKind, exportBundleItem, } from './cert-bundle'; import { CertManagerService } from './cert-manager.service'; /** * CertManagerController — 4 POST endpoints for certificate operations. * * All routes are protected by: * - Global JwtAuthGuard (authentication) * - Global TenantGuard (tenant context) * - @UseModule('cert-manager') ModuleGuard (module activation check) * * File size limit: 5 MB per file (T-09-03 — DoS mitigation). * Password parameter is never passed to a logger (T-09-02 — InfoDisc mitigation). */ @Controller('modules/cert-manager') @UseModule('cert-manager') export class CertManagerController { constructor(private readonly certManagerService: CertManagerService) {} /** * POST /modules/cert-manager/parse * Inspect a single certificate: subject, issuer, validity, SANs, fingerprints. * Accepts multipart file upload OR JSON body with pemText. */ @Post('parse') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, }), ) async parseCert( @UploadedFile() file: UploadedFileLike | undefined, @Body('password') password?: string, @Body('pemText') pemText?: string, ) { if (!file && !pemText) { throw new BadRequestException('No file or PEM text provided'); } return this.certManagerService.parseCert({ file, pemText, password }); } /** * POST /modules/cert-manager/split * Split a fullchain.pem or P7B bundle into individual certificates. */ @Post('split') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, }), ) async splitCerts( @UploadedFile() file: UploadedFileLike | undefined, @Body('password') password?: string, ) { if (!file) { throw new BadRequestException('No file provided'); } return this.certManagerService.splitCerts({ file, password }); } /** * POST /modules/cert-manager/merge * Merge multiple certificates into a PEM chain or PFX bundle. * Uses FilesInterceptor (plural) to accept multiple files with field name "files". */ @Post('merge') @UseInterceptors( FilesInterceptor('files', 20, { limits: { fileSize: 5 * 1024 * 1024 }, }), ) async mergeCerts( @UploadedFiles() files: UploadedFileLike[], @Body('outputFormat') outputFormat: string, @Body('password') password?: string, ) { if (!files || files.length < 2) { throw new BadRequestException('At least 2 files required for merge'); } return this.certManagerService.mergeCerts({ files, outputFormat, password }); } /** * POST /modules/cert-manager/convert * Convert a certificate between PEM, DER, and P7B formats. * Accepts a multipart file upload OR a pemText body field. * * T-09-03: fileSize limit 5 MB (DoS mitigation) * T-09-04: global JwtAuthGuard + @UseModule('cert-manager') ModuleGuard * T-09-02: password is never passed to the logger */ @Post('convert') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, }), ) async convertCert( @UploadedFile() file: UploadedFileLike | undefined, @Body('targetFormat') targetFormat: string, @Body('password') password?: string, @Body('pemText') pemText?: string, ) { if (!file && !pemText) { throw new BadRequestException('No file or PEM text provided'); } return this.certManagerService.convertCert({ file, pemText, targetFormat, password }); } /** * POST /modules/cert-manager/analyze (quick-261001-l4q) * Zertifikatspaket: mehrere Dateien oder ZIP hochladen, jedes Teil erkennen * (Server-/Zwischen-/Stammzertifikat, privater Schluessel, CSR), Duplikate * zusammenfassen, Schluessel und Kette zuordnen. * * T-09-03: 20 Dateien, je 5 MB; ZIP-Inhalt zusaetzlich begrenzt (cert-bundle.ts). * T-09-02: password is never passed to the logger */ @Post('analyze') @UseInterceptors( FilesInterceptor('files', 20, { limits: { fileSize: 5 * 1024 * 1024 }, }), ) async analyze( @UploadedFiles() files: UploadedFileLike[] | undefined, @Body('password') password?: string, ) { return analyzeBundle(files ?? [], password ?? ''); } /** * POST /modules/cert-manager/export (quick-261001-l4q) * Ein Teil aus `analyze` (PEM) in das gewuenschte Format bringen. * JSON-Body; PFX verlangt ein Passwort fuer die neue Datei. */ @Post('export') async export( @Body('kind') kind: BundleItemKind, @Body('pem') pem: string, @Body('format') format: BundleExportFormat, @Body('baseName') baseName?: string, @Body('chain') chain?: string[], @Body('keyPem') keyPem?: string, @Body('password') password?: string, ) { if ( chain !== undefined && (!Array.isArray(chain) || chain.some((c) => typeof c !== 'string')) ) { throw new BadRequestException('chain must be a list of PEM strings'); } return exportBundleItem({ kind, pem, format, baseName, chain, keyPem, password }); } }