@serve.zone/dcrouter 16.2.1 → 16.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/deno.json +1 -1
- package/dist_serve/bundle.js +1190 -1100
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/helpers.http-redirects.d.ts +1 -1
- package/dist_ts/config/helpers.http-redirects.js +4 -3
- package/dist_ts_interfaces/data/route-management.d.ts +2 -0
- package/dist_ts_interfaces/data/route-management.js +6 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/access/ops-view-apitokens.js +3 -3
- package/dist_ts_web/elements/access/ops-view-gatewayclients.js +3 -3
- package/dist_ts_web/elements/network/ops-view-network-activity.js +6 -6
- package/dist_ts_web/elements/network/ops-view-redirects.d.ts +3 -0
- package/dist_ts_web/elements/network/ops-view-redirects.js +38 -21
- package/dist_ts_web/elements/network/ops-view-remoteingress.js +3 -3
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +8 -7
- package/dist_ts_web/elements/network/ops-view-routes.js +331 -161
- package/dist_ts_web/elements/network/ops-view-special-forwards.d.ts +4 -1
- package/dist_ts_web/elements/network/ops-view-special-forwards.js +118 -168
- package/dist_ts_web/elements/network/ops-view-vpn.js +3 -3
- package/dist_ts_web/elements/network/route-view-filters.d.ts +6 -0
- package/dist_ts_web/elements/network/route-view-filters.js +29 -0
- package/dist_ts_web/elements/network/special-forward-dialog.d.ts +11 -0
- package/dist_ts_web/elements/network/special-forward-dialog.js +154 -0
- package/dist_ts_web/elements/security/ops-view-security-blocked.js +6 -6
- package/package.json +2 -2
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/helpers.http-redirects.ts +6 -3
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/access/ops-view-apitokens.ts +2 -2
- package/ts_web/elements/access/ops-view-gatewayclients.ts +2 -2
- package/ts_web/elements/network/ops-view-network-activity.ts +5 -5
- package/ts_web/elements/network/ops-view-redirects.ts +34 -19
- package/ts_web/elements/network/ops-view-remoteingress.ts +2 -2
- package/ts_web/elements/network/ops-view-routes.ts +371 -171
- package/ts_web/elements/network/ops-view-special-forwards.ts +115 -178
- package/ts_web/elements/network/ops-view-vpn.ts +2 -2
- package/ts_web/elements/network/route-view-filters.ts +43 -0
- package/ts_web/elements/network/special-forward-dialog.ts +174 -0
- package/ts_web/elements/security/ops-view-security-blocked.ts +5 -5
- package/ts_web/readme.md +4 -4
|
@@ -4,42 +4,34 @@ import {
|
|
|
4
4
|
cssManager,
|
|
5
5
|
customElement,
|
|
6
6
|
html,
|
|
7
|
+
property,
|
|
7
8
|
state,
|
|
8
9
|
type TemplateResult,
|
|
9
10
|
} from '@design.estate/dees-element';
|
|
11
|
+
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
10
12
|
import * as appstate from '../../appstate.js';
|
|
11
13
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
12
14
|
import { viewHostCss } from '../shared/css.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function getDomains(route: interfaces.data.IMergedRoute): string[] {
|
|
25
|
-
const domains = route.route.match.domains;
|
|
26
|
-
if (!domains) return [];
|
|
27
|
-
return Array.isArray(domains) ? domains : [domains];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function getTarget(route: interfaces.data.IMergedRoute): { host: string; port: number } | undefined {
|
|
31
|
-
const target = route.route.action.targets?.[0];
|
|
32
|
-
if (!target || typeof target.port !== 'number') return undefined;
|
|
33
|
-
const host = typeof target.host === 'string'
|
|
34
|
-
? target.host
|
|
35
|
-
: Array.isArray(target.host)
|
|
36
|
-
? target.host[0]
|
|
37
|
-
: undefined;
|
|
38
|
-
return host ? { host, port: target.port } : undefined;
|
|
39
|
-
}
|
|
15
|
+
import {
|
|
16
|
+
getRouteOwner,
|
|
17
|
+
routeMatchesOwner,
|
|
18
|
+
type TRouteOwnerFilter,
|
|
19
|
+
} from './route-view-filters.js';
|
|
20
|
+
import {
|
|
21
|
+
getSpecialForwardDomains,
|
|
22
|
+
getSpecialForwardTarget,
|
|
23
|
+
letsEncryptHttp01ManagedRouteKind,
|
|
24
|
+
showLetsEncryptHttp01ForwardDialog,
|
|
25
|
+
} from './special-forward-dialog.js';
|
|
40
26
|
|
|
41
27
|
@customElement('ops-view-special-forwards')
|
|
42
28
|
export class OpsViewSpecialForwards extends DeesElement {
|
|
29
|
+
@property({ type: Boolean, reflect: true })
|
|
30
|
+
accessor embedded = false;
|
|
31
|
+
|
|
32
|
+
@property({ type: String })
|
|
33
|
+
accessor ownerFilter: TRouteOwnerFilter = 'all';
|
|
34
|
+
|
|
43
35
|
@state()
|
|
44
36
|
accessor routeState: appstate.IRouteManagementState = appstate.routeManagementStatePart.getState()!;
|
|
45
37
|
|
|
@@ -67,6 +59,12 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
67
59
|
display: block;
|
|
68
60
|
}
|
|
69
61
|
|
|
62
|
+
:host([embedded]) {
|
|
63
|
+
margin: 0;
|
|
64
|
+
max-width: none;
|
|
65
|
+
padding: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
70
68
|
.specialForwardsContainer {
|
|
71
69
|
display: flex;
|
|
72
70
|
flex-direction: column;
|
|
@@ -97,12 +95,62 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
97
95
|
|
|
98
96
|
public render(): TemplateResult {
|
|
99
97
|
const forwards = this.routeState.mergedRoutes.filter((route) => {
|
|
100
|
-
return route.metadata?.managedRouteKind ===
|
|
101
|
-
&& route.
|
|
98
|
+
return route.metadata?.managedRouteKind === letsEncryptHttp01ManagedRouteKind
|
|
99
|
+
&& routeMatchesOwner(route, this.ownerFilter);
|
|
102
100
|
});
|
|
101
|
+
const enabledCount = forwards.filter((route) => route.enabled).length;
|
|
102
|
+
const disabledCount = forwards.length - enabledCount;
|
|
103
|
+
const readOnlyCount = forwards.filter((route) => getRouteOwner(route) !== 'operator').length;
|
|
104
|
+
const statsTiles: IStatsTile[] = [
|
|
105
|
+
{
|
|
106
|
+
id: 'matchingSpecialForwards',
|
|
107
|
+
title: 'Matching',
|
|
108
|
+
type: 'number',
|
|
109
|
+
value: forwards.length,
|
|
110
|
+
icon: 'lucide:route',
|
|
111
|
+
description: 'HTTP-01 forwards in this owner scope',
|
|
112
|
+
color: '#3b82f6',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'enabledSpecialForwards',
|
|
116
|
+
title: 'Enabled',
|
|
117
|
+
type: 'number',
|
|
118
|
+
value: enabledCount,
|
|
119
|
+
icon: 'lucide:circleCheck',
|
|
120
|
+
description: 'Currently forwarding challenges',
|
|
121
|
+
color: '#22c55e',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'disabledSpecialForwards',
|
|
125
|
+
title: 'Disabled',
|
|
126
|
+
type: 'number',
|
|
127
|
+
value: disabledCount,
|
|
128
|
+
icon: 'lucide:circlePause',
|
|
129
|
+
description: 'Configured but inactive',
|
|
130
|
+
color: disabledCount > 0 ? '#f59e0b' : '#6b7280',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'readOnlySpecialForwards',
|
|
134
|
+
title: 'Read-only',
|
|
135
|
+
type: 'number',
|
|
136
|
+
value: readOnlyCount,
|
|
137
|
+
icon: 'lucide:lockKeyhole',
|
|
138
|
+
description: 'Managed by gateway clients or system',
|
|
139
|
+
color: '#8b5cf6',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
103
142
|
|
|
104
143
|
return html`
|
|
144
|
+
${this.embedded ? '' : html`<dees-heading level="3">Special Forwards</dees-heading>`}
|
|
105
145
|
<div class="specialForwardsContainer">
|
|
146
|
+
${this.embedded
|
|
147
|
+
? ''
|
|
148
|
+
: html`
|
|
149
|
+
<dees-statsgrid
|
|
150
|
+
.tiles=${statsTiles}
|
|
151
|
+
.gridActions=${this.getHeaderActions()}
|
|
152
|
+
></dees-statsgrid>
|
|
153
|
+
`}
|
|
106
154
|
<div class="explanation">
|
|
107
155
|
<strong>Let's Encrypt HTTP-01 Forward</strong> exposes only
|
|
108
156
|
<code>/.well-known/acme-challenge/*</code> on public port 80. DCRouter manages the
|
|
@@ -119,12 +167,18 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
119
167
|
.data=${forwards}
|
|
120
168
|
.showColumnFilters=${true}
|
|
121
169
|
.displayFunction=${(route: interfaces.data.IMergedRoute) => {
|
|
122
|
-
const target =
|
|
170
|
+
const target = getSpecialForwardTarget(route);
|
|
171
|
+
const owner = getRouteOwner(route);
|
|
123
172
|
return {
|
|
124
173
|
Status: route.enabled ? 'Enabled' : 'Disabled',
|
|
125
174
|
Type: 'Let\'s Encrypt HTTP-01',
|
|
175
|
+
Owner: owner === 'operator'
|
|
176
|
+
? 'User'
|
|
177
|
+
: owner === 'gatewayClient'
|
|
178
|
+
? 'Gateway Client'
|
|
179
|
+
: 'System',
|
|
126
180
|
Name: route.route.name || route.id,
|
|
127
|
-
Domains:
|
|
181
|
+
Domains: getSpecialForwardDomains(route).join(', '),
|
|
128
182
|
Path: route.route.match.path || '-',
|
|
129
183
|
Target: route.metadata?.networkTargetName
|
|
130
184
|
|| (target ? `${target.host}:${target.port}` : '-'),
|
|
@@ -132,30 +186,27 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
132
186
|
};
|
|
133
187
|
}}
|
|
134
188
|
.dataActions=${[
|
|
135
|
-
{
|
|
136
|
-
name: 'Add HTTP-01 Forward',
|
|
137
|
-
iconName: 'lucide:plus',
|
|
138
|
-
type: ['header' as const],
|
|
139
|
-
actionFunc: async () => this.showForwardDialog(),
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: 'Refresh',
|
|
143
|
-
iconName: 'lucide:refreshCw',
|
|
144
|
-
type: ['header' as const],
|
|
145
|
-
actionFunc: async () => this.refreshData(),
|
|
146
|
-
},
|
|
147
189
|
{
|
|
148
190
|
name: 'Edit',
|
|
149
191
|
iconName: 'lucide:pencil',
|
|
150
192
|
type: ['inRow', 'contextmenu'] as any,
|
|
193
|
+
actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => {
|
|
194
|
+
return getRouteOwner(route) === 'operator';
|
|
195
|
+
},
|
|
151
196
|
actionFunc: async (actionData: any) => {
|
|
152
|
-
await
|
|
197
|
+
await showLetsEncryptHttp01ForwardDialog({
|
|
198
|
+
existing: actionData.item as interfaces.data.IMergedRoute,
|
|
199
|
+
targets: this.profilesTargetsState.targets,
|
|
200
|
+
});
|
|
153
201
|
},
|
|
154
202
|
},
|
|
155
203
|
{
|
|
156
204
|
name: 'Enable / Disable',
|
|
157
205
|
iconName: 'lucide:power',
|
|
158
206
|
type: ['inRow', 'contextmenu'] as any,
|
|
207
|
+
actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => {
|
|
208
|
+
return getRouteOwner(route) === 'operator';
|
|
209
|
+
},
|
|
159
210
|
actionFunc: async (actionData: any) => {
|
|
160
211
|
const route = actionData.item as interfaces.data.IMergedRoute;
|
|
161
212
|
await appstate.routeManagementStatePart.dispatchAction(
|
|
@@ -168,6 +219,9 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
168
219
|
name: 'Delete',
|
|
169
220
|
iconName: 'lucide:trash2',
|
|
170
221
|
type: ['inRow', 'contextmenu'] as any,
|
|
222
|
+
actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => {
|
|
223
|
+
return getRouteOwner(route) === 'operator';
|
|
224
|
+
},
|
|
171
225
|
actionFunc: async (actionData: any) => {
|
|
172
226
|
await this.confirmDelete(actionData.item as interfaces.data.IMergedRoute);
|
|
173
227
|
},
|
|
@@ -188,147 +242,30 @@ export class OpsViewSpecialForwards extends DeesElement {
|
|
|
188
242
|
]);
|
|
189
243
|
}
|
|
190
244
|
|
|
191
|
-
private
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
245
|
+
private getHeaderActions() {
|
|
246
|
+
return [
|
|
247
|
+
...(this.ownerFilter === 'all' || this.ownerFilter === 'operator'
|
|
248
|
+
? [{
|
|
249
|
+
name: 'Add User Special Forward',
|
|
250
|
+
iconName: 'lucide:plus',
|
|
251
|
+
action: async () => showLetsEncryptHttp01ForwardDialog({
|
|
252
|
+
targets: this.profilesTargetsState.targets,
|
|
253
|
+
}),
|
|
254
|
+
}]
|
|
255
|
+
: []),
|
|
256
|
+
{
|
|
257
|
+
name: 'Refresh',
|
|
258
|
+
iconName: 'lucide:refreshCw',
|
|
259
|
+
action: async () => this.refreshData(),
|
|
260
|
+
},
|
|
202
261
|
];
|
|
203
|
-
const selectedTarget = targetOptions.find((option) => {
|
|
204
|
-
return option.key === (existing?.metadata?.networkTargetRef || '');
|
|
205
|
-
}) || targetOptions[0];
|
|
206
|
-
|
|
207
|
-
await DeesModal.createAndShow({
|
|
208
|
-
heading: existing
|
|
209
|
-
? `Edit HTTP-01 Forward: ${existing.route.name || existing.id}`
|
|
210
|
-
: 'Add Let\'s Encrypt HTTP-01 Forward',
|
|
211
|
-
content: html`
|
|
212
|
-
<dees-form>
|
|
213
|
-
<dees-input-text
|
|
214
|
-
.key=${'name'}
|
|
215
|
-
.label=${'Name'}
|
|
216
|
-
.value=${existing?.route.name || ''}
|
|
217
|
-
.required=${true}
|
|
218
|
-
></dees-input-text>
|
|
219
|
-
<dees-input-list
|
|
220
|
-
.key=${'domains'}
|
|
221
|
-
.label=${'Domains'}
|
|
222
|
-
.description=${'Exact public hostnames; HTTP-01 does not support wildcards'}
|
|
223
|
-
.placeholder=${'Add domain...'}
|
|
224
|
-
.value=${existing ? getDomains(existing) : []}
|
|
225
|
-
></dees-input-list>
|
|
226
|
-
<dees-input-dropdown
|
|
227
|
-
.key=${'networkTargetRef'}
|
|
228
|
-
.label=${'Network Target'}
|
|
229
|
-
.options=${targetOptions}
|
|
230
|
-
.selectedOption=${selectedTarget}
|
|
231
|
-
></dees-input-dropdown>
|
|
232
|
-
<dees-input-text
|
|
233
|
-
.key=${'targetHost'}
|
|
234
|
-
.label=${'Inline Target Host'}
|
|
235
|
-
.description=${'Used only when no Network Target is selected'}
|
|
236
|
-
.value=${target?.host || ''}
|
|
237
|
-
></dees-input-text>
|
|
238
|
-
<dees-input-text
|
|
239
|
-
.key=${'targetPort'}
|
|
240
|
-
.label=${'Inline Target Port'}
|
|
241
|
-
.description=${'Usually port 80 on the ACME responder'}
|
|
242
|
-
.value=${target ? String(target.port) : '80'}
|
|
243
|
-
></dees-input-text>
|
|
244
|
-
<dees-input-checkbox
|
|
245
|
-
.key=${'remoteIngressEnabled'}
|
|
246
|
-
.label=${'Enable Remote Ingress'}
|
|
247
|
-
.value=${existing?.route.remoteIngress?.enabled === true}
|
|
248
|
-
></dees-input-checkbox>
|
|
249
|
-
<dees-input-list
|
|
250
|
-
.key=${'remoteIngressEdgeFilter'}
|
|
251
|
-
.label=${'Remote Ingress Edge Filter'}
|
|
252
|
-
.description=${'Optional edge IDs or tags; leave empty for all edges'}
|
|
253
|
-
.placeholder=${'Add edge ID or tag...'}
|
|
254
|
-
.value=${existing?.route.remoteIngress?.edgeFilter || []}
|
|
255
|
-
></dees-input-list>
|
|
256
|
-
</dees-form>
|
|
257
|
-
`,
|
|
258
|
-
menuOptions: [
|
|
259
|
-
{
|
|
260
|
-
name: 'Cancel',
|
|
261
|
-
iconName: 'lucide:x',
|
|
262
|
-
action: async (modalArg: any) => modalArg.destroy(),
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
name: existing ? 'Save' : 'Create',
|
|
266
|
-
iconName: existing ? 'lucide:check' : 'lucide:plus',
|
|
267
|
-
action: async (modalArg: any) => {
|
|
268
|
-
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
269
|
-
if (!form) return;
|
|
270
|
-
const data = await form.collectFormData();
|
|
271
|
-
const name = String(data.name || '').trim();
|
|
272
|
-
const domains = Array.isArray(data.domains)
|
|
273
|
-
? data.domains.map((domain: unknown) => String(domain).trim()).filter(Boolean)
|
|
274
|
-
: [];
|
|
275
|
-
const networkTargetRef = getDropdownKey(data.networkTargetRef).trim();
|
|
276
|
-
const targetHost = String(data.targetHost || '').trim();
|
|
277
|
-
const targetPort = Number.parseInt(String(data.targetPort || ''), 10);
|
|
278
|
-
if (!name || domains.length === 0) {
|
|
279
|
-
alert('Name and at least one domain are required.');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (
|
|
283
|
-
!networkTargetRef
|
|
284
|
-
&& (!targetHost || !Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535)
|
|
285
|
-
) {
|
|
286
|
-
alert('A valid inline target host and port are required.');
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const edgeFilter = Array.isArray(data.remoteIngressEdgeFilter)
|
|
291
|
-
? data.remoteIngressEdgeFilter.map((entry: unknown) => String(entry).trim()).filter(Boolean)
|
|
292
|
-
: [];
|
|
293
|
-
const config: interfaces.data.ILetsEncryptHttp01ForwardConfig = {
|
|
294
|
-
name,
|
|
295
|
-
domains,
|
|
296
|
-
...(networkTargetRef
|
|
297
|
-
? { networkTargetRef }
|
|
298
|
-
: { target: { host: targetHost, port: targetPort } }),
|
|
299
|
-
...(data.remoteIngressEnabled
|
|
300
|
-
? {
|
|
301
|
-
remoteIngress: {
|
|
302
|
-
enabled: true,
|
|
303
|
-
...(edgeFilter.length ? { edgeFilter } : {}),
|
|
304
|
-
},
|
|
305
|
-
}
|
|
306
|
-
: {}),
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
if (existing) {
|
|
310
|
-
await appstate.routeManagementStatePart.dispatchAction(
|
|
311
|
-
appstate.updateLetsEncryptHttp01ForwardAction,
|
|
312
|
-
{ id: existing.id, config, enabled: existing.enabled },
|
|
313
|
-
);
|
|
314
|
-
} else {
|
|
315
|
-
await appstate.routeManagementStatePart.dispatchAction(
|
|
316
|
-
appstate.createLetsEncryptHttp01ForwardAction,
|
|
317
|
-
{ config },
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
await modalArg.destroy();
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
});
|
|
325
262
|
}
|
|
326
263
|
|
|
327
264
|
private async confirmDelete(route: interfaces.data.IMergedRoute): Promise<void> {
|
|
328
265
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
329
266
|
await DeesModal.createAndShow({
|
|
330
267
|
heading: `Delete Special Forward: ${route.route.name || route.id}`,
|
|
331
|
-
content: html`<p>This removes the managed HTTP-01 route for ${
|
|
268
|
+
content: html`<p>This removes the managed HTTP-01 route for ${getSpecialForwardDomains(route).join(', ')}.</p>`,
|
|
332
269
|
menuOptions: [
|
|
333
270
|
{
|
|
334
271
|
name: 'Cancel',
|
|
@@ -515,7 +515,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
515
515
|
name: 'Enable',
|
|
516
516
|
iconName: 'lucide:power',
|
|
517
517
|
type: ['contextmenu', 'inRow'],
|
|
518
|
-
actionRelevancyCheckFunc: (
|
|
518
|
+
actionRelevancyCheckFunc: (client: any) => !client.enabled,
|
|
519
519
|
actionFunc: async (actionData: any) => {
|
|
520
520
|
const client = actionData.item as interfaces.data.IVpnClient;
|
|
521
521
|
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
|
@@ -528,7 +528,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
528
528
|
name: 'Disable',
|
|
529
529
|
iconName: 'lucide:power',
|
|
530
530
|
type: ['contextmenu', 'inRow'],
|
|
531
|
-
actionRelevancyCheckFunc: (
|
|
531
|
+
actionRelevancyCheckFunc: (client: any) => client.enabled,
|
|
532
532
|
actionFunc: async (actionData: any) => {
|
|
533
533
|
const client = actionData.item as interfaces.data.IVpnClient;
|
|
534
534
|
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeSourceRouteId,
|
|
3
|
+
type IHttpRedirectInfo,
|
|
4
|
+
type IMergedRoute,
|
|
5
|
+
} from '../../../dist_ts_interfaces/data/route-management.js';
|
|
6
|
+
|
|
7
|
+
export type TRouteOwnerFilter = 'all' | 'operator' | 'gatewayClient' | 'system';
|
|
8
|
+
export type TResolvedRouteOwner = Exclude<TRouteOwnerFilter, 'all'>;
|
|
9
|
+
|
|
10
|
+
export function getRouteOwner(route: IMergedRoute): TResolvedRouteOwner {
|
|
11
|
+
if (route.metadata?.ownerType === 'gatewayClient') return 'gatewayClient';
|
|
12
|
+
if (route.metadata?.ownerType === 'system') return 'system';
|
|
13
|
+
if (route.metadata?.ownerType === 'operator') return 'operator';
|
|
14
|
+
return route.origin === 'api' ? 'operator' : 'system';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function routeMatchesOwner(
|
|
18
|
+
route: IMergedRoute,
|
|
19
|
+
ownerFilter: TRouteOwnerFilter,
|
|
20
|
+
): boolean {
|
|
21
|
+
return ownerFilter === 'all' || getRouteOwner(route) === ownerFilter;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCanonicalRouteIds(route: IMergedRoute): string[] {
|
|
25
|
+
const configuredRouteId = (route.route as { id?: string }).id;
|
|
26
|
+
return [route.id, configuredRouteId]
|
|
27
|
+
.filter((routeId): routeId is string => Boolean(routeId))
|
|
28
|
+
.map(canonicalizeSourceRouteId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function redirectMatchesOwner(
|
|
32
|
+
redirect: IHttpRedirectInfo,
|
|
33
|
+
mergedRoutes: IMergedRoute[],
|
|
34
|
+
ownerFilter: TRouteOwnerFilter,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (ownerFilter === 'all') return true;
|
|
37
|
+
|
|
38
|
+
const sourceRouteIds = new Set(redirect.sourceRouteIds.map(canonicalizeSourceRouteId));
|
|
39
|
+
return mergedRoutes.some((route) => {
|
|
40
|
+
return getCanonicalRouteIds(route).some((routeId) => sourceRouteIds.has(routeId))
|
|
41
|
+
&& getRouteOwner(route) === ownerFilter;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { html } from '@design.estate/dees-element';
|
|
2
|
+
import * as appstate from '../../appstate.js';
|
|
3
|
+
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export const letsEncryptHttp01ManagedRouteKind = 'letsencrypt-http01-forward' as const;
|
|
6
|
+
|
|
7
|
+
export function getSpecialForwardDomains(
|
|
8
|
+
route: interfaces.data.IMergedRoute,
|
|
9
|
+
): string[] {
|
|
10
|
+
const domains = route.route.match.domains;
|
|
11
|
+
if (!domains) return [];
|
|
12
|
+
return Array.isArray(domains) ? domains : [domains];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getSpecialForwardTarget(
|
|
16
|
+
route: interfaces.data.IMergedRoute,
|
|
17
|
+
): { host: string; port: number } | undefined {
|
|
18
|
+
const target = route.route.action.targets?.[0];
|
|
19
|
+
if (!target || typeof target.port !== 'number') return undefined;
|
|
20
|
+
const host = typeof target.host === 'string'
|
|
21
|
+
? target.host
|
|
22
|
+
: Array.isArray(target.host)
|
|
23
|
+
? target.host[0]
|
|
24
|
+
: undefined;
|
|
25
|
+
return host ? { host, port: target.port } : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDropdownKey(value: unknown): string {
|
|
29
|
+
return typeof value === 'string'
|
|
30
|
+
? value
|
|
31
|
+
: value && typeof value === 'object' && 'key' in value
|
|
32
|
+
? String((value as { key: unknown }).key)
|
|
33
|
+
: '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function showLetsEncryptHttp01ForwardDialog(options: {
|
|
37
|
+
existing?: interfaces.data.IMergedRoute;
|
|
38
|
+
targets: interfaces.data.INetworkTarget[];
|
|
39
|
+
}): Promise<void> {
|
|
40
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
41
|
+
const { existing, targets } = options;
|
|
42
|
+
const target = existing ? getSpecialForwardTarget(existing) : undefined;
|
|
43
|
+
const targetOptions = [
|
|
44
|
+
{ key: '', option: '(none — inline target)' },
|
|
45
|
+
...targets.map((networkTarget) => ({
|
|
46
|
+
key: networkTarget.id,
|
|
47
|
+
option: `${networkTarget.name} (${
|
|
48
|
+
Array.isArray(networkTarget.host) ? networkTarget.host.join(', ') : networkTarget.host
|
|
49
|
+
}:${networkTarget.port})`,
|
|
50
|
+
})),
|
|
51
|
+
];
|
|
52
|
+
const selectedTarget = targetOptions.find((option) => {
|
|
53
|
+
return option.key === (existing?.metadata?.networkTargetRef || '');
|
|
54
|
+
}) || targetOptions[0];
|
|
55
|
+
|
|
56
|
+
await DeesModal.createAndShow({
|
|
57
|
+
heading: existing
|
|
58
|
+
? `Edit User HTTP-01 Forward: ${existing.route.name || existing.id}`
|
|
59
|
+
: 'Add User Let\'s Encrypt HTTP-01 Forward',
|
|
60
|
+
content: html`
|
|
61
|
+
<dees-form>
|
|
62
|
+
<dees-input-text
|
|
63
|
+
.key=${'name'}
|
|
64
|
+
.label=${'Name'}
|
|
65
|
+
.value=${existing?.route.name || ''}
|
|
66
|
+
.required=${true}
|
|
67
|
+
></dees-input-text>
|
|
68
|
+
<dees-input-list
|
|
69
|
+
.key=${'domains'}
|
|
70
|
+
.label=${'Domains'}
|
|
71
|
+
.description=${'Exact public hostnames; HTTP-01 does not support wildcards'}
|
|
72
|
+
.placeholder=${'Add domain...'}
|
|
73
|
+
.value=${existing ? getSpecialForwardDomains(existing) : []}
|
|
74
|
+
></dees-input-list>
|
|
75
|
+
<dees-input-dropdown
|
|
76
|
+
.key=${'networkTargetRef'}
|
|
77
|
+
.label=${'Network Target'}
|
|
78
|
+
.options=${targetOptions}
|
|
79
|
+
.selectedOption=${selectedTarget}
|
|
80
|
+
></dees-input-dropdown>
|
|
81
|
+
<dees-input-text
|
|
82
|
+
.key=${'targetHost'}
|
|
83
|
+
.label=${'Inline Target Host'}
|
|
84
|
+
.description=${'Used only when no Network Target is selected'}
|
|
85
|
+
.value=${target?.host || ''}
|
|
86
|
+
></dees-input-text>
|
|
87
|
+
<dees-input-text
|
|
88
|
+
.key=${'targetPort'}
|
|
89
|
+
.label=${'Inline Target Port'}
|
|
90
|
+
.description=${'Usually port 80 on the ACME responder'}
|
|
91
|
+
.value=${target ? String(target.port) : '80'}
|
|
92
|
+
></dees-input-text>
|
|
93
|
+
<dees-input-checkbox
|
|
94
|
+
.key=${'remoteIngressEnabled'}
|
|
95
|
+
.label=${'Enable Remote Ingress'}
|
|
96
|
+
.value=${existing?.route.remoteIngress?.enabled === true}
|
|
97
|
+
></dees-input-checkbox>
|
|
98
|
+
<dees-input-list
|
|
99
|
+
.key=${'remoteIngressEdgeFilter'}
|
|
100
|
+
.label=${'Remote Ingress Edge Filter'}
|
|
101
|
+
.description=${'Optional edge IDs or tags; leave empty for all edges'}
|
|
102
|
+
.placeholder=${'Add edge ID or tag...'}
|
|
103
|
+
.value=${existing?.route.remoteIngress?.edgeFilter || []}
|
|
104
|
+
></dees-input-list>
|
|
105
|
+
</dees-form>
|
|
106
|
+
`,
|
|
107
|
+
menuOptions: [
|
|
108
|
+
{
|
|
109
|
+
name: 'Cancel',
|
|
110
|
+
iconName: 'lucide:x',
|
|
111
|
+
action: async (modalArg: any) => modalArg.destroy(),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: existing ? 'Save' : 'Create',
|
|
115
|
+
iconName: existing ? 'lucide:check' : 'lucide:plus',
|
|
116
|
+
action: async (modalArg: any) => {
|
|
117
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
118
|
+
if (!form) return;
|
|
119
|
+
const data = await form.collectFormData();
|
|
120
|
+
const name = String(data.name || '').trim();
|
|
121
|
+
const domains = Array.isArray(data.domains)
|
|
122
|
+
? data.domains.map((domain: unknown) => String(domain).trim()).filter(Boolean)
|
|
123
|
+
: [];
|
|
124
|
+
const networkTargetRef = getDropdownKey(data.networkTargetRef).trim();
|
|
125
|
+
const targetHost = String(data.targetHost || '').trim();
|
|
126
|
+
const targetPort = Number.parseInt(String(data.targetPort || ''), 10);
|
|
127
|
+
if (!name || domains.length === 0) {
|
|
128
|
+
alert('Name and at least one domain are required.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
!networkTargetRef
|
|
133
|
+
&& (!targetHost || !Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535)
|
|
134
|
+
) {
|
|
135
|
+
alert('A valid inline target host and port are required.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const edgeFilter = Array.isArray(data.remoteIngressEdgeFilter)
|
|
140
|
+
? data.remoteIngressEdgeFilter.map((entry: unknown) => String(entry).trim()).filter(Boolean)
|
|
141
|
+
: [];
|
|
142
|
+
const config: interfaces.data.ILetsEncryptHttp01ForwardConfig = {
|
|
143
|
+
name,
|
|
144
|
+
domains,
|
|
145
|
+
...(networkTargetRef
|
|
146
|
+
? { networkTargetRef }
|
|
147
|
+
: { target: { host: targetHost, port: targetPort } }),
|
|
148
|
+
...(data.remoteIngressEnabled
|
|
149
|
+
? {
|
|
150
|
+
remoteIngress: {
|
|
151
|
+
enabled: true,
|
|
152
|
+
...(edgeFilter.length ? { edgeFilter } : {}),
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (existing) {
|
|
159
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
160
|
+
appstate.updateLetsEncryptHttp01ForwardAction,
|
|
161
|
+
{ id: existing.id, config, enabled: existing.enabled },
|
|
162
|
+
);
|
|
163
|
+
} else {
|
|
164
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
165
|
+
appstate.createLetsEncryptHttp01ForwardAction,
|
|
166
|
+
{ config },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
await modalArg.destroy();
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -268,7 +268,7 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|
|
268
268
|
name: 'Enable',
|
|
269
269
|
iconName: 'lucide:play',
|
|
270
270
|
type: ['contextmenu'] as any,
|
|
271
|
-
actionRelevancyCheckFunc: (
|
|
271
|
+
actionRelevancyCheckFunc: (entry: any) => !entry.enabled,
|
|
272
272
|
actionFunc: async (actionData: any) => {
|
|
273
273
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
|
274
274
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
|
@@ -281,7 +281,7 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|
|
281
281
|
name: 'Disable',
|
|
282
282
|
iconName: 'lucide:pause',
|
|
283
283
|
type: ['contextmenu'] as any,
|
|
284
|
-
actionRelevancyCheckFunc: (
|
|
284
|
+
actionRelevancyCheckFunc: (entry: any) => entry.enabled,
|
|
285
285
|
actionFunc: async (actionData: any) => {
|
|
286
286
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
|
287
287
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
|
@@ -331,7 +331,7 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|
|
331
331
|
name: 'Block Network Range',
|
|
332
332
|
iconName: 'lucide:network',
|
|
333
333
|
type: ['contextmenu'] as any,
|
|
334
|
-
actionRelevancyCheckFunc: (
|
|
334
|
+
actionRelevancyCheckFunc: (entry: any) => Boolean(entry.networkRange),
|
|
335
335
|
actionFunc: async (actionData: any) => {
|
|
336
336
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
|
337
337
|
await this.showRuleDialog(undefined, {
|
|
@@ -345,7 +345,7 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|
|
345
345
|
name: 'Block ASN',
|
|
346
346
|
iconName: 'lucide:radio-tower',
|
|
347
347
|
type: ['contextmenu'] as any,
|
|
348
|
-
actionRelevancyCheckFunc: (
|
|
348
|
+
actionRelevancyCheckFunc: (entry: any) => Boolean(entry.asn),
|
|
349
349
|
actionFunc: async (actionData: any) => {
|
|
350
350
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
|
351
351
|
await this.showRuleDialog(undefined, {
|
|
@@ -359,7 +359,7 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|
|
359
359
|
name: 'Block Organization',
|
|
360
360
|
iconName: 'lucide:building-2',
|
|
361
361
|
type: ['contextmenu'] as any,
|
|
362
|
-
actionRelevancyCheckFunc: (
|
|
362
|
+
actionRelevancyCheckFunc: (entry: any) => Boolean(entry.asnOrg || entry.registrantOrg),
|
|
363
363
|
actionFunc: async (actionData: any) => {
|
|
364
364
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
|
365
365
|
await this.showRuleDialog(undefined, {
|
package/ts_web/readme.md
CHANGED
|
@@ -29,12 +29,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
29
29
|
|
|
30
30
|
## Routes Navigation
|
|
31
31
|
|
|
32
|
-
The `/network/routes` view uses two
|
|
32
|
+
The `/network/routes` view uses two adjacent, independent multitoggles:
|
|
33
33
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
34
|
+
- type: **All**, **Standard**, **Redirect**, and **Special**
|
|
35
|
+
- owner: **All**, **User**, **Gateway Client**, and **System**
|
|
36
36
|
|
|
37
|
-
Special
|
|
37
|
+
The owner selection filters every displayed route projection. **All** keeps Standard, Special, and Redirect routes in separate sections rather than mixing unlike rows. Special routes include managed Let's Encrypt HTTP-01 forwards for exact domains and a selected inline or Network Target backend; non-user routes are read-only. Redirects remain a derived runtime view and inherit ownership from their canonical source routes. Filter state is stored in the `tab` and `owner` query parameters so deep links and browser history remain stable; the legacy `/network/redirects` URL is normalized to `/network/routes?tab=redirects`.
|
|
38
38
|
|
|
39
39
|
## Runtime Communication
|
|
40
40
|
|