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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user