@serve.zone/dcrouter 11.15.0 → 11.17.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.
@@ -7,6 +7,7 @@ import {
7
7
  state,
8
8
  cssManager,
9
9
  } from '@design.estate/dees-element';
10
+ import * as plugins from '../plugins.js';
10
11
  import * as appstate from '../appstate.js';
11
12
  import * as interfaces from '../../dist_ts_interfaces/index.js';
12
13
  import { viewHostCss } from './shared/css.js';
@@ -181,13 +182,14 @@ export class OpsViewVpn extends DeesElement {
181
182
  type: 'text',
182
183
  value: status?.running ? 'Running' : 'Stopped',
183
184
  icon: 'lucide:server',
184
- description: status?.running ? `${status.forwardingMode} mode` : 'VPN server not running',
185
+ description: status?.running ? 'Active' : 'VPN server not running',
185
186
  color: status?.running ? '#10b981' : '#ef4444',
186
187
  },
187
188
  ];
188
189
 
189
190
  return html`
190
191
  <ops-sectionheading>VPN</ops-sectionheading>
192
+ <div class="vpnContainer">
191
193
 
192
194
  ${this.vpnState.newClientConfig ? html`
193
195
  <div class="configDialog">
@@ -220,7 +222,7 @@ export class OpsViewVpn extends DeesElement {
220
222
  </div>
221
223
  ` : ''}
222
224
 
223
- <dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
225
+ <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
224
226
 
225
227
  ${status ? html`
226
228
  <div class="serverInfo">
@@ -232,10 +234,6 @@ export class OpsViewVpn extends DeesElement {
232
234
  <span class="infoLabel">WireGuard Port</span>
233
235
  <span class="infoValue">${status.wgListenPort}</span>
234
236
  </div>
235
- <div class="infoItem">
236
- <span class="infoLabel">Forwarding Mode</span>
237
- <span class="infoValue">${status.forwardingMode}</span>
238
- </div>
239
237
  ${status.serverPublicKeys ? html`
240
238
  <div class="infoItem">
241
239
  <span class="infoLabel">WG Public Key</span>
@@ -262,31 +260,149 @@ export class OpsViewVpn extends DeesElement {
262
260
  'Created': new Date(client.createdAt).toLocaleDateString(),
263
261
  })}
264
262
  .dataActions=${[
263
+ {
264
+ name: 'Create Client',
265
+ iconName: 'lucide:plus',
266
+ type: ['header'],
267
+ actionFunc: async () => {
268
+ const { DeesModal } = await import('@design.estate/dees-catalog');
269
+ await DeesModal.createAndShow({
270
+ heading: 'Create VPN Client',
271
+ content: html`
272
+ <dees-form>
273
+ <dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
274
+ <dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
275
+ <dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
276
+ </dees-form>
277
+ `,
278
+ menuOptions: [
279
+ {
280
+ name: 'Cancel',
281
+ iconName: 'lucide:x',
282
+ action: async (modalArg: any) => await modalArg.destroy(),
283
+ },
284
+ {
285
+ name: 'Create',
286
+ iconName: 'lucide:plus',
287
+ action: async (modalArg: any) => {
288
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
289
+ if (!form) return;
290
+ const data = await form.collectFormData();
291
+ if (!data.clientId) return;
292
+ const serverDefinedClientTags = data.tags
293
+ ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
294
+ : undefined;
295
+ await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
296
+ clientId: data.clientId,
297
+ description: data.description || undefined,
298
+ serverDefinedClientTags,
299
+ });
300
+ await modalArg.destroy();
301
+ },
302
+ },
303
+ ],
304
+ });
305
+ },
306
+ },
265
307
  {
266
308
  name: 'Toggle',
267
309
  iconName: 'lucide:power',
268
- action: async (client: interfaces.data.IVpnClient) => {
310
+ type: ['contextmenu', 'inRow'],
311
+ actionFunc: async (client: interfaces.data.IVpnClient) => {
269
312
  await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
270
313
  clientId: client.clientId,
271
314
  enabled: !client.enabled,
272
315
  });
273
316
  },
274
317
  },
318
+ {
319
+ name: 'Export Config',
320
+ iconName: 'lucide:download',
321
+ type: ['contextmenu', 'inRow'],
322
+ actionFunc: async (client: interfaces.data.IVpnClient) => {
323
+ const { DeesToast } = await import('@design.estate/dees-catalog');
324
+ try {
325
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
326
+ interfaces.requests.IReq_ExportVpnClientConfig
327
+ >('/typedrequest', 'exportVpnClientConfig');
328
+ const response = await request.fire({
329
+ identity: appstate.loginStatePart.getState()!.identity!,
330
+ clientId: client.clientId,
331
+ format: 'wireguard',
332
+ });
333
+ if (response.success && response.config) {
334
+ const blob = new Blob([response.config], { type: 'text/plain' });
335
+ const url = URL.createObjectURL(blob);
336
+ const a = document.createElement('a');
337
+ a.href = url;
338
+ a.download = `${client.clientId}.conf`;
339
+ a.click();
340
+ URL.revokeObjectURL(url);
341
+ DeesToast.createAndShow({ message: 'Config downloaded', type: 'success', duration: 3000 });
342
+ } else {
343
+ DeesToast.createAndShow({ message: response.message || 'Export failed', type: 'error', duration: 5000 });
344
+ }
345
+ } catch (err: any) {
346
+ DeesToast.createAndShow({ message: err.message || 'Export failed', type: 'error', duration: 5000 });
347
+ }
348
+ },
349
+ },
350
+ {
351
+ name: 'Rotate Keys',
352
+ iconName: 'lucide:rotate-cw',
353
+ type: ['contextmenu'],
354
+ actionFunc: async (client: interfaces.data.IVpnClient) => {
355
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
356
+ DeesModal.createAndShow({
357
+ heading: 'Rotate Client Keys',
358
+ content: html`<p>Generate new keys for "${client.clientId}"? The old keys will be invalidated and the client will need the new config to reconnect.</p>`,
359
+ menuOptions: [
360
+ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
361
+ {
362
+ name: 'Rotate',
363
+ iconName: 'lucide:rotate-cw',
364
+ action: async (modalArg: any) => {
365
+ try {
366
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
367
+ interfaces.requests.IReq_RotateVpnClientKey
368
+ >('/typedrequest', 'rotateVpnClientKey');
369
+ const response = await request.fire({
370
+ identity: appstate.loginStatePart.getState()!.identity!,
371
+ clientId: client.clientId,
372
+ });
373
+ if (response.success && response.wireguardConfig) {
374
+ appstate.vpnStatePart.setState({
375
+ ...appstate.vpnStatePart.getState()!,
376
+ newClientConfig: response.wireguardConfig,
377
+ });
378
+ }
379
+ await modalArg.destroy();
380
+ } catch (err: any) {
381
+ DeesToast.createAndShow({ message: err.message || 'Rotate failed', type: 'error', duration: 5000 });
382
+ }
383
+ },
384
+ },
385
+ ],
386
+ });
387
+ },
388
+ },
275
389
  {
276
390
  name: 'Delete',
277
391
  iconName: 'lucide:trash2',
278
- action: async (client: interfaces.data.IVpnClient) => {
392
+ type: ['contextmenu'],
393
+ actionFunc: async (client: interfaces.data.IVpnClient) => {
279
394
  const { DeesModal } = await import('@design.estate/dees-catalog');
280
395
  DeesModal.createAndShow({
281
396
  heading: 'Delete VPN Client',
282
397
  content: html`<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
283
398
  menuOptions: [
284
- { name: 'Cancel', action: async (modal: any) => modal.destroy() },
399
+ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
285
400
  {
286
401
  name: 'Delete',
287
- action: async (modal: any) => {
402
+ iconName: 'lucide:trash2',
403
+ action: async (modalArg: any) => {
288
404
  await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId);
289
- modal.destroy();
405
+ await modalArg.destroy();
290
406
  },
291
407
  },
292
408
  ],
@@ -294,37 +410,8 @@ export class OpsViewVpn extends DeesElement {
294
410
  },
295
411
  },
296
412
  ]}
297
- .createNewItem=${async () => {
298
- const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog');
299
- DeesModal.createAndShow({
300
- heading: 'Create VPN Client',
301
- content: html`
302
- <dees-form>
303
- <dees-input-text id="clientId" .label=${'Client ID'} .key=${'clientId'} required></dees-input-text>
304
- <dees-input-text id="description" .label=${'Description'} .key=${'description'}></dees-input-text>
305
- <dees-input-text id="tags" .label=${'Tags (comma-separated)'} .key=${'tags'}></dees-input-text>
306
- </dees-form>
307
- `,
308
- menuOptions: [
309
- { name: 'Cancel', action: async (modal: any) => modal.destroy() },
310
- {
311
- name: 'Create',
312
- action: async (modal: any) => {
313
- const form = modal.shadowRoot!.querySelector('dees-form') as any;
314
- const data = await form.collectFormData();
315
- const serverDefinedClientTags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined;
316
- await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
317
- clientId: data.clientId,
318
- description: data.description || undefined,
319
- serverDefinedClientTags,
320
- });
321
- modal.destroy();
322
- },
323
- },
324
- ],
325
- });
326
- }}
327
413
  ></dees-table>
414
+ </div>
328
415
  `;
329
416
  }
330
417
  }