# Phase 9: Cert Manager Module - Pattern Map **Mapped:** 2026-07-01 **Files analyzed:** 11 new/modified files **Analogs found:** 10 / 11 --- ## File Classification | New/Modified File | Role | Data Flow | Closest Analog | Match Quality | |-------------------|------|-----------|----------------|---------------| | `apps/api/src/cert-manager/cert-manager.module.ts` | module | request-response | `apps/api/src/domaincheck/domaincheck.module.ts` | exact | | `apps/api/src/cert-manager/cert-manager.seed.ts` | utility | — | `apps/api/src/domaincheck/domaincheck.seed.ts` | exact | | `apps/api/src/cert-manager/cert-manager.controller.ts` | controller | file-I/O | `apps/api/src/dkv/dkv.controller.ts` | role-match | | `apps/api/src/cert-manager/cert-manager.service.ts` | service | transform | `apps/api/src/domaincheck/domaincheck.service.ts` | role-match | | `apps/api/src/cert-manager/dto/parse-cert.dto.ts` | dto | — | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | role-match | | `apps/api/src/app.module.ts` (modify) | config | — | current file | exact | | `apps/web/src/app/(portal)/modules/cert-manager/page.tsx` | component | request-response | `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` | exact | | `apps/web/src/app/(portal)/modules/cert-manager/actions.ts` | utility | request-response | `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` | exact | | `apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx` | component | file-I/O | `apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx` | role-match | | `apps/web/src/messages/de.json` (modify) | config | — | existing `domaincheck` namespace | exact | | `apps/web/src/messages/en.json` (modify) | config | — | existing `domaincheck` namespace | exact | --- ## Pattern Assignments ### `apps/api/src/cert-manager/cert-manager.module.ts` (module, OnModuleInit) **Analog:** `apps/api/src/domaincheck/domaincheck.module.ts` **Full pattern** (lines 1-38): ```typescript import { Logger, Module, OnModuleInit } from '@nestjs/common'; import { ModuleRegistryModule } from '../module-registry/module-registry.module'; import { ModuleRegistryService } from '../module-registry/module-registry.service'; import { CertManagerController } from './cert-manager.controller'; import { seedCertManagerModule } from './cert-manager.seed'; import { CertManagerService } from './cert-manager.service'; @Module({ imports: [ModuleRegistryModule], controllers: [CertManagerController], providers: [CertManagerService], }) export class CertManagerModule implements OnModuleInit { private readonly logger = new Logger(CertManagerModule.name); constructor( private readonly moduleRegistryService: ModuleRegistryService, ) {} async onModuleInit(): Promise { try { await seedCertManagerModule(this.moduleRegistryService); this.logger.log('Cert-Manager module seeded in registry'); } catch (error) { this.logger.error('Failed to seed cert-manager module', error); } } } ``` --- ### `apps/api/src/cert-manager/cert-manager.seed.ts` (utility, seed) **Analog:** `apps/api/src/domaincheck/domaincheck.seed.ts` **Full pattern** (lines 1-25): ```typescript import { ModuleRegistryService } from '../module-registry/module-registry.service'; export async function seedCertManagerModule( moduleRegistryService: ModuleRegistryService, ): Promise { await moduleRegistryService.seedModule({ slug: 'cert-manager', name: 'Cert Manager', version: '1.0.0', category: 'security-tools', description: { de: 'Zertifikate analysieren, konvertieren und verwalten', en: 'Inspect, convert and manage certificates', }, isSystem: true, }); } ``` **Critical note:** `isSystem: true` registers the module in the `Module` table but does NOT auto-activate it per tenant. The `TenantModuleActivation` record must be created manually via the marketplace UI before any API endpoint responds (otherwise `ModuleGuard` returns 403). --- ### `apps/api/src/cert-manager/cert-manager.controller.ts` (controller, file-I/O) **Analog:** `apps/api/src/dkv/dkv.controller.ts` **Imports pattern** (lines 1-17 of dkv.controller.ts): ```typescript import { BadRequestException, Body, Controller, Post, Req, 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'; ``` **Module guard pattern** (from `apps/api/src/domaincheck/domaincheck.controller.ts` lines 6-8): ```typescript @Controller('modules/cert-manager') @UseModule('cert-manager') export class CertManagerController { constructor(private readonly certManagerService: CertManagerService) {} ``` **Single file upload pattern** (from `apps/api/src/dkv/dkv.controller.ts` lines 210-229): ```typescript @Post('parse') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB })) async parseCert( @Req() req: any, @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 }); } ``` **Multi-file upload pattern for merge** (FilesInterceptor — plural): ```typescript @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 }); } ``` **Tenant extraction helper** (from `apps/api/src/dkv/dkv.controller.ts` lines 233-240): ```typescript private _requireTenant(req: any): string { const tenantId = req.tenantId as string | undefined; if (!tenantId) { throw new BadRequestException('No tenant context'); } return tenantId; } ``` **Binary download pattern** (from `apps/api/src/dkv/dkv.controller.ts` lines 143-160) — NOTE: cert-manager uses JSON base64 response instead of `res.send(buffer)` because endpoints are POST-only and cannot use GET anchors for authenticated downloads: ```typescript // cert-manager variant: return base64 JSON (not res.send) return { filename: 'certificate.pem', content: Buffer.from(pemString, 'utf-8').toString('base64'), mimeType: 'application/x-pem-file', }; ``` --- ### `apps/api/src/cert-manager/cert-manager.service.ts` (service, transform) **Analog:** `apps/api/src/domaincheck/domaincheck.service.ts` **Service structure pattern** (lines 1-16 of domaincheck.service.ts): ```typescript import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import * as forge from 'node-forge'; @Injectable() export class CertManagerService { private readonly logger = new Logger(CertManagerService.name); async parseCert(input: { file?: any; pemText?: string; password?: string }) { try { // ... node-forge operations } catch (error) { this.logger.warn(`parseCert failed: ${error}`); throw new BadRequestException( 'Invalid certificate. Check the file format or password.', ); } } } ``` **Error handling pattern:** All node-forge operations throw synchronously on malformed input. Wrap every service method in `try/catch` → `throw new BadRequestException(...)`. Never log the `password` parameter. **Binary encoding rule:** For DER/PFX/P7B buffers, use `buffer.toString('binary')` (never `'utf-8'`) when passing to `forge.util.createBuffer()`. --- ### `apps/api/src/cert-manager/dto/parse-cert.dto.ts` (dto) **Analog:** `apps/api/src/domaincheck/dto/check-domain.dto.ts` ```typescript // parse-cert.dto.ts — for JSON body (text paste path only) export class ParseCertDto { pemText!: string; password?: string; } // merge-certs.dto.ts — body fields alongside FilesInterceptor export class MergeCertsDto { outputFormat!: 'pem' | 'pfx'; password?: string; } // convert-cert.dto.ts — body fields alongside FileInterceptor export class ConvertCertDto { targetFormat!: 'pem' | 'der' | 'pfx' | 'p7b'; password?: string; } ``` --- ### `apps/api/src/app.module.ts` (modify — add CertManagerModule) **Analog:** Current file, lines 14-41 **Pattern:** Add import + add to imports array, following domaincheck: ```typescript // Add to imports at top: import { CertManagerModule } from './cert-manager/cert-manager.module'; // Add to @Module({ imports: [...] }) array (after DomaincheckModule): CertManagerModule, ``` --- ### `apps/web/src/app/(portal)/modules/cert-manager/page.tsx` (component, request-response) **Analog:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` **Header + layout pattern** (lines 1-64 of domaincheck/page.tsx): ```typescript 'use client'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; export default function CertManagerPage() { const t = useTranslations('certManager'); const [activeTab, setActiveTab] = useState<'inspect' | 'split' | 'merge' | 'convert'>('inspect'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); return (
{/* Header */}

{t('title')}

{t('description')}

{/* Shared Input Card */}
{/* DropZone + OR divider + Textarea + conditional Password field */}
{/* Tab Navigation */}
{(['inspect', 'split', 'merge', 'convert'] as const).map((tab) => ( ))}
{/* Tab Content Card */}
{error &&

{error}

} {/* Tab-specific content */}
); } ``` **Layout differences from domaincheck:** Use `max-w-4xl` (not `max-w-2xl`) per UI-SPEC. Tab navigation is manually rendered (no shadcn Tabs component — UI-SPEC confirms no shadcn). --- ### `apps/web/src/app/(portal)/modules/cert-manager/actions.ts` (utility, request-response) **Analog:** `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` **Full pattern** (lines 1-33 of domaincheck/actions.ts): ```typescript const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; // JSON body endpoint (PEM text paste) export async function parseCertAction(pemText: string, password?: string) { const response = await fetch(`${API_URL}/modules/cert-manager/parse`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pemText, password }), credentials: 'include', }); if (!response.ok) { const errorBody = await response.text().catch(() => ''); throw new Error(`${response.status} ${errorBody}`.trim()); } return response.json(); } // Multipart endpoint (file upload) export async function parseCertFileAction(file: File, password?: string) { const form = new FormData(); form.append('file', file); if (password) form.append('password', password); const response = await fetch(`${API_URL}/modules/cert-manager/parse`, { method: 'POST', // Do NOT set Content-Type — fetch sets multipart/form-data + boundary body: form, credentials: 'include', }); if (!response.ok) { const errorBody = await response.text().catch(() => ''); throw new Error(`${response.status} ${errorBody}`.trim()); } return response.json(); } // Multi-file upload (merge) export async function mergeCertsAction(files: File[], outputFormat: string, password?: string) { const form = new FormData(); files.forEach(file => form.append('files', file)); // same field name, multiple values form.append('outputFormat', outputFormat); if (password) form.append('password', password); const response = await fetch(`${API_URL}/modules/cert-manager/merge`, { method: 'POST', body: form, credentials: 'include', }); if (!response.ok) { const errorBody = await response.text().catch(() => ''); throw new Error(`${response.status} ${errorBody}`.trim()); } return response.json(); // { filename, content (base64), mimeType } } ``` **Base64 download helper** (no analog exists — new pattern): ```typescript export function downloadBase64(filename: string, content: string, mimeType: string) { const bytes = Uint8Array.from(atob(content), c => c.charCodeAt(0)); const blob = new Blob([bytes], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } ``` --- ### `apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx` (component, file-I/O) **Analog:** `apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx` **Hidden file input + click-to-browse pattern** (lines 92-109 of CsvImportButton.tsx): ```typescript 'use client'; import { useRef, useState } from 'react'; export function DropZone({ onFile, accept }: { onFile: (file: File) => void; accept: string }) { const fileInputRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); const [currentFile, setCurrentFile] = useState(null); return (
{ e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { e.preventDefault(); setIsDragOver(false); const file = e.dataTransfer.files[0]; if (file) { setCurrentFile(file); onFile(file); } }} onClick={() => fileInputRef.current?.click()} className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${ isDragOver ? 'border-primary bg-primary/5' : 'border-border' }`} > { const file = e.target.files?.[0]; if (file) { setCurrentFile(file); onFile(file); } e.target.value = ''; // allow re-selecting same file }} /> {currentFile ? (

{currentFile.name}

) : (

Datei hierher ziehen oder klicken

)}
); } ``` **Error display pattern** (from CsvImportButton.tsx lines 178-180): ```typescript {importError && (

{importError}

)} ``` **Loading button pattern** (from CsvImportButton.tsx lines 192-208): ```typescript ``` --- ### `apps/web/src/messages/de.json` and `en.json` (modify) **Analog:** Existing `domaincheck` namespace at line 337 in de.json **Pattern:** Add `certManager` namespace at the same level as `domaincheck`. The full namespace content is specified in `09-RESEARCH.md` under "i18n Namespace Structure". No structural changes — append only. --- ## Shared Patterns ### Module Guard (`@UseModule`) **Source:** `apps/api/src/module-registry/module.guard.ts` lines 75-80 **Apply to:** `cert-manager.controller.ts` — decorate the controller class (not individual handlers) ```typescript import { UseModule } from '../module-registry/module.guard'; @Controller('modules/cert-manager') @UseModule('cert-manager') // applies ModuleGuard to ALL routes in this controller export class CertManagerController { ... } ``` ### Error Handling (NestJS) **Source:** `apps/api/src/dkv/dkv.controller.ts` lines 152-158 + domaincheck.service.ts lines 80-88 **Apply to:** `cert-manager.controller.ts` and `cert-manager.service.ts` ```typescript // Controller: re-throw known exceptions, let unknown bubble if (error instanceof NotFoundException || error instanceof BadRequestException) { throw error; } throw error; // Service: wrap node-forge ops in try/catch → BadRequestException try { const cert = forge.pki.certificateFromPem(pemString); } catch (_err) { throw new BadRequestException('Invalid certificate format'); } ``` ### Client State + Loading Pattern **Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` lines 20-36 **Apply to:** `cert-manager/page.tsx` ```typescript const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const handleAction = async () => { setIsLoading(true); setError(null); try { const result = await someAction(); setResult(result); } catch (err) { setError(err instanceof Error ? err.message : t('certManager.error.generic')); } finally { setIsLoading(false); } }; ``` ### i18n String Pattern **Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` line 2, 20 **Apply to:** All frontend files in `cert-manager/` ```typescript import { useTranslations } from 'next-intl'; const t = useTranslations('certManager'); // Usage: t('title'), t('tabs.inspect'), t('actions.processing') ``` ### Fetch with Auth (Multipart) **Source:** `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` lines 18-23 **Apply to:** All fetch calls in `cert-manager/actions.ts` - JSON body: set `Content-Type: application/json` - Multipart (FormData): do NOT set `Content-Type` — let fetch set boundary automatically - Always include `credentials: 'include'` for cookie auth --- ## No Analog Found | File | Role | Data Flow | Reason | |------|------|-----------|--------| | `apps/api/src/cert-manager/cert-manager.service.ts` (node-forge internals) | service | transform | No crypto/binary-processing service exists in codebase — node-forge API patterns must follow RESEARCH.md Pattern 5 | --- ## Metadata **Analog search scope:** `apps/api/src/domaincheck/`, `apps/api/src/dkv/`, `apps/api/src/module-registry/`, `apps/web/src/app/(portal)/modules/domaincheck/`, `apps/web/src/app/(portal)/modules/dkv-fleet/` **Files scanned:** 10 **Pattern extraction date:** 2026-07-01