@serve.zone/dcrouter 12.0.0 → 12.2.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.
Files changed (84) hide show
  1. package/dist_serve/bundle.js +1179 -1004
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +17 -1
  4. package/dist_ts/classes.dcrouter.js +16 -3
  5. package/dist_ts/config/classes.db-seeder.d.ts +25 -0
  6. package/dist_ts/config/classes.db-seeder.js +69 -0
  7. package/dist_ts/config/classes.reference-resolver.d.ts +80 -0
  8. package/dist_ts/config/classes.reference-resolver.js +482 -0
  9. package/dist_ts/config/classes.route-config-manager.d.ts +13 -3
  10. package/dist_ts/config/classes.route-config-manager.js +53 -3
  11. package/dist_ts/config/index.d.ts +2 -0
  12. package/dist_ts/config/index.js +3 -1
  13. package/dist_ts/db/documents/classes.network-target.doc.d.ts +15 -0
  14. package/dist_ts/db/documents/classes.network-target.doc.js +118 -0
  15. package/dist_ts/db/documents/classes.security-profile.doc.d.ts +16 -0
  16. package/dist_ts/db/documents/classes.security-profile.doc.js +118 -0
  17. package/dist_ts/db/documents/classes.stored-route.doc.d.ts +2 -0
  18. package/dist_ts/db/documents/classes.stored-route.doc.js +8 -2
  19. package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +8 -0
  20. package/dist_ts/db/documents/classes.vpn-client.doc.js +50 -2
  21. package/dist_ts/db/documents/index.d.ts +2 -0
  22. package/dist_ts/db/documents/index.js +3 -1
  23. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  24. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  25. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  26. package/dist_ts/opsserver/handlers/index.js +3 -1
  27. package/dist_ts/opsserver/handlers/network-target.handler.d.ts +10 -0
  28. package/dist_ts/opsserver/handlers/network-target.handler.js +117 -0
  29. package/dist_ts/opsserver/handlers/route-management.handler.js +3 -2
  30. package/dist_ts/opsserver/handlers/security-profile.handler.d.ts +10 -0
  31. package/dist_ts/opsserver/handlers/security-profile.handler.js +119 -0
  32. package/dist_ts/opsserver/handlers/vpn.handler.js +35 -1
  33. package/dist_ts/vpn/classes.vpn-manager.d.ts +33 -0
  34. package/dist_ts/vpn/classes.vpn-manager.js +122 -7
  35. package/dist_ts_interfaces/data/route-management.d.ts +48 -1
  36. package/dist_ts_interfaces/data/vpn.d.ts +8 -0
  37. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  38. package/dist_ts_interfaces/requests/index.js +3 -1
  39. package/dist_ts_interfaces/requests/network-targets.d.ts +102 -0
  40. package/dist_ts_interfaces/requests/network-targets.js +2 -0
  41. package/dist_ts_interfaces/requests/route-management.d.ts +3 -1
  42. package/dist_ts_interfaces/requests/security-profiles.d.ts +102 -0
  43. package/dist_ts_interfaces/requests/security-profiles.js +2 -0
  44. package/dist_ts_interfaces/requests/vpn.d.ts +16 -0
  45. package/dist_ts_web/00_commitinfo_data.js +1 -1
  46. package/dist_ts_web/appstate.d.ts +59 -0
  47. package/dist_ts_web/appstate.js +192 -2
  48. package/dist_ts_web/elements/index.d.ts +2 -0
  49. package/dist_ts_web/elements/index.js +3 -1
  50. package/dist_ts_web/elements/ops-dashboard.js +13 -1
  51. package/dist_ts_web/elements/ops-view-networktargets.d.ts +17 -0
  52. package/dist_ts_web/elements/ops-view-networktargets.js +246 -0
  53. package/dist_ts_web/elements/ops-view-securityprofiles.d.ts +17 -0
  54. package/dist_ts_web/elements/ops-view-securityprofiles.js +275 -0
  55. package/dist_ts_web/elements/ops-view-vpn.js +155 -3
  56. package/dist_ts_web/router.d.ts +1 -1
  57. package/dist_ts_web/router.js +2 -2
  58. package/package.json +3 -3
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/classes.dcrouter.ts +35 -1
  61. package/ts/config/classes.db-seeder.ts +95 -0
  62. package/ts/config/classes.reference-resolver.ts +576 -0
  63. package/ts/config/classes.route-config-manager.ts +64 -1
  64. package/ts/config/index.ts +3 -1
  65. package/ts/db/documents/classes.network-target.doc.ts +48 -0
  66. package/ts/db/documents/classes.security-profile.doc.ts +49 -0
  67. package/ts/db/documents/classes.stored-route.doc.ts +4 -0
  68. package/ts/db/documents/classes.vpn-client.doc.ts +24 -0
  69. package/ts/db/documents/index.ts +2 -0
  70. package/ts/opsserver/classes.opsserver.ts +4 -0
  71. package/ts/opsserver/handlers/index.ts +3 -1
  72. package/ts/opsserver/handlers/network-target.handler.ts +167 -0
  73. package/ts/opsserver/handlers/route-management.handler.ts +2 -1
  74. package/ts/opsserver/handlers/security-profile.handler.ts +169 -0
  75. package/ts/opsserver/handlers/vpn.handler.ts +37 -0
  76. package/ts/vpn/classes.vpn-manager.ts +143 -6
  77. package/ts_web/00_commitinfo_data.ts +1 -1
  78. package/ts_web/appstate.ts +275 -1
  79. package/ts_web/elements/index.ts +2 -0
  80. package/ts_web/elements/ops-dashboard.ts +12 -0
  81. package/ts_web/elements/ops-view-networktargets.ts +214 -0
  82. package/ts_web/elements/ops-view-securityprofiles.ts +242 -0
  83. package/ts_web/elements/ops-view-vpn.ts +153 -2
  84. package/ts_web/router.ts +1 -1
@@ -0,0 +1,242 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ customElement,
5
+ type TemplateResult,
6
+ css,
7
+ state,
8
+ cssManager,
9
+ } from '@design.estate/dees-element';
10
+ import * as appstate from '../appstate.js';
11
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
12
+ import { viewHostCss } from './shared/css.js';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-securityprofiles': OpsViewSecurityProfiles;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-securityprofiles')
22
+ export class OpsViewSecurityProfiles extends DeesElement {
23
+ @state()
24
+ accessor profilesState: appstate.IProfilesTargetsState = appstate.profilesTargetsStatePart.getState()!;
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.profilesTargetsStatePart.select().subscribe((newState) => {
29
+ this.profilesState = newState;
30
+ });
31
+ this.rxSubscriptions.push(sub);
32
+ }
33
+
34
+ async connectedCallback() {
35
+ await super.connectedCallback();
36
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
37
+ }
38
+
39
+ public static styles = [
40
+ cssManager.defaultStyles,
41
+ viewHostCss,
42
+ css`
43
+ .profilesContainer {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 24px;
47
+ }
48
+ `,
49
+ ];
50
+
51
+ public render(): TemplateResult {
52
+ const profiles = this.profilesState.profiles;
53
+
54
+ const statsTiles: IStatsTile[] = [
55
+ {
56
+ id: 'totalProfiles',
57
+ title: 'Total Profiles',
58
+ type: 'number',
59
+ value: profiles.length,
60
+ icon: 'lucide:shieldCheck',
61
+ description: 'Reusable security profiles',
62
+ color: '#3b82f6',
63
+ },
64
+ ];
65
+
66
+ return html`
67
+ <div class="profilesContainer">
68
+ <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
69
+ <dees-table
70
+ .heading=${'Security Profiles'}
71
+ .data=${profiles}
72
+ .displayFunction=${(profile: interfaces.data.ISecurityProfile) => ({
73
+ Name: profile.name,
74
+ Description: profile.description || '-',
75
+ 'IP Allow List': (profile.security?.ipAllowList || []).join(', ') || '-',
76
+ 'IP Block List': (profile.security?.ipBlockList || []).join(', ') || '-',
77
+ 'Max Connections': profile.security?.maxConnections ?? '-',
78
+ 'Rate Limit': profile.security?.rateLimit?.enabled ? `${profile.security.rateLimit.maxRequests}/${profile.security.rateLimit.window}s` : '-',
79
+ Extends: (profile.extendsProfiles || []).length > 0
80
+ ? profile.extendsProfiles!.map(id => {
81
+ const p = profiles.find(pp => pp.id === id);
82
+ return p ? p.name : id.slice(0, 8);
83
+ }).join(', ')
84
+ : '-',
85
+ })}
86
+ .dataActions=${[
87
+ {
88
+ name: 'Create Profile',
89
+ iconName: 'lucide:plus',
90
+ type: ['header' as const],
91
+ action: async (_: any, table: any) => {
92
+ await this.showCreateProfileDialog(table);
93
+ },
94
+ },
95
+ {
96
+ name: 'Refresh',
97
+ iconName: 'lucide:rotateCw',
98
+ type: ['header' as const],
99
+ action: async () => {
100
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
101
+ },
102
+ },
103
+ {
104
+ name: 'Edit',
105
+ iconName: 'lucide:pencil',
106
+ type: ['contextmenu' as const],
107
+ action: async (profile: interfaces.data.ISecurityProfile, table: any) => {
108
+ await this.showEditProfileDialog(profile, table);
109
+ },
110
+ },
111
+ {
112
+ name: 'Delete',
113
+ iconName: 'lucide:trash2',
114
+ type: ['contextmenu' as const],
115
+ action: async (profile: interfaces.data.ISecurityProfile) => {
116
+ await this.deleteProfile(profile);
117
+ },
118
+ },
119
+ ]}
120
+ ></dees-table>
121
+ </div>
122
+ `;
123
+ }
124
+
125
+ private async showCreateProfileDialog(table: any) {
126
+ const { DeesModal } = await import('@design.estate/dees-catalog');
127
+ DeesModal.createAndShow({
128
+ heading: 'Create Security Profile',
129
+ content: html`
130
+ <dees-form>
131
+ <dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
132
+ <dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
133
+ <dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'}></dees-input-text>
134
+ <dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'}></dees-input-text>
135
+ <dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
136
+ </dees-form>
137
+ `,
138
+ menuOptions: [
139
+ {
140
+ name: 'Create',
141
+ action: async (modalArg: any) => {
142
+ const form = modalArg.shadowRoot!.querySelector('dees-form');
143
+ const data = await form.collectFormData();
144
+ const ipAllowList = data.ipAllowList
145
+ ? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
146
+ : undefined;
147
+ const ipBlockList = data.ipBlockList
148
+ ? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
149
+ : undefined;
150
+ const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
151
+
152
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
153
+ name: String(data.name),
154
+ description: data.description ? String(data.description) : undefined,
155
+ security: {
156
+ ...(ipAllowList ? { ipAllowList } : {}),
157
+ ...(ipBlockList ? { ipBlockList } : {}),
158
+ ...(maxConnections ? { maxConnections } : {}),
159
+ },
160
+ });
161
+ modalArg.destroy();
162
+ },
163
+ },
164
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
165
+ ],
166
+ });
167
+ }
168
+
169
+ private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile, table: any) {
170
+ const { DeesModal } = await import('@design.estate/dees-catalog');
171
+ DeesModal.createAndShow({
172
+ heading: `Edit Profile: ${profile.name}`,
173
+ content: html`
174
+ <dees-form>
175
+ <dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
176
+ <dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
177
+ <dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'} .value=${(profile.security?.ipAllowList || []).join(', ')}></dees-input-text>
178
+ <dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'} .value=${(profile.security?.ipBlockList || []).join(', ')}></dees-input-text>
179
+ <dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
180
+ </dees-form>
181
+ `,
182
+ menuOptions: [
183
+ {
184
+ name: 'Save',
185
+ action: async (modalArg: any) => {
186
+ const form = modalArg.shadowRoot!.querySelector('dees-form');
187
+ const data = await form.collectFormData();
188
+ const ipAllowList = data.ipAllowList
189
+ ? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
190
+ : [];
191
+ const ipBlockList = data.ipBlockList
192
+ ? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
193
+ : [];
194
+ const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
195
+
196
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
197
+ id: profile.id,
198
+ name: String(data.name),
199
+ description: data.description ? String(data.description) : undefined,
200
+ security: {
201
+ ipAllowList,
202
+ ipBlockList,
203
+ ...(maxConnections ? { maxConnections } : {}),
204
+ },
205
+ });
206
+ modalArg.destroy();
207
+ },
208
+ },
209
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
210
+ ],
211
+ });
212
+ }
213
+
214
+ private async deleteProfile(profile: interfaces.data.ISecurityProfile) {
215
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteProfileAction, {
216
+ id: profile.id,
217
+ force: false,
218
+ });
219
+
220
+ const currentState = appstate.profilesTargetsStatePart.getState()!;
221
+ if (currentState.error?.includes('in use')) {
222
+ const { DeesModal } = await import('@design.estate/dees-catalog');
223
+ DeesModal.createAndShow({
224
+ heading: 'Profile In Use',
225
+ content: html`<p>${currentState.error} Force delete?</p>`,
226
+ menuOptions: [
227
+ {
228
+ name: 'Force Delete',
229
+ action: async (modalArg: any) => {
230
+ await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteProfileAction, {
231
+ id: profile.id,
232
+ force: true,
233
+ });
234
+ modalArg.destroy();
235
+ },
236
+ },
237
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
238
+ ],
239
+ });
240
+ }
241
+ }
242
+ }
@@ -13,6 +13,31 @@ import * as interfaces from '../../dist_ts_interfaces/index.js';
13
13
  import { viewHostCss } from './shared/css.js';
14
14
  import { type IStatsTile } from '@design.estate/dees-catalog';
15
15
 
16
+ /**
17
+ * Toggle form field visibility based on checkbox states.
18
+ * Used in Create and Edit VPN client dialogs.
19
+ */
20
+ function setupFormVisibility(formEl: any) {
21
+ const show = 'flex'; // match dees-form's flex layout
22
+ const updateVisibility = async () => {
23
+ const data = await formEl.collectFormData();
24
+ const contentEl = formEl.closest('.content') || formEl.parentElement;
25
+ if (!contentEl) return;
26
+ const hostIpGroup = contentEl.querySelector('.hostIpGroup') as HTMLElement;
27
+ const hostIpDetails = contentEl.querySelector('.hostIpDetails') as HTMLElement;
28
+ const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
29
+ const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
30
+ const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
31
+ if (hostIpGroup) hostIpGroup.style.display = data.forceDestinationSmartproxy ? 'none' : show;
32
+ if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
33
+ if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
34
+ if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
35
+ if (aclGroup) aclGroup.style.display = data.allowAdditionalAcls ? show : 'none';
36
+ };
37
+ formEl.changeSubject.subscribe(() => updateVisibility());
38
+ updateVisibility();
39
+ }
40
+
16
41
  declare global {
17
42
  interface HTMLElementTagNameMap {
18
43
  'ops-view-vpn': OpsViewVpn;
@@ -289,9 +314,18 @@ export class OpsViewVpn extends DeesElement {
289
314
  } else {
290
315
  statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
291
316
  }
317
+ let routingHtml;
318
+ if (client.forceDestinationSmartproxy !== false) {
319
+ routingHtml = html`<span class="statusBadge enabled">SmartProxy</span>`;
320
+ } else if (client.useHostIp) {
321
+ routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
322
+ } else {
323
+ routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
324
+ }
292
325
  return {
293
326
  'Client ID': client.clientId,
294
327
  'Status': statusHtml,
328
+ 'Routing': routingHtml,
295
329
  'VPN IP': client.assignedIp || '-',
296
330
  'Tags': client.serverDefinedClientTags?.length
297
331
  ? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
@@ -307,13 +341,32 @@ export class OpsViewVpn extends DeesElement {
307
341
  type: ['header'],
308
342
  actionFunc: async () => {
309
343
  const { DeesModal } = await import('@design.estate/dees-catalog');
310
- await DeesModal.createAndShow({
344
+ const createModal = await DeesModal.createAndShow({
311
345
  heading: 'Create VPN Client',
312
346
  content: html`
313
347
  <dees-form>
314
348
  <dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
315
349
  <dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
316
350
  <dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
351
+ <dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
352
+ <div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
353
+ <dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
354
+ <div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
355
+ <dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
356
+ <div class="staticIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
357
+ <dees-input-text .key=${'staticIp'} .label=${'Static IP'}></dees-input-text>
358
+ </div>
359
+ <dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${false}></dees-input-checkbox>
360
+ <div class="vlanIdGroup" style="display: none; flex-direction: column; gap: 16px;">
361
+ <dees-input-text .key=${'vlanId'} .label=${'VLAN ID'}></dees-input-text>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ <dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox>
366
+ <div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;">
367
+ <dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'}></dees-input-text>
368
+ <dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'}></dees-input-text>
369
+ </div>
317
370
  </dees-form>
318
371
  `,
319
372
  menuOptions: [
@@ -333,16 +386,47 @@ export class OpsViewVpn extends DeesElement {
333
386
  const serverDefinedClientTags = data.tags
334
387
  ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
335
388
  : undefined;
389
+
390
+ // Apply conditional logic based on checkbox states
391
+ const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
392
+ const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
393
+ const useDhcp = useHostIp && (data.useDhcp ?? false);
394
+ const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
395
+ const forceVlan = useHostIp && (data.forceVlan ?? false);
396
+ const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
397
+
398
+ const allowAcls = data.allowAdditionalAcls ?? false;
399
+ const destinationAllowList = allowAcls && data.destinationAllowList
400
+ ? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
401
+ : undefined;
402
+ const destinationBlockList = allowAcls && data.destinationBlockList
403
+ ? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
404
+ : undefined;
405
+
336
406
  await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
337
407
  clientId: data.clientId,
338
408
  description: data.description || undefined,
339
409
  serverDefinedClientTags,
410
+ forceDestinationSmartproxy: forceSmartproxy,
411
+ useHostIp: useHostIp || undefined,
412
+ useDhcp: useDhcp || undefined,
413
+ staticIp,
414
+ forceVlan: forceVlan || undefined,
415
+ vlanId,
416
+ destinationAllowList,
417
+ destinationBlockList,
340
418
  });
341
419
  await modalArg.destroy();
342
420
  },
343
421
  },
344
422
  ],
345
423
  });
424
+ // Setup conditional form visibility after modal renders
425
+ const createForm = createModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
426
+ if (createForm) {
427
+ await createForm.updateComplete;
428
+ setupFormVisibility(createForm);
429
+ }
346
430
  },
347
431
  },
348
432
  {
@@ -396,6 +480,13 @@ export class OpsViewVpn extends DeesElement {
396
480
  ` : ''}
397
481
  <div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
398
482
  <div class="infoItem"><span class="infoLabel">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
483
+ <div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
484
+ ${client.useHostIp ? html`
485
+ <div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
486
+ <div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
487
+ ` : ''}
488
+ <div class="infoItem"><span class="infoLabel">Allow List</span><span class="infoValue">${client.destinationAllowList?.length ? client.destinationAllowList.join(', ') : 'None'}</span></div>
489
+ <div class="infoItem"><span class="infoLabel">Block List</span><span class="infoValue">${client.destinationBlockList?.length ? client.destinationBlockList.join(', ') : 'None'}</span></div>
399
490
  <div class="infoItem"><span class="infoLabel">Created</span><span class="infoValue">${new Date(client.createdAt).toLocaleString()}</span></div>
400
491
  <div class="infoItem"><span class="infoLabel">Updated</span><span class="infoValue">${new Date(client.updatedAt).toLocaleString()}</span></div>
401
492
  </div>
@@ -553,12 +644,41 @@ export class OpsViewVpn extends DeesElement {
553
644
  const { DeesModal } = await import('@design.estate/dees-catalog');
554
645
  const currentDescription = client.description ?? '';
555
646
  const currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
556
- DeesModal.createAndShow({
647
+ const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
648
+ const currentUseHostIp = client.useHostIp ?? false;
649
+ const currentUseDhcp = client.useDhcp ?? false;
650
+ const currentStaticIp = client.staticIp ?? '';
651
+ const currentForceVlan = client.forceVlan ?? false;
652
+ const currentVlanId = client.vlanId != null ? String(client.vlanId) : '';
653
+ const currentAllowList = client.destinationAllowList?.join(', ') ?? '';
654
+ const currentBlockList = client.destinationBlockList?.join(', ') ?? '';
655
+ const currentAllowAcls = (client.destinationAllowList?.length ?? 0) > 0
656
+ || (client.destinationBlockList?.length ?? 0) > 0;
657
+ const editModal = await DeesModal.createAndShow({
557
658
  heading: `Edit: ${client.clientId}`,
558
659
  content: html`
559
660
  <dees-form>
560
661
  <dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
561
662
  <dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
663
+ <dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
664
+ <div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
665
+ <dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
666
+ <div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
667
+ <dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
668
+ <div class="staticIpGroup" style="display: ${currentUseDhcp ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
669
+ <dees-input-text .key=${'staticIp'} .label=${'Static IP'} .value=${currentStaticIp}></dees-input-text>
670
+ </div>
671
+ <dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${currentForceVlan}></dees-input-checkbox>
672
+ <div class="vlanIdGroup" style="display: ${currentForceVlan ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
673
+ <dees-input-text .key=${'vlanId'} .label=${'VLAN ID'} .value=${currentVlanId}></dees-input-text>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ <dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox>
678
+ <div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
679
+ <dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'} .value=${currentAllowList}></dees-input-text>
680
+ <dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'} .value=${currentBlockList}></dees-input-text>
681
+ </div>
562
682
  </dees-form>
563
683
  `,
564
684
  menuOptions: [
@@ -573,16 +693,47 @@ export class OpsViewVpn extends DeesElement {
573
693
  const serverDefinedClientTags = data.tags
574
694
  ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
575
695
  : [];
696
+
697
+ // Apply conditional logic based on checkbox states
698
+ const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
699
+ const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
700
+ const useDhcp = useHostIp && (data.useDhcp ?? false);
701
+ const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
702
+ const forceVlan = useHostIp && (data.forceVlan ?? false);
703
+ const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
704
+
705
+ const allowAcls = data.allowAdditionalAcls ?? false;
706
+ const destinationAllowList = allowAcls && data.destinationAllowList
707
+ ? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
708
+ : [];
709
+ const destinationBlockList = allowAcls && data.destinationBlockList
710
+ ? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
711
+ : [];
712
+
576
713
  await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
577
714
  clientId: client.clientId,
578
715
  description: data.description || undefined,
579
716
  serverDefinedClientTags,
717
+ forceDestinationSmartproxy: forceSmartproxy,
718
+ useHostIp: useHostIp || undefined,
719
+ useDhcp: useDhcp || undefined,
720
+ staticIp,
721
+ forceVlan: forceVlan || undefined,
722
+ vlanId,
723
+ destinationAllowList,
724
+ destinationBlockList,
580
725
  });
581
726
  await modalArg.destroy();
582
727
  },
583
728
  },
584
729
  ],
585
730
  });
731
+ // Setup conditional form visibility for edit dialog
732
+ const editForm = editModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
733
+ if (editForm) {
734
+ await editForm.updateComplete;
735
+ setupFormVisibility(editForm);
736
+ }
586
737
  },
587
738
  },
588
739
  {
package/ts_web/router.ts CHANGED
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
3
3
 
4
4
  const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
5
5
 
6
- export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress', 'vpn'] as const;
6
+ export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress', 'vpn', 'securityprofiles', 'networktargets'] as const;
7
7
 
8
8
  export type TValidView = typeof validViews[number];
9
9