@serve.zone/dcrouter 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +2444 -2300
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +12 -0
- package/dist_ts/classes.dcrouter.js +53 -6
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +11 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +170 -0
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts_interfaces/requests/certificate.d.ts +44 -0
- package/dist_ts_interfaces/requests/certificate.js +3 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +17 -0
- package/dist_ts_web/appstate.js +71 -2
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.js +6 -1
- package/dist_ts_web/elements/ops-view-certificates.d.ts +20 -0
- package/dist_ts_web/elements/ops-view-certificates.js +379 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +63 -10
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/certificate.handler.ts +186 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +98 -2
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +5 -0
- package/ts_web/elements/ops-view-certificates.ts +355 -0
- package/ts_web/router.ts +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export class CertificateHandler {
|
|
6
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
7
|
+
|
|
8
|
+
constructor(private opsServerRef: OpsServer) {
|
|
9
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
10
|
+
this.registerHandlers();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private registerHandlers(): void {
|
|
14
|
+
// Get Certificate Overview
|
|
15
|
+
this.typedrouter.addTypedHandler(
|
|
16
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
|
17
|
+
'getCertificateOverview',
|
|
18
|
+
async (dataArg) => {
|
|
19
|
+
const certificates = await this.buildCertificateOverview();
|
|
20
|
+
const summary = this.buildSummary(certificates);
|
|
21
|
+
return { certificates, summary };
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Reprovision Certificate
|
|
27
|
+
this.typedrouter.addTypedHandler(
|
|
28
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
|
29
|
+
'reprovisionCertificate',
|
|
30
|
+
async (dataArg) => {
|
|
31
|
+
return this.reprovisionCertificate(dataArg.routeName);
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
|
38
|
+
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
39
|
+
const smartProxy = dcRouter.smartProxy;
|
|
40
|
+
if (!smartProxy) return [];
|
|
41
|
+
|
|
42
|
+
const routes = smartProxy.routeManager.getRoutes();
|
|
43
|
+
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
|
44
|
+
|
|
45
|
+
for (const route of routes) {
|
|
46
|
+
if (!route.name) continue;
|
|
47
|
+
|
|
48
|
+
const tls = route.action?.tls;
|
|
49
|
+
if (!tls) continue;
|
|
50
|
+
|
|
51
|
+
// Skip passthrough routes - they don't manage certificates
|
|
52
|
+
if (tls.mode === 'passthrough') continue;
|
|
53
|
+
|
|
54
|
+
const routeDomains = route.match.domains
|
|
55
|
+
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
|
56
|
+
: [];
|
|
57
|
+
|
|
58
|
+
// Determine source
|
|
59
|
+
let source: interfaces.requests.TCertificateSource = 'none';
|
|
60
|
+
if (tls.certificate === 'auto') {
|
|
61
|
+
// Check if a certProvisionFunction is configured
|
|
62
|
+
if ((smartProxy.settings as any).certProvisionFunction) {
|
|
63
|
+
source = 'provision-function';
|
|
64
|
+
} else {
|
|
65
|
+
source = 'acme';
|
|
66
|
+
}
|
|
67
|
+
} else if (tls.certificate && typeof tls.certificate === 'object') {
|
|
68
|
+
source = 'static';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Start with unknown status
|
|
72
|
+
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
|
73
|
+
let expiryDate: string | undefined;
|
|
74
|
+
let issuedAt: string | undefined;
|
|
75
|
+
let issuer: string | undefined;
|
|
76
|
+
let error: string | undefined;
|
|
77
|
+
|
|
78
|
+
// Check event-based status from DcRouter's certificateStatusMap
|
|
79
|
+
const eventStatus = dcRouter.certificateStatusMap.get(route.name);
|
|
80
|
+
if (eventStatus) {
|
|
81
|
+
status = eventStatus.status;
|
|
82
|
+
expiryDate = eventStatus.expiryDate;
|
|
83
|
+
issuedAt = eventStatus.issuedAt;
|
|
84
|
+
error = eventStatus.error;
|
|
85
|
+
if (eventStatus.source) {
|
|
86
|
+
issuer = eventStatus.source;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try Rust-side certificate status if no event data
|
|
91
|
+
if (status === 'unknown') {
|
|
92
|
+
try {
|
|
93
|
+
const rustStatus = await smartProxy.getCertificateStatus(route.name);
|
|
94
|
+
if (rustStatus) {
|
|
95
|
+
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
|
96
|
+
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
|
97
|
+
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
|
|
98
|
+
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
|
|
99
|
+
status = rustStatus.status;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Rust bridge may not support this command yet — ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compute status from expiry date if we have one and status is still valid/unknown
|
|
108
|
+
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
|
109
|
+
const expiry = new Date(expiryDate);
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
|
112
|
+
|
|
113
|
+
if (daysUntilExpiry < 0) {
|
|
114
|
+
status = 'expired';
|
|
115
|
+
} else if (daysUntilExpiry < 30) {
|
|
116
|
+
status = 'expiring';
|
|
117
|
+
} else {
|
|
118
|
+
status = 'valid';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Static certs with no other info default to 'valid'
|
|
123
|
+
if (source === 'static' && status === 'unknown') {
|
|
124
|
+
status = 'valid';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const canReprovision = source === 'acme' || source === 'provision-function';
|
|
128
|
+
|
|
129
|
+
certificates.push({
|
|
130
|
+
routeName: route.name,
|
|
131
|
+
domains: routeDomains,
|
|
132
|
+
status,
|
|
133
|
+
source,
|
|
134
|
+
tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
|
|
135
|
+
expiryDate,
|
|
136
|
+
issuer,
|
|
137
|
+
issuedAt,
|
|
138
|
+
error,
|
|
139
|
+
canReprovision,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return certificates;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
|
|
147
|
+
total: number;
|
|
148
|
+
valid: number;
|
|
149
|
+
expiring: number;
|
|
150
|
+
expired: number;
|
|
151
|
+
failed: number;
|
|
152
|
+
unknown: number;
|
|
153
|
+
} {
|
|
154
|
+
const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
|
|
155
|
+
summary.total = certificates.length;
|
|
156
|
+
for (const cert of certificates) {
|
|
157
|
+
switch (cert.status) {
|
|
158
|
+
case 'valid': summary.valid++; break;
|
|
159
|
+
case 'expiring': summary.expiring++; break;
|
|
160
|
+
case 'expired': summary.expired++; break;
|
|
161
|
+
case 'failed': summary.failed++; break;
|
|
162
|
+
case 'provisioning': // count as unknown
|
|
163
|
+
case 'unknown': summary.unknown++; break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return summary;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
|
|
170
|
+
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
171
|
+
const smartProxy = dcRouter.smartProxy;
|
|
172
|
+
|
|
173
|
+
if (!smartProxy) {
|
|
174
|
+
return { success: false, message: 'SmartProxy is not running' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await smartProxy.provisionCertificate(routeName);
|
|
179
|
+
// Clear event-based status so it gets refreshed
|
|
180
|
+
dcRouter.certificateStatusMap.delete(routeName);
|
|
181
|
+
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -4,4 +4,5 @@ export * from './logs.handler.js';
|
|
|
4
4
|
export * from './security.handler.js';
|
|
5
5
|
export * from './stats.handler.js';
|
|
6
6
|
export * from './radius.handler.js';
|
|
7
|
-
export * from './email-ops.handler.js';
|
|
7
|
+
export * from './email-ops.handler.js';
|
|
8
|
+
export * from './certificate.handler.js';
|
package/ts_web/appstate.ts
CHANGED
|
@@ -53,6 +53,14 @@ export interface INetworkState {
|
|
|
53
53
|
error: string | null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export interface ICertificateState {
|
|
57
|
+
certificates: interfaces.requests.ICertificateInfo[];
|
|
58
|
+
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
|
59
|
+
isLoading: boolean;
|
|
60
|
+
error: string | null;
|
|
61
|
+
lastUpdated: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
export interface IEmailOpsState {
|
|
57
65
|
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
|
58
66
|
queuedEmails: interfaces.requests.IEmailQueueItem[];
|
|
@@ -103,7 +111,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|
|
103
111
|
// Determine initial view from URL path
|
|
104
112
|
const getInitialView = (): string => {
|
|
105
113
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
|
106
|
-
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'];
|
|
114
|
+
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
|
107
115
|
const segments = path.split('/').filter(Boolean);
|
|
108
116
|
const view = segments[0];
|
|
109
117
|
return validViews.includes(view) ? view : 'overview';
|
|
@@ -162,6 +170,18 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
|
|
162
170
|
'soft'
|
|
163
171
|
);
|
|
164
172
|
|
|
173
|
+
export const certificateStatePart = await appState.getStatePart<ICertificateState>(
|
|
174
|
+
'certificates',
|
|
175
|
+
{
|
|
176
|
+
certificates: [],
|
|
177
|
+
summary: { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 },
|
|
178
|
+
isLoading: false,
|
|
179
|
+
error: null,
|
|
180
|
+
lastUpdated: 0,
|
|
181
|
+
},
|
|
182
|
+
'soft'
|
|
183
|
+
);
|
|
184
|
+
|
|
165
185
|
// Actions for state management
|
|
166
186
|
interface IActionContext {
|
|
167
187
|
identity: interfaces.data.IIdentity | null;
|
|
@@ -340,7 +360,14 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|
|
340
360
|
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
|
341
361
|
}, 100);
|
|
342
362
|
}
|
|
343
|
-
|
|
363
|
+
|
|
364
|
+
// If switching to certificates view, ensure we fetch certificate data
|
|
365
|
+
if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
368
|
+
}, 100);
|
|
369
|
+
}
|
|
370
|
+
|
|
344
371
|
return {
|
|
345
372
|
...currentState,
|
|
346
373
|
activeView: viewName,
|
|
@@ -641,6 +668,66 @@ export const removeFromSuppressionListAction = emailOpsStatePart.createAction<st
|
|
|
641
668
|
}
|
|
642
669
|
);
|
|
643
670
|
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// Certificate Actions
|
|
673
|
+
// ============================================================================
|
|
674
|
+
|
|
675
|
+
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
|
|
676
|
+
const context = getActionContext();
|
|
677
|
+
const currentState = statePartArg.getState();
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
681
|
+
interfaces.requests.IReq_GetCertificateOverview
|
|
682
|
+
>('/typedrequest', 'getCertificateOverview');
|
|
683
|
+
|
|
684
|
+
const response = await request.fire({
|
|
685
|
+
identity: context.identity,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
certificates: response.certificates,
|
|
690
|
+
summary: response.summary,
|
|
691
|
+
isLoading: false,
|
|
692
|
+
error: null,
|
|
693
|
+
lastUpdated: Date.now(),
|
|
694
|
+
};
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
...currentState,
|
|
698
|
+
isLoading: false,
|
|
699
|
+
error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
|
705
|
+
async (statePartArg, routeName) => {
|
|
706
|
+
const context = getActionContext();
|
|
707
|
+
const currentState = statePartArg.getState();
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
711
|
+
interfaces.requests.IReq_ReprovisionCertificate
|
|
712
|
+
>('/typedrequest', 'reprovisionCertificate');
|
|
713
|
+
|
|
714
|
+
await request.fire({
|
|
715
|
+
identity: context.identity,
|
|
716
|
+
routeName,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Re-fetch overview after reprovisioning
|
|
720
|
+
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
721
|
+
return statePartArg.getState();
|
|
722
|
+
} catch (error) {
|
|
723
|
+
return {
|
|
724
|
+
...currentState,
|
|
725
|
+
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
|
|
644
731
|
// Combined refresh action for efficient polling
|
|
645
732
|
async function dispatchCombinedRefreshAction() {
|
|
646
733
|
const context = getActionContext();
|
|
@@ -725,6 +812,15 @@ async function dispatchCombinedRefreshAction() {
|
|
|
725
812
|
});
|
|
726
813
|
}
|
|
727
814
|
}
|
|
815
|
+
|
|
816
|
+
// Refresh certificate data if on certificates view
|
|
817
|
+
if (currentView === 'certificates') {
|
|
818
|
+
try {
|
|
819
|
+
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
console.error('Certificate refresh failed:', error);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
728
824
|
} catch (error) {
|
|
729
825
|
console.error('Combined refresh failed:', error);
|
|
730
826
|
}
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { OpsViewEmails } from './ops-view-emails.js';
|
|
|
19
19
|
import { OpsViewLogs } from './ops-view-logs.js';
|
|
20
20
|
import { OpsViewConfig } from './ops-view-config.js';
|
|
21
21
|
import { OpsViewSecurity } from './ops-view-security.js';
|
|
22
|
+
import { OpsViewCertificates } from './ops-view-certificates.js';
|
|
22
23
|
|
|
23
24
|
@customElement('ops-dashboard')
|
|
24
25
|
export class OpsDashboard extends DeesElement {
|
|
@@ -61,6 +62,10 @@ export class OpsDashboard extends DeesElement {
|
|
|
61
62
|
name: 'Security',
|
|
62
63
|
element: OpsViewSecurity,
|
|
63
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Certificates',
|
|
67
|
+
element: OpsViewCertificates,
|
|
68
|
+
},
|
|
64
69
|
];
|
|
65
70
|
|
|
66
71
|
/**
|