feat(certificates): add certificate import, export, and deletion support (server handlers, request types, and UI)
This commit is contained in:
@@ -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}'` };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user