feat(certificates): add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events
This commit is contained in:
174
ts/opsserver/handlers/certificate.handler.ts
Normal file
174
ts/opsserver/handlers/certificate.handler.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class CertificateHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Get Certificate Overview
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
||||
'getCertificateOverview',
|
||||
async (dataArg) => {
|
||||
const certificates = this.buildCertificateOverview();
|
||||
const summary = this.buildSummary(certificates);
|
||||
return { certificates, summary };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Reprovision Certificate
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||
'reprovisionCertificate',
|
||||
async (dataArg) => {
|
||||
return this.reprovisionCertificate(dataArg.routeName);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private buildCertificateOverview(): interfaces.requests.ICertificateInfo[] {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
if (!smartProxy) return [];
|
||||
|
||||
const routes = smartProxy.routeManager.getRoutes();
|
||||
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.name) continue;
|
||||
|
||||
const tls = route.action?.tls;
|
||||
if (!tls) continue;
|
||||
|
||||
// Skip passthrough routes - they don't manage certificates
|
||||
if (tls.mode === 'passthrough') continue;
|
||||
|
||||
const routeDomains = route.match.domains
|
||||
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
||||
: [];
|
||||
|
||||
// Determine source
|
||||
let source: interfaces.requests.TCertificateSource = 'none';
|
||||
if (tls.certificate === 'auto') {
|
||||
// Check if a certProvisionFunction is configured
|
||||
if ((smartProxy.settings as any).certProvisionFunction) {
|
||||
source = 'provision-function';
|
||||
} else {
|
||||
source = 'acme';
|
||||
}
|
||||
} else if (tls.certificate && typeof tls.certificate === 'object') {
|
||||
source = 'static';
|
||||
}
|
||||
|
||||
// Start with unknown status
|
||||
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
||||
let expiryDate: string | undefined;
|
||||
let issuedAt: string | undefined;
|
||||
let issuer: string | undefined;
|
||||
let error: string | undefined;
|
||||
|
||||
// Check event-based status from DcRouter's certificateStatusMap
|
||||
const eventStatus = dcRouter.certificateStatusMap.get(route.name);
|
||||
if (eventStatus) {
|
||||
status = eventStatus.status;
|
||||
expiryDate = eventStatus.expiryDate;
|
||||
issuedAt = eventStatus.issuedAt;
|
||||
error = eventStatus.error;
|
||||
}
|
||||
|
||||
// Try to get Rust-side certificate data
|
||||
try {
|
||||
// getCertificateStatus is async but we're in a sync context
|
||||
// We'll rely on event-based data primarily
|
||||
} catch {
|
||||
// Ignore errors from Rust bridge
|
||||
}
|
||||
|
||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
||||
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
status = 'expired';
|
||||
} else if (daysUntilExpiry < 30) {
|
||||
status = 'expiring';
|
||||
} else {
|
||||
status = 'valid';
|
||||
}
|
||||
}
|
||||
|
||||
// Static certs with no other info default to 'valid'
|
||||
if (source === 'static' && status === 'unknown') {
|
||||
status = 'valid';
|
||||
}
|
||||
|
||||
const canReprovision = source === 'acme' || source === 'provision-function';
|
||||
|
||||
certificates.push({
|
||||
routeName: route.name,
|
||||
domains: routeDomains,
|
||||
status,
|
||||
source,
|
||||
tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
|
||||
expiryDate,
|
||||
issuer,
|
||||
issuedAt,
|
||||
error,
|
||||
canReprovision,
|
||||
});
|
||||
}
|
||||
|
||||
return certificates;
|
||||
}
|
||||
|
||||
private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
|
||||
total: number;
|
||||
valid: number;
|
||||
expiring: number;
|
||||
expired: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
} {
|
||||
const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
|
||||
summary.total = certificates.length;
|
||||
for (const cert of certificates) {
|
||||
switch (cert.status) {
|
||||
case 'valid': summary.valid++; break;
|
||||
case 'expiring': summary.expiring++; break;
|
||||
case 'expired': summary.expired++; break;
|
||||
case 'failed': summary.failed++; break;
|
||||
case 'provisioning': // count as unknown
|
||||
case 'unknown': summary.unknown++; break;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
|
||||
if (!smartProxy) {
|
||||
return { success: false, message: 'SmartProxy is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
await smartProxy.provisionCertificate(routeName);
|
||||
// Clear event-based status so it gets refreshed
|
||||
dcRouter.certificateStatusMap.delete(routeName);
|
||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||
} catch (err) {
|
||||
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user