feat(09-01): scaffold cert-manager module + shared node-forge helpers (GREEN)

- cert-manager.module.ts: OnModuleInit + seedCertManagerModule (CERT-06)
- cert-manager.seed.ts: slug='cert-manager', category='security-tools', isSystem=true
- cert-manager.service.ts: detectFormat, toForgeBuffer, getFingerprint, parsePemChain;
  operation stubs parseCert/splitCerts/mergeCerts/convertCert throw NotImplementedException
- cert-manager.controller.ts: 4 POST routes with FileInterceptor/FilesInterceptor
  (5 MB limit each), @UseModule('cert-manager') guard, BadRequestException on missing input
- dto/: ParseCertDto, MergeCertsDto, ConvertCertDto
- app.module.ts: CertManagerModule added to imports array
- All 11 Vitest tests pass; type-check clean
This commit is contained in:
2026-07-01 23:21:36 +02:00
parent a06694f915
commit 8bb5cf208d
8 changed files with 329 additions and 0 deletions
@@ -0,0 +1,114 @@
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 { 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: any,
@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: any,
@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: any[],
@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, PFX/P12, P7B, CRT/CER formats.
*/
@Post('convert')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 },
}),
)
async convertCert(
@UploadedFile() file: any,
@Body('targetFormat') targetFormat: string,
@Body('password') password?: string,
) {
if (!file) {
throw new BadRequestException('No file provided');
}
return this.certManagerService.convertCert({ file, targetFormat, password });
}
}