From 11cce23e214e6fdae6d72619db8021d453bf3a9a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 30 Mar 2026 16:49:58 +0000 Subject: [PATCH] feat(vpn): expand VPN operations view with client management and config export actions --- changelog.md | 7 ++ test_watch/devserver.ts | 10 ++ ts/00_commitinfo_data.ts | 2 +- ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/ops-view-vpn.ts | 163 +++++++++++++++++++++++++------- 5 files changed, 146 insertions(+), 38 deletions(-) diff --git a/changelog.md b/changelog.md index d4fcd45..94ba157 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 11.17.0 - feat(vpn) +expand VPN operations view with client management and config export actions + +- adds predefined VPN clients to the dev server configuration for local testing +- adds table actions to create clients, export WireGuard configs, rotate client keys, toggle access, and delete clients +- updates the VPN view layout and stats grid binding to match the current component API + ## 2026-03-30 - 11.16.0 - feat(vpn) add destination-based VPN routing policy and standardize socket proxy forwarding diff --git a/test_watch/devserver.ts b/test_watch/devserver.ts index de296ef..f4d0cfa 100644 --- a/test_watch/devserver.ts +++ b/test_watch/devserver.ts @@ -25,6 +25,16 @@ const devRouter = new DcRouter({ }, ], }, + // VPN with pre-defined clients + vpnConfig: { + enabled: true, + serverEndpoint: 'vpn.dev.local', + clients: [ + { clientId: 'dev-laptop', serverDefinedClientTags: ['engineering', 'dev'], description: 'Developer laptop' }, + { clientId: 'ci-runner', serverDefinedClientTags: ['engineering', 'ci'], description: 'CI/CD pipeline' }, + { clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' }, + ], + }, // Disable cache/mongo for dev cacheConfig: { enabled: false }, }); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8dae047..11d2044 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.16.0', + version: '11.17.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 8dae047..11d2044 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.16.0', + version: '11.17.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/elements/ops-view-vpn.ts b/ts_web/elements/ops-view-vpn.ts index 144dbc7..ea95ef7 100644 --- a/ts_web/elements/ops-view-vpn.ts +++ b/ts_web/elements/ops-view-vpn.ts @@ -7,6 +7,7 @@ import { state, cssManager, } from '@design.estate/dees-element'; +import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as interfaces from '../../dist_ts_interfaces/index.js'; import { viewHostCss } from './shared/css.js'; @@ -188,6 +189,7 @@ export class OpsViewVpn extends DeesElement { return html` VPN +
${this.vpnState.newClientConfig ? html`
@@ -220,7 +222,7 @@ export class OpsViewVpn extends DeesElement {
` : ''} - + ${status ? html`
@@ -258,31 +260,149 @@ export class OpsViewVpn extends DeesElement { 'Created': new Date(client.createdAt).toLocaleDateString(), })} .dataActions=${[ + { + name: 'Create Client', + iconName: 'lucide:plus', + type: ['header'], + actionFunc: async () => { + const { DeesModal } = await import('@design.estate/dees-catalog'); + await DeesModal.createAndShow({ + heading: 'Create VPN Client', + content: html` + + + + + + `, + menuOptions: [ + { + name: 'Cancel', + iconName: 'lucide:x', + action: async (modalArg: any) => await modalArg.destroy(), + }, + { + name: 'Create', + iconName: 'lucide:plus', + action: async (modalArg: any) => { + const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); + if (!form) return; + const data = await form.collectFormData(); + if (!data.clientId) return; + const serverDefinedClientTags = data.tags + ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) + : undefined; + await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, { + clientId: data.clientId, + description: data.description || undefined, + serverDefinedClientTags, + }); + await modalArg.destroy(); + }, + }, + ], + }); + }, + }, { name: 'Toggle', iconName: 'lucide:power', - action: async (client: interfaces.data.IVpnClient) => { + type: ['contextmenu', 'inRow'], + actionFunc: async (client: interfaces.data.IVpnClient) => { await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, { clientId: client.clientId, enabled: !client.enabled, }); }, }, + { + name: 'Export Config', + iconName: 'lucide:download', + type: ['contextmenu', 'inRow'], + actionFunc: async (client: interfaces.data.IVpnClient) => { + const { DeesToast } = await import('@design.estate/dees-catalog'); + try { + const request = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_ExportVpnClientConfig + >('/typedrequest', 'exportVpnClientConfig'); + const response = await request.fire({ + identity: appstate.loginStatePart.getState()!.identity!, + clientId: client.clientId, + format: 'wireguard', + }); + if (response.success && response.config) { + const blob = new Blob([response.config], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${client.clientId}.conf`; + a.click(); + URL.revokeObjectURL(url); + DeesToast.createAndShow({ message: 'Config downloaded', type: 'success', duration: 3000 }); + } else { + DeesToast.createAndShow({ message: response.message || 'Export failed', type: 'error', duration: 5000 }); + } + } catch (err: any) { + DeesToast.createAndShow({ message: err.message || 'Export failed', type: 'error', duration: 5000 }); + } + }, + }, + { + name: 'Rotate Keys', + iconName: 'lucide:rotate-cw', + type: ['contextmenu'], + actionFunc: async (client: interfaces.data.IVpnClient) => { + const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); + DeesModal.createAndShow({ + heading: 'Rotate Client Keys', + content: html`

Generate new keys for "${client.clientId}"? The old keys will be invalidated and the client will need the new config to reconnect.

`, + menuOptions: [ + { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() }, + { + name: 'Rotate', + iconName: 'lucide:rotate-cw', + action: async (modalArg: any) => { + try { + const request = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_RotateVpnClientKey + >('/typedrequest', 'rotateVpnClientKey'); + const response = await request.fire({ + identity: appstate.loginStatePart.getState()!.identity!, + clientId: client.clientId, + }); + if (response.success && response.wireguardConfig) { + appstate.vpnStatePart.setState({ + ...appstate.vpnStatePart.getState()!, + newClientConfig: response.wireguardConfig, + }); + } + await modalArg.destroy(); + } catch (err: any) { + DeesToast.createAndShow({ message: err.message || 'Rotate failed', type: 'error', duration: 5000 }); + } + }, + }, + ], + }); + }, + }, { name: 'Delete', iconName: 'lucide:trash2', - action: async (client: interfaces.data.IVpnClient) => { + type: ['contextmenu'], + actionFunc: async (client: interfaces.data.IVpnClient) => { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Delete VPN Client', content: html`

Are you sure you want to delete client "${client.clientId}"?

`, menuOptions: [ - { name: 'Cancel', action: async (modal: any) => modal.destroy() }, + { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() }, { name: 'Delete', - action: async (modal: any) => { + iconName: 'lucide:trash2', + action: async (modalArg: any) => { await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId); - modal.destroy(); + await modalArg.destroy(); }, }, ], @@ -290,37 +410,8 @@ export class OpsViewVpn extends DeesElement { }, }, ]} - .createNewItem=${async () => { - const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog'); - DeesModal.createAndShow({ - heading: 'Create VPN Client', - content: html` - - - - - - `, - menuOptions: [ - { name: 'Cancel', action: async (modal: any) => modal.destroy() }, - { - name: 'Create', - action: async (modal: any) => { - const form = modal.shadowRoot!.querySelector('dees-form') as any; - const data = await form.collectFormData(); - const serverDefinedClientTags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined; - await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, { - clientId: data.clientId, - description: data.description || undefined, - serverDefinedClientTags, - }); - modal.destroy(); - }, - }, - ], - }); - }} > +
`; } }