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( 'getCertificateOverview', async (dataArg) => { const certificates = this.buildCertificateOverview(); const summary = this.buildSummary(certificates); return { certificates, summary }; } ) ); // Reprovision Certificate this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( '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' }; } } }