Files
tessera-ctl/apps/api/src/cert-manager/cert-manager.controller.ts
T
schalli c07b0cfaf0 feat(cert-manager): Zertifikatspaket hochladen, erkennen und in jedem Format herunterladen
Neuer erster Reiter Übersicht: mehrere Dateien oder die ZIP vom Aussteller
auf einmal ablegen. Der Server erkennt jedes Teil (Server-, Zwischen-,
Stammzertifikat, privater Schlüssel, CSR, auch aus PFX/P7B), fasst
Duplikate zusammen, ordnet Schlüssel/CSR dem Zertifikat zu und baut die
Kette. Unter jedem Teil stehen Download-Knöpfe für alle passenden Formate
(crt, cer, Fullchain, p7b, pfx mit Schlüssel und Kette; key PKCS#8/PKCS#1/
DER; csr PEM/DER). Geschützte PFX lassen sich mit Passwort entsperren.

Neue Endpunkte POST analyze (Dateien/ZIP, Begrenzung Anzahl und Größe vor
dem Entpacken) und POST export (JSON, zustandslos). Modultexte siezen jetzt
durchgehend; Gültigkeit in UTC wie im Zertifikat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 15:23:34 +02:00

174 lines
5.3 KiB
TypeScript

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 });
}
}