@serve.zone/dcrouter 13.30.0 → 13.32.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 (88) hide show
  1. package/dist_serve/bundle.js +1042 -1014
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +1 -1
  4. package/dist_ts/opsserver/classes.opsserver.d.ts +4 -2
  5. package/dist_ts/opsserver/classes.opsserver.js +2 -11
  6. package/dist_ts/opsserver/handlers/acme-config.handler.js +7 -24
  7. package/dist_ts/opsserver/handlers/admin.handler.d.ts +12 -0
  8. package/dist_ts/opsserver/handlers/admin.handler.js +129 -95
  9. package/dist_ts/opsserver/handlers/api-token.handler.js +28 -2
  10. package/dist_ts/opsserver/handlers/certificate.handler.js +7 -24
  11. package/dist_ts/opsserver/handlers/config.handler.js +3 -1
  12. package/dist_ts/opsserver/handlers/dns-provider.handler.js +7 -24
  13. package/dist_ts/opsserver/handlers/dns-record.handler.js +7 -24
  14. package/dist_ts/opsserver/handlers/domain.handler.js +7 -24
  15. package/dist_ts/opsserver/handlers/email-domain.handler.js +7 -24
  16. package/dist_ts/opsserver/handlers/email-ops.handler.js +8 -1
  17. package/dist_ts/opsserver/handlers/logs.handler.js +4 -1
  18. package/dist_ts/opsserver/handlers/network-target.handler.js +7 -24
  19. package/dist_ts/opsserver/handlers/radius.handler.js +32 -1
  20. package/dist_ts/opsserver/handlers/remoteingress.handler.js +24 -1
  21. package/dist_ts/opsserver/handlers/route-management.handler.js +7 -26
  22. package/dist_ts/opsserver/handlers/security.handler.js +32 -7
  23. package/dist_ts/opsserver/handlers/source-profile.handler.js +7 -24
  24. package/dist_ts/opsserver/handlers/stats.handler.js +8 -1
  25. package/dist_ts/opsserver/handlers/target-profile.handler.js +7 -24
  26. package/dist_ts/opsserver/handlers/users.handler.d.ts +1 -1
  27. package/dist_ts/opsserver/handlers/users.handler.js +35 -4
  28. package/dist_ts/opsserver/handlers/vpn.handler.js +34 -1
  29. package/dist_ts/opsserver/handlers/workhoster.handler.js +16 -35
  30. package/dist_ts/opsserver/helpers/auth.d.ts +21 -0
  31. package/dist_ts/opsserver/helpers/auth.js +63 -0
  32. package/dist_ts_interfaces/data/route-management.d.ts +2 -1
  33. package/dist_ts_interfaces/data/route-management.js +48 -2
  34. package/dist_ts_interfaces/requests/api-tokens.d.ts +10 -5
  35. package/dist_ts_interfaces/requests/combined.stats.d.ts +2 -1
  36. package/dist_ts_interfaces/requests/config.d.ts +2 -1
  37. package/dist_ts_interfaces/requests/email-ops.d.ts +6 -3
  38. package/dist_ts_interfaces/requests/logs.d.ts +4 -2
  39. package/dist_ts_interfaces/requests/radius.d.ts +24 -12
  40. package/dist_ts_interfaces/requests/remoteingress.d.ts +14 -7
  41. package/dist_ts_interfaces/requests/security-policy.d.ts +16 -8
  42. package/dist_ts_interfaces/requests/stats.d.ts +18 -9
  43. package/dist_ts_interfaces/requests/users.d.ts +39 -2
  44. package/dist_ts_interfaces/requests/vpn.d.ts +22 -11
  45. package/dist_ts_interfaces/requests/workhoster.d.ts +10 -5
  46. package/dist_ts_migrations/index.js +3 -1
  47. package/dist_ts_web/00_commitinfo_data.js +1 -1
  48. package/dist_ts_web/appstate.d.ts +8 -0
  49. package/dist_ts_web/appstate.js +52 -2
  50. package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -21
  51. package/dist_ts_web/elements/access/ops-view-users.d.ts +2 -0
  52. package/dist_ts_web/elements/access/ops-view-users.js +133 -2
  53. package/dist_ts_web/elements/network/ops-view-routes.js +11 -1
  54. package/dist_ts_web/elements/ops-dashboard.js +2 -4
  55. package/package.json +3 -3
  56. package/readme.md +1 -1
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/classes.dcrouter.ts +1 -1
  59. package/ts/opsserver/classes.opsserver.ts +3 -14
  60. package/ts/opsserver/handlers/acme-config.handler.ts +6 -23
  61. package/ts/opsserver/handlers/admin.handler.ts +155 -111
  62. package/ts/opsserver/handlers/api-token.handler.ts +27 -1
  63. package/ts/opsserver/handlers/certificate.handler.ts +6 -23
  64. package/ts/opsserver/handlers/config.handler.ts +2 -0
  65. package/ts/opsserver/handlers/dns-provider.handler.ts +6 -23
  66. package/ts/opsserver/handlers/dns-record.handler.ts +6 -23
  67. package/ts/opsserver/handlers/domain.handler.ts +6 -23
  68. package/ts/opsserver/handlers/email-domain.handler.ts +6 -23
  69. package/ts/opsserver/handlers/email-ops.handler.ts +7 -0
  70. package/ts/opsserver/handlers/logs.handler.ts +3 -0
  71. package/ts/opsserver/handlers/network-target.handler.ts +6 -23
  72. package/ts/opsserver/handlers/radius.handler.ts +31 -0
  73. package/ts/opsserver/handlers/remoteingress.handler.ts +23 -0
  74. package/ts/opsserver/handlers/route-management.handler.ts +6 -25
  75. package/ts/opsserver/handlers/security.handler.ts +31 -6
  76. package/ts/opsserver/handlers/source-profile.handler.ts +6 -23
  77. package/ts/opsserver/handlers/stats.handler.ts +7 -0
  78. package/ts/opsserver/handlers/target-profile.handler.ts +6 -23
  79. package/ts/opsserver/handlers/users.handler.ts +46 -3
  80. package/ts/opsserver/handlers/vpn.handler.ts +33 -0
  81. package/ts/opsserver/handlers/workhoster.handler.ts +18 -33
  82. package/ts/opsserver/helpers/auth.ts +91 -0
  83. package/ts_web/00_commitinfo_data.ts +1 -1
  84. package/ts_web/appstate.ts +69 -1
  85. package/ts_web/elements/access/ops-view-apitokens.ts +1 -20
  86. package/ts_web/elements/access/ops-view-users.ts +139 -1
  87. package/ts_web/elements/network/ops-view-routes.ts +9 -0
  88. package/ts_web/elements/ops-dashboard.ts +1 -3
@@ -2637,7 +2637,7 @@ export async function createGatewayClientToken(
2637
2637
  });
2638
2638
  }
2639
2639
 
2640
- // Users (read-only list)
2640
+ // Users
2641
2641
  export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
2642
2642
  const context = getActionContext();
2643
2643
  const currentState = statePartArg.getState()!;
@@ -2666,6 +2666,74 @@ export const fetchUsersAction = usersStatePart.createAction(async (statePartArg)
2666
2666
  }
2667
2667
  });
2668
2668
 
2669
+ export const createUserAction = usersStatePart.createAction<{
2670
+ email: string;
2671
+ name?: string;
2672
+ role: interfaces.requests.TUserManagementRole;
2673
+ password: string;
2674
+ enableIdpGlobalAuth?: boolean;
2675
+ }>(async (statePartArg, dataArg, actionContext): Promise<IUsersState> => {
2676
+ const context = getActionContext();
2677
+ const currentState = statePartArg.getState()!;
2678
+ if (!context.identity) return currentState;
2679
+
2680
+ try {
2681
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2682
+ interfaces.requests.IReq_CreateUser
2683
+ >('/typedrequest', 'createUser');
2684
+
2685
+ const response = await request.fire({
2686
+ identity: context.identity,
2687
+ email: dataArg.email,
2688
+ name: dataArg.name,
2689
+ role: dataArg.role,
2690
+ password: dataArg.password,
2691
+ enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
2692
+ });
2693
+
2694
+ if (!response.success) {
2695
+ throw new Error(response.message || 'Failed to create user');
2696
+ }
2697
+
2698
+ return await actionContext!.dispatch(fetchUsersAction, null);
2699
+ } catch (error) {
2700
+ return {
2701
+ ...currentState,
2702
+ error: error instanceof Error ? error.message : 'Failed to create user',
2703
+ };
2704
+ }
2705
+ });
2706
+
2707
+ export const deleteUserAction = usersStatePart.createAction<string>(
2708
+ async (statePartArg, userIdArg, actionContext): Promise<IUsersState> => {
2709
+ const context = getActionContext();
2710
+ const currentState = statePartArg.getState()!;
2711
+ if (!context.identity) return currentState;
2712
+
2713
+ try {
2714
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2715
+ interfaces.requests.IReq_DeleteUser
2716
+ >('/typedrequest', 'deleteUser');
2717
+
2718
+ const response = await request.fire({
2719
+ identity: context.identity,
2720
+ id: userIdArg,
2721
+ });
2722
+
2723
+ if (!response.success) {
2724
+ throw new Error(response.message || 'Failed to delete user');
2725
+ }
2726
+
2727
+ return await actionContext!.dispatch(fetchUsersAction, null);
2728
+ } catch (error) {
2729
+ return {
2730
+ ...currentState,
2731
+ error: error instanceof Error ? error.message : 'Failed to delete user',
2732
+ };
2733
+ }
2734
+ },
2735
+ );
2736
+
2669
2737
  export async function createApiToken(
2670
2738
  name: string,
2671
2739
  scopes: interfaces.data.TApiTokenScope[],
@@ -200,26 +200,7 @@ export class OpsViewApiTokens extends DeesElement {
200
200
  private async showCreateTokenDialog() {
201
201
  const { DeesModal } = await import('@design.estate/dees-catalog');
202
202
 
203
- const allScopes = [
204
- '*',
205
- 'routes:read',
206
- 'routes:write',
207
- 'config:read',
208
- 'certificates:read',
209
- 'certificates:write',
210
- 'tokens:read',
211
- 'tokens:manage',
212
- 'domains:read',
213
- 'domains:write',
214
- 'dns-records:read',
215
- 'dns-records:write',
216
- 'email-domains:read',
217
- 'email-domains:write',
218
- 'gateway-clients:read',
219
- 'gateway-clients:write',
220
- 'workhosters:read',
221
- 'workhosters:write',
222
- ];
203
+ const allScopes = [...interfaces.data.apiTokenScopes];
223
204
 
224
205
  await DeesModal.createAndShow({
225
206
  heading: 'Create API Token',
@@ -116,12 +116,31 @@ export class OpsViewUsers extends DeesElement {
116
116
  .showColumnFilters=${true}
117
117
  .displayFunction=${(user: appstate.IUser) => ({
118
118
  ID: html`<span class="userIdCell">${user.id}</span>`,
119
- Username: user.username,
119
+ Email: user.email || user.username,
120
+ Name: user.name || '',
120
121
  Role: this.renderRoleBadge(user.role),
122
+ Status: user.status || 'active',
123
+ Auth: (user.authSources || []).join(', ') || 'bootstrap',
121
124
  Session: user.id === currentUserId
122
125
  ? html`<span class="sessionBadge">current</span>`
123
126
  : '',
124
127
  })}
128
+ .dataActions=${[
129
+ {
130
+ name: 'Create User',
131
+ iconName: 'lucide:userPlus',
132
+ type: ['header'],
133
+ actionFunc: async () => await this.showCreateUserDialog(),
134
+ },
135
+ {
136
+ name: 'Delete',
137
+ iconName: 'lucide:trash2',
138
+ type: ['inRow', 'contextmenu'] as any,
139
+ actionFunc: async (actionData: any) => {
140
+ await this.showDeleteUserDialog(actionData.item as appstate.IUser);
141
+ },
142
+ },
143
+ ]}
125
144
  ></dees-table>
126
145
  </div>
127
146
  `;
@@ -132,6 +151,125 @@ export class OpsViewUsers extends DeesElement {
132
151
  return html`<span class="roleBadge ${cls}">${role}</span>`;
133
152
  }
134
153
 
154
+ private async showCreateUserDialog(): Promise<void> {
155
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
156
+
157
+ await DeesModal.createAndShow({
158
+ heading: 'Create User',
159
+ content: html`
160
+ <dees-form>
161
+ <dees-input-text .key=${'email'} .label=${'Email'} .required=${true}></dees-input-text>
162
+ <dees-input-text .key=${'name'} .label=${'Display name'}></dees-input-text>
163
+ <dees-input-dropdown
164
+ .key=${'role'}
165
+ .label=${'Role'}
166
+ .options=${[
167
+ { option: 'User', key: 'user' },
168
+ { option: 'Admin', key: 'admin' },
169
+ ]}
170
+ .selectedOption=${{ option: 'User', key: 'user' }}
171
+ .required=${true}
172
+ ></dees-input-dropdown>
173
+ <dees-input-text .key=${'password'} .label=${'Password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
174
+ <dees-input-text .key=${'passwordConfirm'} .label=${'Confirm password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
175
+ <dees-input-checkbox
176
+ .key=${'enableIdpGlobalAuth'}
177
+ .label=${'Allow idp.global login for this email'}
178
+ .description=${'Uses https://idp.global by default; the local dcrouter account and role remain authoritative.'}
179
+ ></dees-input-checkbox>
180
+ </dees-form>
181
+ `,
182
+ menuOptions: [
183
+ {
184
+ name: 'Cancel',
185
+ iconName: 'lucide:x',
186
+ action: async (modalArg: any) => await modalArg.destroy(),
187
+ },
188
+ {
189
+ name: 'Create',
190
+ iconName: 'lucide:userPlus',
191
+ action: async (modalArg: any) => {
192
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
193
+ if (!form) return;
194
+ const data = await form.collectFormData();
195
+ const email = String(data.email || '').trim();
196
+ const name = String(data.name || '').trim();
197
+ const password = String(data.password || '');
198
+ const passwordConfirm = String(data.passwordConfirm || '');
199
+ const roleValue = String(data.role?.key ?? data.role ?? 'user');
200
+
201
+ if (!email || !password) {
202
+ form.setStatus?.('error', 'Email and password are required.');
203
+ return;
204
+ }
205
+ if (password !== passwordConfirm) {
206
+ form.setStatus?.('error', 'Passwords do not match.');
207
+ return;
208
+ }
209
+
210
+ form.setStatus?.('pending', 'Creating user...');
211
+ await appstate.usersStatePart.dispatchAction(appstate.createUserAction, {
212
+ email,
213
+ name,
214
+ role: roleValue === 'admin' ? 'admin' : 'user',
215
+ password,
216
+ enableIdpGlobalAuth: Boolean(data.enableIdpGlobalAuth),
217
+ });
218
+
219
+ const state = appstate.usersStatePart.getState();
220
+ if (state?.error) {
221
+ form.setStatus?.('error', state.error);
222
+ return;
223
+ }
224
+
225
+ DeesToast.show({ message: `User created for ${email}`, type: 'success', duration: 3000 });
226
+ await modalArg.destroy();
227
+ },
228
+ },
229
+ ],
230
+ });
231
+ }
232
+
233
+ private async showDeleteUserDialog(userArg: appstate.IUser): Promise<void> {
234
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
235
+ const currentUserId = this.loginState.identity?.userId;
236
+ if (userArg.id === currentUserId) {
237
+ DeesToast.show({ message: 'You cannot delete the current user.', type: 'error', duration: 4000 });
238
+ return;
239
+ }
240
+
241
+ await DeesModal.createAndShow({
242
+ heading: 'Delete User',
243
+ content: html`
244
+ <div style="padding: 8px 0; font-size: 14px; line-height: 1.5;">
245
+ <p>Delete <strong>${userArg.email || userArg.username}</strong>?</p>
246
+ <p style="color: #f59e0b; margin-top: 12px;">This removes the local dcrouter account and cannot be undone.</p>
247
+ </div>
248
+ `,
249
+ menuOptions: [
250
+ {
251
+ name: 'Cancel',
252
+ iconName: 'lucide:x',
253
+ action: async (modalArg: any) => await modalArg.destroy(),
254
+ },
255
+ {
256
+ name: 'Delete',
257
+ iconName: 'lucide:trash2',
258
+ action: async (modalArg: any) => {
259
+ await appstate.usersStatePart.dispatchAction(appstate.deleteUserAction, userArg.id);
260
+ const state = appstate.usersStatePart.getState();
261
+ if (state?.error) {
262
+ DeesToast.show({ message: state.error, type: 'error', duration: 4000 });
263
+ return;
264
+ }
265
+ DeesToast.show({ message: 'User deleted.', type: 'success', duration: 3000 });
266
+ await modalArg.destroy();
267
+ },
268
+ },
269
+ ],
270
+ });
271
+ }
272
+
135
273
  async firstUpdated() {
136
274
  if (this.loginState.isLoggedIn) {
137
275
  await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
@@ -271,6 +271,7 @@ export class OpsViewRoutes extends DeesElement {
271
271
  const tags = [...(mr.route.tags || [])];
272
272
  tags.push(mr.origin);
273
273
  if (!mr.enabled) tags.push('disabled');
274
+ if (mr.route.vpnOnly) tags.push('vpn-only');
274
275
 
275
276
  return {
276
277
  ...mr.route,
@@ -360,6 +361,7 @@ export class OpsViewRoutes extends DeesElement {
360
361
  <div style="color: #ccc; padding: 8px 0;">
361
362
  <p>Origin: <strong style="color: #0af;">${merged.origin}</strong></p>
362
363
  <p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
364
+ ${merged.route.vpnOnly ? html`<p>Access: <strong style="color: #22c55e;">VPN only</strong></p>` : ''}
363
365
  <p>ID: <code style="color: #888;">${merged.id}</code></p>
364
366
  ${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
365
367
  ${meta?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
@@ -491,6 +493,7 @@ export class OpsViewRoutes extends DeesElement {
491
493
  ? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
492
494
  : '';
493
495
  const currentTargetPort = typeof firstTarget?.port === 'number' ? String(firstTarget.port) : '';
496
+ const currentVpnOnly = route.vpnOnly === true;
494
497
  const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
495
498
  const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
496
499
 
@@ -518,6 +521,7 @@ export class OpsViewRoutes extends DeesElement {
518
521
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
519
522
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
520
523
  <dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${currentPreserveMatchPort}></dees-input-checkbox>
524
+ <dees-input-checkbox .key=${'vpnOnly'} .label=${'VPN only'} .description=${'Only VPN clients with matching target profiles can access this route'} .value=${currentVpnOnly}></dees-input-checkbox>
521
525
  <dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${currentRemoteIngressEnabled}></dees-input-checkbox>
522
526
  <div class="remoteIngressGroup" style="display: ${currentRemoteIngressEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
523
527
  <dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'} .value=${currentEdgeFilter}></dees-input-list>
@@ -570,6 +574,7 @@ export class OpsViewRoutes extends DeesElement {
570
574
  const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
571
575
  ? formData.remoteIngressEdgeFilter.filter(Boolean)
572
576
  : [];
577
+ const vpnOnly = Boolean(formData.vpnOnly);
573
578
 
574
579
  const updatedRoute: any = {
575
580
  name: formData.name,
@@ -586,6 +591,7 @@ export class OpsViewRoutes extends DeesElement {
586
591
  },
587
592
  ],
588
593
  },
594
+ vpnOnly: vpnOnly ? true : null,
589
595
  remoteIngress: remoteIngressEnabled
590
596
  ? {
591
597
  enabled: true,
@@ -684,6 +690,7 @@ export class OpsViewRoutes extends DeesElement {
684
690
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
685
691
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
686
692
  <dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${false}></dees-input-checkbox>
693
+ <dees-input-checkbox .key=${'vpnOnly'} .label=${'VPN only'} .description=${'Only VPN clients with matching target profiles can access this route'} .value=${false}></dees-input-checkbox>
687
694
  <dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${false}></dees-input-checkbox>
688
695
  <div class="remoteIngressGroup" style="display: none; flex-direction: column; gap: 16px;">
689
696
  <dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'}></dees-input-list>
@@ -736,6 +743,7 @@ export class OpsViewRoutes extends DeesElement {
736
743
  const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
737
744
  ? formData.remoteIngressEdgeFilter.filter(Boolean)
738
745
  : [];
746
+ const vpnOnly = Boolean(formData.vpnOnly);
739
747
 
740
748
  const route: any = {
741
749
  name: formData.name,
@@ -752,6 +760,7 @@ export class OpsViewRoutes extends DeesElement {
752
760
  },
753
761
  ],
754
762
  },
763
+ ...(vpnOnly ? { vpnOnly: true } : {}),
755
764
  ...(remoteIngressEnabled
756
765
  ? {
757
766
  remoteIngress: {
@@ -426,9 +426,7 @@ export class OpsDashboard extends DeesElement {
426
426
  <dees-input-checkbox
427
427
  .key=${'enableIdpGlobalAuth'}
428
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.'}
429
+ .description=${'Uses https://idp.global by default; the local dcrouter account and role remain authoritative.'}
432
430
  ></dees-input-checkbox>
433
431
  </dees-form>
434
432
  </div>