feat(cert-manager): cert role badges + ZIP download in split view
- API: detectCertRole() classifies certs as root/intermediate/end-entity via basicConstraints.cA + self-signed check (subject.hash === issuer.hash) - API: SplitEntry gains certRole field; filenames now reflect role (root-ca.pem, intermediate-1.pem, cert.pem) - Web: SplitTab shows colour-coded role badge per cert (red=Root-CA, amber=Zwischen-CA, blue=Zertifikat) - Web: "Alle als ZIP herunterladen" button via fflate (client-side) - i18n: add certRole labels + downloadZip action key (de + en) - i18n: add missing accentColor* and deleteAvatar* keys (de + en) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ export interface CertDetails {
|
||||
// SplitResponse — the structured result returned by splitCerts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type CertRole = 'root' | 'intermediate' | 'end-entity';
|
||||
|
||||
export interface SplitEntry {
|
||||
index: number;
|
||||
filename: string;
|
||||
@@ -29,6 +31,7 @@ export interface SplitEntry {
|
||||
content: string;
|
||||
subject: { cn: string };
|
||||
validity: { notAfter: string };
|
||||
certRole: CertRole;
|
||||
}
|
||||
|
||||
export interface SplitResponse {
|
||||
@@ -140,6 +143,13 @@ export class CertManagerService {
|
||||
return blocks.map((b) => forge.pki.certificateFromPem(b));
|
||||
}
|
||||
|
||||
private detectCertRole(cert: forge.pki.Certificate): CertRole {
|
||||
const bc = cert.getExtension('basicConstraints') as { cA?: boolean } | null;
|
||||
if (!bc?.cA) return 'end-entity';
|
||||
// Self-signed = subject hash matches issuer hash → Root CA
|
||||
return cert.subject.hash === cert.issuer.hash ? 'root' : 'intermediate';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseCert — CERT-01 + CERT-05 (read half)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -364,19 +374,34 @@ export class CertManagerService {
|
||||
throw new BadRequestException('Failed to split certificates: invalid format or corrupted file');
|
||||
}
|
||||
|
||||
// ── Determine cert roles ───────────────────────────────────────────────
|
||||
const roles: CertRole[] = certs.map((cert) => this.detectCertRole(cert));
|
||||
|
||||
// Build counters for filename disambiguation
|
||||
const roleCounters: Record<CertRole, number> = { root: 0, intermediate: 0, 'end-entity': 0 };
|
||||
const roleFilename = (role: CertRole): string => {
|
||||
roleCounters[role]++;
|
||||
const n = roleCounters[role];
|
||||
if (role === 'root') return n === 1 ? 'root-ca.pem' : `root-ca-${n}.pem`;
|
||||
if (role === 'intermediate') return `intermediate-${n}.pem`;
|
||||
return n === 1 ? 'cert.pem' : `cert-${n}.pem`;
|
||||
};
|
||||
|
||||
// ── Build SplitResponse ────────────────────────────────────────────────
|
||||
const certEntries: SplitEntry[] = certs.map((cert, index) => {
|
||||
const pemStr = forge.pki.certificateToPem(cert);
|
||||
const content = Buffer.from(pemStr, 'utf-8').toString('base64');
|
||||
const cn: string = cert.subject.getField('CN')?.value ?? '';
|
||||
const notAfter: string = cert.validity.notAfter.toISOString();
|
||||
const certRole = roles[index];
|
||||
|
||||
return {
|
||||
index,
|
||||
filename: `cert-${index + 1}.pem`,
|
||||
filename: roleFilename(certRole),
|
||||
content,
|
||||
subject: { cn },
|
||||
validity: { notAfter },
|
||||
certRole,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user