feat(certificates): add certificate import, export, and deletion support (server handlers, request types, and UI)

This commit is contained in:
2026-02-17 16:28:33 +00:00
parent 529a4bae00
commit c23f16149c
7 changed files with 455 additions and 2 deletions

View File

@@ -42,6 +42,36 @@ export class CertificateHandler {
}
)
);
// Delete certificate
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
'deleteCertificate',
async (dataArg) => {
return this.deleteCertificate(dataArg.domain);
}
)
);
// Export certificate
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
'exportCertificate',
async (dataArg) => {
return this.exportCertificate(dataArg.domain);
}
)
);
// Import certificate
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
'importCertificate',
async (dataArg) => {
return this.importCertificate(dataArg.cert);
}
)
);
}
/**
@@ -324,4 +354,154 @@ export class CertificateHandler {
return { success: false, message: `No routes found for domain '${domain}'` };
}
/**
* Delete certificate data for a domain from storage
*/
private async deleteCertificate(domain: string): Promise<{ success: boolean; message?: string }> {
const dcRouter = this.opsServerRef.dcRouterRef;
const cleanDomain = domain.replace(/^\*\.?/, '');
// Delete from all known storage paths
const paths = [
`/proxy-certs/${domain}`,
`/proxy-certs/${cleanDomain}`,
`/certs/${cleanDomain}`,
];
for (const path of paths) {
try {
await dcRouter.storageManager.delete(path);
} catch {
// Path may not exist — ignore
}
}
// Clear from in-memory status map
dcRouter.certificateStatusMap.delete(domain);
// Clear backoff info
if (dcRouter.certProvisionScheduler) {
await dcRouter.certProvisionScheduler.clearBackoff(domain);
}
return { success: true, message: `Certificate data deleted for '${domain}'` };
}
/**
* Export certificate data for a domain as ICert-shaped JSON
*/
private async exportCertificate(domain: string): Promise<{
success: boolean;
cert?: {
id: string;
domainName: string;
created: number;
validUntil: number;
privateKey: string;
publicKey: string;
csr: string;
};
message?: string;
}> {
const dcRouter = this.opsServerRef.dcRouterRef;
const cleanDomain = domain.replace(/^\*\.?/, '');
// Try SmartAcme /certs/ path first (has full ICert fields)
let certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
if (certData && certData.publicKey && certData.privateKey) {
return {
success: true,
cert: {
id: certData.id || plugins.crypto.randomUUID(),
domainName: certData.domainName || domain,
created: certData.created || Date.now(),
validUntil: certData.validUntil || 0,
privateKey: certData.privateKey,
publicKey: certData.publicKey,
csr: certData.csr || '',
},
};
}
// Fallback: try /proxy-certs/ with original domain
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${domain}`);
if (!certData || !certData.publicKey) {
// Try with clean domain
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${cleanDomain}`);
}
if (certData && certData.publicKey && certData.privateKey) {
return {
success: true,
cert: {
id: plugins.crypto.randomUUID(),
domainName: domain,
created: certData.validFrom || Date.now(),
validUntil: certData.validUntil || 0,
privateKey: certData.privateKey,
publicKey: certData.publicKey,
csr: '',
},
};
}
return { success: false, message: `No certificate data found for '${domain}'` };
}
/**
* Import a certificate from ICert-shaped JSON
*/
private async importCertificate(cert: {
id: string;
domainName: string;
created: number;
validUntil: number;
privateKey: string;
publicKey: string;
csr: string;
}): Promise<{ success: boolean; message?: string }> {
// Validate PEM content
if (!cert.publicKey || !cert.publicKey.includes('-----BEGIN CERTIFICATE-----')) {
return { success: false, message: 'Invalid publicKey: must contain a PEM-encoded certificate' };
}
if (!cert.privateKey || !cert.privateKey.includes('-----BEGIN')) {
return { success: false, message: 'Invalid privateKey: must contain a PEM-encoded key' };
}
const dcRouter = this.opsServerRef.dcRouterRef;
const cleanDomain = cert.domainName.replace(/^\*\.?/, '');
// Save to /certs/ (SmartAcme-compatible path)
await dcRouter.storageManager.setJSON(`/certs/${cleanDomain}`, {
id: cert.id,
domainName: cert.domainName,
created: cert.created,
validUntil: cert.validUntil,
privateKey: cert.privateKey,
publicKey: cert.publicKey,
csr: cert.csr || '',
});
// Also save to /proxy-certs/ (proxy-cert format)
await dcRouter.storageManager.setJSON(`/proxy-certs/${cert.domainName}`, {
domain: cert.domainName,
publicKey: cert.publicKey,
privateKey: cert.privateKey,
ca: undefined,
validUntil: cert.validUntil,
validFrom: cert.created,
});
// Update in-memory status map
dcRouter.certificateStatusMap.set(cert.domainName, {
status: 'valid',
source: 'static',
expiryDate: cert.validUntil ? new Date(cert.validUntil).toISOString() : undefined,
issuedAt: cert.created ? new Date(cert.created).toISOString() : undefined,
routeNames: [],
});
return { success: true, message: `Certificate imported for '${cert.domainName}'` };
}
}