@serve.zone/dcrouter 13.27.1 → 13.28.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 +913 -799
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +11 -6
- package/dist_ts/config/classes.gateway-client-manager.d.ts +22 -0
- package/dist_ts/config/classes.gateway-client-manager.js +101 -0
- package/dist_ts/config/index.d.ts +1 -0
- package/dist_ts/config/index.js +2 -1
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +18 -0
- package/dist_ts/db/documents/classes.gateway-client.doc.js +133 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +5 -1
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +4 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +146 -16
- package/dist_ts_apiclient/classes.workhoster.d.ts +1 -0
- package/dist_ts_apiclient/classes.workhoster.js +5 -1
- package/dist_ts_interfaces/data/workhoster.d.ts +28 -3
- package/dist_ts_interfaces/requests/workhoster.d.ts +83 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +29 -0
- package/dist_ts_web/appstate.js +79 -1
- package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -1
- package/dist_ts_web/elements/access/ops-view-gatewayclients.d.ts +15 -0
- package/dist_ts_web/elements/access/ops-view-gatewayclients.js +293 -0
- package/dist_ts_web/elements/domains/ops-view-certificates.d.ts +6 -0
- package/dist_ts_web/elements/domains/ops-view-certificates.js +155 -13
- package/dist_ts_web/elements/network/ops-view-routes.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.js +3 -1
- package/dist_ts_web/router.js +3 -3
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +10 -5
- package/ts/config/classes.gateway-client-manager.ts +117 -0
- package/ts/config/index.ts +2 -1
- package/ts/db/documents/classes.gateway-client.doc.ts +54 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/opsserver/handlers/certificate.handler.ts +5 -0
- package/ts/opsserver/handlers/workhoster.handler.ts +191 -17
- package/ts_apiclient/classes.workhoster.ts +8 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +111 -0
- package/ts_web/elements/access/ops-view-apitokens.ts +1 -0
- package/ts_web/elements/access/ops-view-gatewayclients.ts +250 -0
- package/ts_web/elements/domains/ops-view-certificates.ts +166 -11
- package/ts_web/elements/network/ops-view-routes.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +2 -0
- package/ts_web/router.ts +2 -2
package/ts_web/appstate.ts
CHANGED
|
@@ -285,6 +285,7 @@ export interface IRouteManagementState {
|
|
|
285
285
|
mergedRoutes: interfaces.data.IMergedRoute[];
|
|
286
286
|
warnings: interfaces.data.IRouteWarning[];
|
|
287
287
|
apiTokens: interfaces.data.IApiTokenInfo[];
|
|
288
|
+
gatewayClients: interfaces.data.IGatewayClient[];
|
|
288
289
|
isLoading: boolean;
|
|
289
290
|
error: string | null;
|
|
290
291
|
lastUpdated: number;
|
|
@@ -296,6 +297,7 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
|
|
|
296
297
|
mergedRoutes: [],
|
|
297
298
|
warnings: [],
|
|
298
299
|
apiTokens: [],
|
|
300
|
+
gatewayClients: [],
|
|
299
301
|
isLoading: false,
|
|
300
302
|
error: null,
|
|
301
303
|
lastUpdated: 0,
|
|
@@ -2477,6 +2479,115 @@ export const fetchApiTokensAction = routeManagementStatePart.createAction(async
|
|
|
2477
2479
|
}
|
|
2478
2480
|
});
|
|
2479
2481
|
|
|
2482
|
+
export const fetchGatewayClientsAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
|
|
2483
|
+
const context = getActionContext();
|
|
2484
|
+
const currentState = statePartArg.getState()!;
|
|
2485
|
+
if (!context.identity) return currentState;
|
|
2486
|
+
|
|
2487
|
+
try {
|
|
2488
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2489
|
+
interfaces.requests.IReq_ListGatewayClients
|
|
2490
|
+
>('/typedrequest', 'listGatewayClients');
|
|
2491
|
+
const response = await request.fire({ identity: context.identity });
|
|
2492
|
+
return {
|
|
2493
|
+
...currentState,
|
|
2494
|
+
gatewayClients: response.gatewayClients,
|
|
2495
|
+
error: null,
|
|
2496
|
+
lastUpdated: Date.now(),
|
|
2497
|
+
};
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
return {
|
|
2500
|
+
...currentState,
|
|
2501
|
+
error: error instanceof Error ? error.message : 'Failed to fetch gateway clients',
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
export async function createGatewayClient(data: {
|
|
2507
|
+
id?: string;
|
|
2508
|
+
type: interfaces.data.IGatewayClient['type'];
|
|
2509
|
+
name: string;
|
|
2510
|
+
description?: string;
|
|
2511
|
+
hostnamePatterns?: string[];
|
|
2512
|
+
allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
|
|
2513
|
+
}) {
|
|
2514
|
+
const context = getActionContext();
|
|
2515
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2516
|
+
interfaces.requests.IReq_CreateGatewayClient
|
|
2517
|
+
>('/typedrequest', 'createGatewayClient');
|
|
2518
|
+
return request.fire({
|
|
2519
|
+
identity: context.identity!,
|
|
2520
|
+
capabilities: {
|
|
2521
|
+
readDomains: true,
|
|
2522
|
+
readDnsRecords: true,
|
|
2523
|
+
syncRoutes: true,
|
|
2524
|
+
syncDnsRecords: false,
|
|
2525
|
+
requestCertificates: false,
|
|
2526
|
+
},
|
|
2527
|
+
...data,
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
export const updateGatewayClientAction = routeManagementStatePart.createAction<{
|
|
2532
|
+
id: string;
|
|
2533
|
+
name?: string;
|
|
2534
|
+
description?: string;
|
|
2535
|
+
hostnamePatterns?: string[];
|
|
2536
|
+
allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
|
|
2537
|
+
enabled?: boolean;
|
|
2538
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
|
2539
|
+
const context = getActionContext();
|
|
2540
|
+
const currentState = statePartArg.getState()!;
|
|
2541
|
+
try {
|
|
2542
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2543
|
+
interfaces.requests.IReq_UpdateGatewayClient
|
|
2544
|
+
>('/typedrequest', 'updateGatewayClient');
|
|
2545
|
+
await request.fire({ identity: context.identity!, ...dataArg });
|
|
2546
|
+
return await actionContext!.dispatch(fetchGatewayClientsAction, null);
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
return {
|
|
2549
|
+
...currentState,
|
|
2550
|
+
error: error instanceof Error ? error.message : 'Failed to update gateway client',
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
export const deleteGatewayClientAction = routeManagementStatePart.createAction<string>(
|
|
2556
|
+
async (statePartArg, gatewayClientId, actionContext): Promise<IRouteManagementState> => {
|
|
2557
|
+
const context = getActionContext();
|
|
2558
|
+
const currentState = statePartArg.getState()!;
|
|
2559
|
+
try {
|
|
2560
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2561
|
+
interfaces.requests.IReq_DeleteGatewayClient
|
|
2562
|
+
>('/typedrequest', 'deleteGatewayClient');
|
|
2563
|
+
await request.fire({ identity: context.identity!, id: gatewayClientId });
|
|
2564
|
+
return await actionContext!.dispatch(fetchGatewayClientsAction, null);
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
return {
|
|
2567
|
+
...currentState,
|
|
2568
|
+
error: error instanceof Error ? error.message : 'Failed to delete gateway client',
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
},
|
|
2572
|
+
);
|
|
2573
|
+
|
|
2574
|
+
export async function createGatewayClientToken(
|
|
2575
|
+
gatewayClientId: string,
|
|
2576
|
+
name?: string,
|
|
2577
|
+
expiresInDays?: number | null,
|
|
2578
|
+
) {
|
|
2579
|
+
const context = getActionContext();
|
|
2580
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2581
|
+
interfaces.requests.IReq_CreateGatewayClientToken
|
|
2582
|
+
>('/typedrequest', 'createGatewayClientToken');
|
|
2583
|
+
return request.fire({
|
|
2584
|
+
identity: context.identity!,
|
|
2585
|
+
gatewayClientId,
|
|
2586
|
+
name,
|
|
2587
|
+
expiresInDays,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2480
2591
|
// Users (read-only list)
|
|
2481
2592
|
export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
|
|
2482
2593
|
const context = getActionContext();
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as appstate from '../../appstate.js';
|
|
2
|
+
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
3
|
+
import { viewHostCss } from '../shared/css.js';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DeesElement,
|
|
7
|
+
css,
|
|
8
|
+
cssManager,
|
|
9
|
+
customElement,
|
|
10
|
+
html,
|
|
11
|
+
state,
|
|
12
|
+
type TemplateResult,
|
|
13
|
+
} from '@design.estate/dees-element';
|
|
14
|
+
|
|
15
|
+
@customElement('ops-view-gatewayclients')
|
|
16
|
+
export class OpsViewGatewayClients extends DeesElement {
|
|
17
|
+
@state() accessor routeState: appstate.IRouteManagementState = {
|
|
18
|
+
mergedRoutes: [],
|
|
19
|
+
warnings: [],
|
|
20
|
+
apiTokens: [],
|
|
21
|
+
gatewayClients: [],
|
|
22
|
+
isLoading: false,
|
|
23
|
+
error: null,
|
|
24
|
+
lastUpdated: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
const sub = appstate.routeManagementStatePart
|
|
30
|
+
.select((s) => s)
|
|
31
|
+
.subscribe((routeState) => {
|
|
32
|
+
this.routeState = routeState;
|
|
33
|
+
});
|
|
34
|
+
this.rxSubscriptions.push(sub);
|
|
35
|
+
|
|
36
|
+
const loginSub = appstate.loginStatePart
|
|
37
|
+
.select((s) => s.isLoggedIn)
|
|
38
|
+
.subscribe((isLoggedIn) => {
|
|
39
|
+
if (isLoggedIn) {
|
|
40
|
+
appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.rxSubscriptions.push(loginSub);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static styles = [
|
|
47
|
+
cssManager.defaultStyles,
|
|
48
|
+
viewHostCss,
|
|
49
|
+
css`
|
|
50
|
+
.pill {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
padding: 2px 6px;
|
|
53
|
+
border-radius: 3px;
|
|
54
|
+
font-size: 11px;
|
|
55
|
+
background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.1)', 'rgba(96, 165, 250, 0.14)')};
|
|
56
|
+
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
|
|
57
|
+
margin-right: 4px;
|
|
58
|
+
margin-bottom: 2px;
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
public render(): TemplateResult {
|
|
64
|
+
return html`
|
|
65
|
+
<dees-heading level="3">Gateway Clients</dees-heading>
|
|
66
|
+
<dees-table
|
|
67
|
+
.heading1=${'Gateway Clients'}
|
|
68
|
+
.heading2=${'Create durable clients and token credentials for Onebox, Cloudly, or custom integrations'}
|
|
69
|
+
.data=${this.routeState.gatewayClients}
|
|
70
|
+
.dataName=${'gateway client'}
|
|
71
|
+
.searchable=${true}
|
|
72
|
+
.showColumnFilters=${true}
|
|
73
|
+
.displayFunction=${(client: interfaces.data.IGatewayClient) => ({
|
|
74
|
+
name: client.name,
|
|
75
|
+
id: client.id,
|
|
76
|
+
type: client.type,
|
|
77
|
+
hostnames: this.renderPills(client.hostnamePatterns),
|
|
78
|
+
targets: this.renderTargets(client.allowedRouteTargets),
|
|
79
|
+
tokens: client.tokenCount || 0,
|
|
80
|
+
status: client.enabled ? 'Active' : 'Disabled',
|
|
81
|
+
})}
|
|
82
|
+
.dataActions=${[
|
|
83
|
+
{
|
|
84
|
+
name: 'Create Client',
|
|
85
|
+
iconName: 'lucide:plus',
|
|
86
|
+
type: ['header'],
|
|
87
|
+
actionFunc: async () => await this.showCreateClientDialog(),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Create Token',
|
|
91
|
+
iconName: 'lucide:keyRound',
|
|
92
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
93
|
+
actionFunc: async (actionData: any) => await this.showCreateTokenDialog(actionData.item),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'Enable',
|
|
97
|
+
iconName: 'lucide:play',
|
|
98
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
99
|
+
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
|
100
|
+
actionFunc: async (actionData: any) => {
|
|
101
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.updateGatewayClientAction, {
|
|
102
|
+
id: actionData.item.id,
|
|
103
|
+
enabled: true,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'Disable',
|
|
109
|
+
iconName: 'lucide:pause',
|
|
110
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
111
|
+
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
|
112
|
+
actionFunc: async (actionData: any) => {
|
|
113
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.updateGatewayClientAction, {
|
|
114
|
+
id: actionData.item.id,
|
|
115
|
+
enabled: false,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'Delete',
|
|
121
|
+
iconName: 'lucide:trash2',
|
|
122
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
123
|
+
actionFunc: async (actionData: any) => {
|
|
124
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.deleteGatewayClientAction, actionData.item.id);
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
></dees-table>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private renderPills(values: string[]): TemplateResult {
|
|
133
|
+
if (!values.length) return html`<span>None</span>`;
|
|
134
|
+
return html`${values.map((value) => html`<span class="pill">${value}</span>`)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private renderTargets(targets: interfaces.data.IGatewayClient['allowedRouteTargets']): TemplateResult {
|
|
138
|
+
if (!targets.length) return html`<span>None</span>`;
|
|
139
|
+
return html`${targets.map((target) => html`<span class="pill">${target.host}:${target.ports.join(',')}</span>`)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async showCreateClientDialog(): Promise<void> {
|
|
143
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
144
|
+
await DeesModal.createAndShow({
|
|
145
|
+
heading: 'Create Gateway Client',
|
|
146
|
+
content: html`
|
|
147
|
+
<dees-form>
|
|
148
|
+
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
|
149
|
+
<dees-input-text .key=${'type'} .label=${'Type'} .value=${'onebox'} .description=${'onebox, cloudly, or custom'}></dees-input-text>
|
|
150
|
+
<dees-input-text .key=${'id'} .label=${'Client ID'} .description=${'Optional stable ID; generated when empty'}></dees-input-text>
|
|
151
|
+
<dees-input-text .key=${'hostnamePatterns'} .label=${'Hostname Patterns'} .description=${'Comma separated, e.g. *.apps.example.com'}></dees-input-text>
|
|
152
|
+
<dees-input-text .key=${'allowedRouteTarget'} .label=${'Allowed Route Target'} .description=${'Optional host:ports, e.g. onebox-smartproxy:80'}></dees-input-text>
|
|
153
|
+
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
|
154
|
+
</dees-form>
|
|
155
|
+
`,
|
|
156
|
+
menuOptions: [
|
|
157
|
+
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
|
|
158
|
+
{
|
|
159
|
+
name: 'Create',
|
|
160
|
+
iconName: 'lucide:plus',
|
|
161
|
+
action: async (modalArg: any) => {
|
|
162
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
163
|
+
if (!form) return;
|
|
164
|
+
const formData = await (form as any).collectFormData();
|
|
165
|
+
const name = String(formData.name || '').trim();
|
|
166
|
+
if (!name) return;
|
|
167
|
+
await modalArg.destroy();
|
|
168
|
+
await appstate.createGatewayClient({
|
|
169
|
+
id: String(formData.id || '').trim() || undefined,
|
|
170
|
+
type: this.normalizeClientType(String(formData.type || 'onebox')),
|
|
171
|
+
name,
|
|
172
|
+
description: String(formData.description || '').trim() || undefined,
|
|
173
|
+
hostnamePatterns: this.parseList(String(formData.hostnamePatterns || '')),
|
|
174
|
+
allowedRouteTargets: this.parseAllowedRouteTargets(String(formData.allowedRouteTarget || '')),
|
|
175
|
+
});
|
|
176
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async showCreateTokenDialog(client: interfaces.data.IGatewayClient): Promise<void> {
|
|
184
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
185
|
+
await DeesModal.createAndShow({
|
|
186
|
+
heading: `Create Token for ${client.name}`,
|
|
187
|
+
content: html`
|
|
188
|
+
<div style="color: #888; margin-bottom: 12px; font-size: 13px;">
|
|
189
|
+
The token will be shown once. Configure Onebox with the dcrouter URL and this token.
|
|
190
|
+
</div>
|
|
191
|
+
<dees-form>
|
|
192
|
+
<dees-input-text .key=${'name'} .label=${'Token Name'} .value=${`${client.name} Token`}></dees-input-text>
|
|
193
|
+
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in'} .description=${'Number of days; leave blank for no expiration'}></dees-input-text>
|
|
194
|
+
</dees-form>
|
|
195
|
+
`,
|
|
196
|
+
menuOptions: [
|
|
197
|
+
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
|
|
198
|
+
{
|
|
199
|
+
name: 'Create Token',
|
|
200
|
+
iconName: 'lucide:key',
|
|
201
|
+
action: async (modalArg: any) => {
|
|
202
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
203
|
+
if (!form) return;
|
|
204
|
+
const formData = await (form as any).collectFormData();
|
|
205
|
+
const expiresInDays = formData.expiresInDays ? parseInt(formData.expiresInDays, 10) : null;
|
|
206
|
+
await modalArg.destroy();
|
|
207
|
+
const response = await appstate.createGatewayClientToken(
|
|
208
|
+
client.id,
|
|
209
|
+
String(formData.name || '').trim() || undefined,
|
|
210
|
+
expiresInDays,
|
|
211
|
+
);
|
|
212
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null);
|
|
213
|
+
if (response.success && response.tokenValue) {
|
|
214
|
+
await DeesModal.createAndShow({
|
|
215
|
+
heading: 'Gateway Client Token Created',
|
|
216
|
+
content: html`
|
|
217
|
+
<p>Copy this token now. It will not be shown again.</p>
|
|
218
|
+
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
|
|
219
|
+
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
|
|
220
|
+
</div>
|
|
221
|
+
`,
|
|
222
|
+
menuOptions: [
|
|
223
|
+
{ name: 'Done', iconName: 'lucide:check', action: async (m: any) => await m.destroy() },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private normalizeClientType(value: string): interfaces.data.IGatewayClient['type'] {
|
|
234
|
+
const normalized = value.trim().toLowerCase();
|
|
235
|
+
if (normalized === 'cloudly' || normalized === 'custom') return normalized;
|
|
236
|
+
return 'onebox';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private parseList(value: string): string[] {
|
|
240
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private parseAllowedRouteTargets(value: string): interfaces.data.IGatewayClient['allowedRouteTargets'] {
|
|
244
|
+
const target = value.trim();
|
|
245
|
+
if (!target.includes(':')) return [];
|
|
246
|
+
const [host, portsValue] = target.split(':');
|
|
247
|
+
const ports = portsValue.split(',').map((port) => Number(port.trim())).filter((port) => Number.isInteger(port));
|
|
248
|
+
return host.trim() && ports.length ? [{ host: host.trim(), ports }] : [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -11,6 +11,7 @@ import * as appstate from '../../appstate.js';
|
|
|
11
11
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
12
12
|
import { viewHostCss } from '../shared/css.js';
|
|
13
13
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
14
|
+
import { appRouter } from '../../router.js';
|
|
14
15
|
|
|
15
16
|
declare global {
|
|
16
17
|
interface HTMLElementTagNameMap {
|
|
@@ -26,6 +27,9 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
26
27
|
@state()
|
|
27
28
|
accessor acmeState: appstate.IAcmeConfigState = appstate.acmeConfigStatePart.getState()!;
|
|
28
29
|
|
|
30
|
+
@state()
|
|
31
|
+
accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
|
|
32
|
+
|
|
29
33
|
constructor() {
|
|
30
34
|
super();
|
|
31
35
|
const certSub = appstate.certificateStatePart.select().subscribe((newState) => {
|
|
@@ -36,12 +40,19 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
36
40
|
this.acmeState = newState;
|
|
37
41
|
});
|
|
38
42
|
this.rxSubscriptions.push(acmeSub);
|
|
43
|
+
const domainsSub = appstate.domainsStatePart.select().subscribe((newState) => {
|
|
44
|
+
this.domainsState = newState;
|
|
45
|
+
});
|
|
46
|
+
this.rxSubscriptions.push(domainsSub);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
async connectedCallback() {
|
|
42
50
|
await super.connectedCallback();
|
|
43
|
-
await
|
|
44
|
-
|
|
51
|
+
await Promise.all([
|
|
52
|
+
appstate.certificateStatePart.dispatchAction(appstate.fetchCertificateOverviewAction, null),
|
|
53
|
+
appstate.acmeConfigStatePart.dispatchAction(appstate.fetchAcmeConfigAction, null),
|
|
54
|
+
appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null),
|
|
55
|
+
]);
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
public static styles = [
|
|
@@ -127,10 +138,16 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
127
138
|
.errorText {
|
|
128
139
|
font-size: 12px;
|
|
129
140
|
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
|
130
|
-
max-width:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
max-width: 420px;
|
|
142
|
+
line-height: 1.35;
|
|
143
|
+
white-space: normal;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.errorStack {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
align-items: flex-start;
|
|
150
|
+
gap: 4px;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
.backoffIndicator {
|
|
@@ -160,6 +177,39 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
160
177
|
.expiryInfo .daysLeft.danger {
|
|
161
178
|
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
|
162
179
|
}
|
|
180
|
+
|
|
181
|
+
.dnsWarningPanel {
|
|
182
|
+
border: 1px solid ${cssManager.bdTheme('#fed7aa', '#7c2d12')};
|
|
183
|
+
border-radius: 12px;
|
|
184
|
+
padding: 16px;
|
|
185
|
+
background: ${cssManager.bdTheme('#fff7ed', '#1c1917')};
|
|
186
|
+
color: ${cssManager.bdTheme('#7c2d12', '#fdba74')};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.dnsWarningTitle {
|
|
190
|
+
font-weight: 700;
|
|
191
|
+
margin-bottom: 6px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.dnsWarningText {
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
line-height: 1.45;
|
|
197
|
+
color: ${cssManager.bdTheme('#9a3412', '#fed7aa')};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.dnsWarningList {
|
|
201
|
+
margin: 12px 0 0;
|
|
202
|
+
padding-left: 18px;
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
line-height: 1.5;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.dnsWarningActions {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-wrap: wrap;
|
|
210
|
+
gap: 8px;
|
|
211
|
+
margin-top: 14px;
|
|
212
|
+
}
|
|
163
213
|
`,
|
|
164
214
|
];
|
|
165
215
|
|
|
@@ -172,11 +222,102 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
172
222
|
<div class="certificatesContainer">
|
|
173
223
|
${this.renderStatsTiles(summary)}
|
|
174
224
|
${this.renderAcmeSettingsTile()}
|
|
225
|
+
${this.renderManagedDomainWarnings()}
|
|
175
226
|
${this.renderCertificateTable()}
|
|
176
227
|
</div>
|
|
177
228
|
`;
|
|
178
229
|
}
|
|
179
230
|
|
|
231
|
+
private renderManagedDomainWarnings(): TemplateResult {
|
|
232
|
+
const issues = this.getMissingManagedDomainIssues();
|
|
233
|
+
if (issues.length === 0) {
|
|
234
|
+
return html``;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const shownIssues = issues.slice(0, 6);
|
|
238
|
+
const remaining = issues.length - shownIssues.length;
|
|
239
|
+
|
|
240
|
+
return html`
|
|
241
|
+
<div class="dnsWarningPanel">
|
|
242
|
+
<div class="dnsWarningTitle">DNS-01 certificate provisioning needs managed DNS domains</div>
|
|
243
|
+
<div class="dnsWarningText">
|
|
244
|
+
DcRouter can only create ACME TXT records for domains listed under Domains > Domains.
|
|
245
|
+
Add the zone directly or import it from a DNS provider before reprovisioning certificates.
|
|
246
|
+
</div>
|
|
247
|
+
<ul class="dnsWarningList">
|
|
248
|
+
${shownIssues.map((issue) => html`
|
|
249
|
+
<li>
|
|
250
|
+
<strong>${issue.domain}</strong>: no managed DNS domain covers
|
|
251
|
+
<code>${issue.challengeHost}</code>. Add/import <code>${issue.requiredDomain}</code>
|
|
252
|
+
or a parent zone.
|
|
253
|
+
</li>
|
|
254
|
+
`)}
|
|
255
|
+
${remaining > 0 ? html`<li>${remaining} more domain${remaining === 1 ? '' : 's'} need managed DNS.</li>` : ''}
|
|
256
|
+
</ul>
|
|
257
|
+
<div class="dnsWarningActions">
|
|
258
|
+
<dees-button @click=${() => appRouter.navigateToView('domains', 'domains')}>Manage Domains</dees-button>
|
|
259
|
+
<dees-button @click=${() => appRouter.navigateToView('domains', 'providers')}>DNS Providers</dees-button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private getMissingManagedDomainIssues(): Array<{
|
|
266
|
+
domain: string;
|
|
267
|
+
challengeHost: string;
|
|
268
|
+
requiredDomain: string;
|
|
269
|
+
}> {
|
|
270
|
+
const managedDomains = this.domainsState.domains
|
|
271
|
+
.map((domain) => this.normalizeDomain(domain.name))
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
const issues: Array<{ domain: string; challengeHost: string; requiredDomain: string }> = [];
|
|
274
|
+
const seen = new Set<string>();
|
|
275
|
+
|
|
276
|
+
for (const cert of this.certState.certificates) {
|
|
277
|
+
if (!cert.canReprovision || (cert.source !== 'acme' && cert.source !== 'provision-function')) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const requiredDomain = this.getAcmeChallengeDomain(cert.domain);
|
|
282
|
+
if (!requiredDomain) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const covered = managedDomains.some((managedDomain) =>
|
|
287
|
+
requiredDomain === managedDomain || requiredDomain.endsWith(`.${managedDomain}`),
|
|
288
|
+
);
|
|
289
|
+
if (covered) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const key = `${cert.domain}:${requiredDomain}`;
|
|
294
|
+
if (seen.has(key)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
seen.add(key);
|
|
298
|
+
issues.push({
|
|
299
|
+
domain: cert.domain,
|
|
300
|
+
challengeHost: `_acme-challenge.${requiredDomain}`,
|
|
301
|
+
requiredDomain,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return issues;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private getAcmeChallengeDomain(domain: string): string {
|
|
309
|
+
const normalized = this.normalizeDomain(domain).replace(/^\*\.?/, '');
|
|
310
|
+
const parts = normalized.split('.').filter(Boolean);
|
|
311
|
+
if (parts.length >= 2 && parts.length <= 3) {
|
|
312
|
+
return parts.slice(-2).join('.');
|
|
313
|
+
}
|
|
314
|
+
return normalized;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private normalizeDomain(domain: string): string {
|
|
318
|
+
return domain.trim().toLowerCase().replace(/^\*\.?/, '').replace(/\.$/, '');
|
|
319
|
+
}
|
|
320
|
+
|
|
180
321
|
private renderAcmeSettingsTile(): TemplateResult {
|
|
181
322
|
const config = this.acmeState.config;
|
|
182
323
|
|
|
@@ -349,11 +490,7 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
349
490
|
Status: this.renderStatusBadge(cert.status),
|
|
350
491
|
Source: this.renderSourceBadge(cert.source),
|
|
351
492
|
Expires: this.renderExpiry(cert.expiryDate),
|
|
352
|
-
Error: cert
|
|
353
|
-
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
|
|
354
|
-
: cert.error
|
|
355
|
-
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
|
356
|
-
: '',
|
|
493
|
+
Error: this.renderError(cert),
|
|
357
494
|
})}
|
|
358
495
|
.dataActions=${[
|
|
359
496
|
{
|
|
@@ -632,6 +769,24 @@ export class OpsViewCertificates extends DeesElement {
|
|
|
632
769
|
`;
|
|
633
770
|
}
|
|
634
771
|
|
|
772
|
+
private renderError(cert: interfaces.requests.ICertificateInfo): TemplateResult | string {
|
|
773
|
+
if (cert.backoffInfo) {
|
|
774
|
+
const message = cert.backoffInfo.lastError || cert.error;
|
|
775
|
+
return html`
|
|
776
|
+
<span class="errorStack">
|
|
777
|
+
${message ? html`<span class="errorText" title=${message}>${message}</span>` : ''}
|
|
778
|
+
<span class="backoffIndicator">
|
|
779
|
+
${cert.backoffInfo.failures} failure${cert.backoffInfo.failures === 1 ? '' : 's'}, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}
|
|
780
|
+
</span>
|
|
781
|
+
</span>
|
|
782
|
+
`;
|
|
783
|
+
}
|
|
784
|
+
if (cert.error) {
|
|
785
|
+
return html`<span class="errorText" title=${cert.error}>${cert.error}</span>`;
|
|
786
|
+
}
|
|
787
|
+
return '';
|
|
788
|
+
}
|
|
789
|
+
|
|
635
790
|
private formatRetryTime(retryAfter?: string): string {
|
|
636
791
|
if (!retryAfter) return 'soon';
|
|
637
792
|
const retryDate = new Date(retryAfter);
|
|
@@ -35,6 +35,7 @@ import { OpsViewEmailSecurity } from './email/ops-view-email-security.js';
|
|
|
35
35
|
import { OpsViewEmailDomains } from './email/ops-view-email-domains.js';
|
|
36
36
|
|
|
37
37
|
// Access group
|
|
38
|
+
import { OpsViewGatewayClients } from './access/ops-view-gatewayclients.js';
|
|
38
39
|
import { OpsViewApiTokens } from './access/ops-view-apitokens.js';
|
|
39
40
|
import { OpsViewUsers } from './access/ops-view-users.js';
|
|
40
41
|
|
|
@@ -121,6 +122,7 @@ export class OpsDashboard extends DeesElement {
|
|
|
121
122
|
name: 'Access',
|
|
122
123
|
iconName: 'lucide:keyRound',
|
|
123
124
|
subViews: [
|
|
125
|
+
{ slug: 'gatewayclients', name: 'Gateway Clients', iconName: 'lucide:plugZap', element: OpsViewGatewayClients },
|
|
124
126
|
{ slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens },
|
|
125
127
|
{ slug: 'users', name: 'Users', iconName: 'lucide:users', element: OpsViewUsers },
|
|
126
128
|
],
|
package/ts_web/router.ts
CHANGED
|
@@ -11,7 +11,7 @@ const subviewMap: Record<string, readonly string[]> = {
|
|
|
11
11
|
overview: ['stats', 'configuration'] as const,
|
|
12
12
|
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
|
|
13
13
|
email: ['log', 'security', 'domains'] as const,
|
|
14
|
-
access: ['apitokens', 'users'] as const,
|
|
14
|
+
access: ['gatewayclients', 'apitokens', 'users'] as const,
|
|
15
15
|
security: ['overview', 'blocked', 'authentication'] as const,
|
|
16
16
|
domains: ['providers', 'domains', 'dns', 'certificates'] as const,
|
|
17
17
|
};
|
|
@@ -21,7 +21,7 @@ const defaultSubview: Record<string, string> = {
|
|
|
21
21
|
overview: 'stats',
|
|
22
22
|
network: 'activity',
|
|
23
23
|
email: 'log',
|
|
24
|
-
access: '
|
|
24
|
+
access: 'gatewayclients',
|
|
25
25
|
security: 'overview',
|
|
26
26
|
domains: 'domains',
|
|
27
27
|
};
|