@serve.zone/dcrouter 13.27.1 → 13.29.1
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/.smartconfig.json +32 -10
- package/dist_serve/bundle.js +930 -799
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -1
- package/dist_ts/classes.dcrouter.js +22 -7
- 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/classes.route-config-manager.js +8 -7
- 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/classes.opsserver.js +4 -1
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +21 -6
- package/dist_ts/opsserver/handlers/admin.handler.js +188 -29
- package/dist_ts/opsserver/handlers/certificate.handler.js +5 -1
- package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
- package/dist_ts/opsserver/handlers/users.handler.js +2 -2
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +4 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +146 -16
- package/dist_ts/plugins.d.ts +2 -0
- package/dist_ts/plugins.js +4 -1
- package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -0
- package/dist_ts/vpn/classes.vpn-manager.js +41 -20
- 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/admin.d.ts +38 -0
- package/dist_ts_interfaces/requests/users.d.ts +2 -5
- 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 +46 -0
- package/dist_ts_web/appstate.js +105 -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.d.ts +4 -0
- package/dist_ts_web/elements/ops-dashboard.js +102 -3
- package/dist_ts_web/router.js +3 -3
- package/package.json +15 -22
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +30 -6
- package/ts/config/classes.gateway-client-manager.ts +117 -0
- package/ts/config/classes.route-config-manager.ts +8 -6
- 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/classes.opsserver.ts +3 -0
- package/ts/opsserver/handlers/admin.handler.ts +244 -32
- package/ts/opsserver/handlers/certificate.handler.ts +5 -0
- package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
- package/ts/opsserver/handlers/users.handler.ts +1 -1
- package/ts/opsserver/handlers/workhoster.handler.ts +191 -17
- package/ts/plugins.ts +7 -0
- package/ts/vpn/classes.vpn-manager.ts +56 -25
- package/ts_apiclient/classes.workhoster.ts +8 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +160 -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 +102 -0
- package/ts_web/router.ts +2 -2
|
@@ -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
|
|
|
@@ -65,6 +66,9 @@ export class OpsDashboard extends DeesElement {
|
|
|
65
66
|
isLoggedIn: false,
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
private bootstrapStepper?: any;
|
|
70
|
+
private bootstrapCheckPromise?: Promise<void>;
|
|
71
|
+
|
|
68
72
|
@state() accessor uiState: appstate.IUiState = {
|
|
69
73
|
activeView: 'overview',
|
|
70
74
|
activeSubview: null,
|
|
@@ -121,6 +125,7 @@ export class OpsDashboard extends DeesElement {
|
|
|
121
125
|
name: 'Access',
|
|
122
126
|
iconName: 'lucide:keyRound',
|
|
123
127
|
subViews: [
|
|
128
|
+
{ slug: 'gatewayclients', name: 'Gateway Clients', iconName: 'lucide:plugZap', element: OpsViewGatewayClients },
|
|
124
129
|
{ slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens },
|
|
125
130
|
{ slug: 'users', name: 'Users', iconName: 'lucide:users', element: OpsViewUsers },
|
|
126
131
|
],
|
|
@@ -334,6 +339,7 @@ export class OpsDashboard extends DeesElement {
|
|
|
334
339
|
await (simpleLogin as any).switchToSlottedContent();
|
|
335
340
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
|
336
341
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
|
342
|
+
await this.ensureAdminBootstrap();
|
|
337
343
|
} else {
|
|
338
344
|
// Server rejected the JWT — clear state, show login
|
|
339
345
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
|
@@ -368,10 +374,106 @@ export class OpsDashboard extends DeesElement {
|
|
|
368
374
|
await simpleLogin!.switchToSlottedContent();
|
|
369
375
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
|
370
376
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
|
377
|
+
await this.ensureAdminBootstrap();
|
|
371
378
|
} else {
|
|
372
379
|
form!.setStatus('error', 'Login failed!');
|
|
373
380
|
await domtools.convenience.smartdelay.delayFor(2000);
|
|
374
381
|
form!.reset();
|
|
375
382
|
}
|
|
376
383
|
}
|
|
384
|
+
|
|
385
|
+
private async ensureAdminBootstrap(): Promise<void> {
|
|
386
|
+
if (!this.loginState.identity || this.bootstrapStepper?.isConnected) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (this.bootstrapCheckPromise) {
|
|
390
|
+
return this.bootstrapCheckPromise;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.bootstrapCheckPromise = (async () => {
|
|
394
|
+
try {
|
|
395
|
+
const status = await appstate.getAdminBootstrapStatus();
|
|
396
|
+
if (status.needsBootstrap) {
|
|
397
|
+
await this.showAdminBootstrapStepper(status);
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('Admin bootstrap status check failed:', error);
|
|
401
|
+
} finally {
|
|
402
|
+
this.bootstrapCheckPromise = undefined;
|
|
403
|
+
}
|
|
404
|
+
})();
|
|
405
|
+
|
|
406
|
+
return this.bootstrapCheckPromise;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async showAdminBootstrapStepper(statusArg: appstate.IAdminBootstrapStatus): Promise<void> {
|
|
410
|
+
const { DeesStepper } = await import('@design.estate/dees-catalog');
|
|
411
|
+
this.bootstrapStepper = await DeesStepper.createAndShow({
|
|
412
|
+
cancelable: false,
|
|
413
|
+
steps: [
|
|
414
|
+
{
|
|
415
|
+
title: 'Create Persisted Admin',
|
|
416
|
+
content: html`
|
|
417
|
+
<div style="display: grid; gap: 16px; color: var(--dees-color-text-secondary); font-size: 14px; line-height: 1.5;">
|
|
418
|
+
<p style="margin: 0;">
|
|
419
|
+
This router is currently using the temporary bootstrap admin. Create the first persisted admin account to continue.
|
|
420
|
+
</p>
|
|
421
|
+
<dees-form>
|
|
422
|
+
<dees-input-text .key=${'email'} .label=${'Admin email'} .required=${true}></dees-input-text>
|
|
423
|
+
<dees-input-text .key=${'name'} .label=${'Display name'}></dees-input-text>
|
|
424
|
+
<dees-input-text .key=${'password'} .label=${'Password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
|
|
425
|
+
<dees-input-text .key=${'passwordConfirm'} .label=${'Confirm password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
|
|
426
|
+
<dees-input-checkbox
|
|
427
|
+
.key=${'enableIdpGlobalAuth'}
|
|
428
|
+
.label=${'Allow idp.global login for this email'}
|
|
429
|
+
.description=${statusArg.idpGlobalConfigured
|
|
430
|
+
? 'The local account remains authoritative; idp.global only verifies identity.'
|
|
431
|
+
: 'Requires DCROUTER_IDP_GLOBAL_URL before idp.global logins can work.'}
|
|
432
|
+
></dees-input-checkbox>
|
|
433
|
+
</dees-form>
|
|
434
|
+
</div>
|
|
435
|
+
`,
|
|
436
|
+
menuOptions: [
|
|
437
|
+
{
|
|
438
|
+
name: 'Create admin',
|
|
439
|
+
action: async (stepperArg: any) => {
|
|
440
|
+
const form = stepperArg.shadowRoot?.querySelector('.selected dees-form') as any;
|
|
441
|
+
if (!form) return;
|
|
442
|
+
const formData = await form.collectFormData();
|
|
443
|
+
const email = String(formData.email || '').trim();
|
|
444
|
+
const name = String(formData.name || '').trim();
|
|
445
|
+
const password = String(formData.password || '');
|
|
446
|
+
const passwordConfirm = String(formData.passwordConfirm || '');
|
|
447
|
+
|
|
448
|
+
if (!email || !password) {
|
|
449
|
+
form.setStatus?.('error', 'Email and password are required.');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (password !== passwordConfirm) {
|
|
453
|
+
form.setStatus?.('error', 'Passwords do not match.');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
form.setStatus?.('pending', 'Creating persisted admin...');
|
|
459
|
+
await appstate.createInitialAdminUser({
|
|
460
|
+
email,
|
|
461
|
+
name,
|
|
462
|
+
password,
|
|
463
|
+
enableIdpGlobalAuth: Boolean(formData.enableIdpGlobalAuth),
|
|
464
|
+
});
|
|
465
|
+
form.setStatus?.('success', 'Persisted admin created.');
|
|
466
|
+
await stepperArg.destroy();
|
|
467
|
+
this.bootstrapStepper = undefined;
|
|
468
|
+
await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
form.setStatus?.('error', error instanceof Error ? error.message : 'Failed to create admin.');
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
}
|
|
377
479
|
}
|
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
|
};
|