diff --git a/changelog.md b/changelog.md index 94ba157..ad7f7d5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2026-03-30 - 11.18.0 - feat(vpn-ui) +add format selection for VPN client config exports + +- Show an export modal that lets operators choose between WireGuard (.conf) and SmartVPN (.json) client configs. +- Update VPN client row actions to read the selected item from actionData for toggle, export, rotate keys, and delete handlers. + ## 2026-03-30 - 11.17.0 - feat(vpn) expand VPN operations view with client management and config export actions diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 11d2044..efd1e99 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.17.0', + version: '11.18.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 11d2044..efd1e99 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.17.0', + version: '11.18.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 ea95ef7..cc592ee 100644 --- a/ts_web/elements/ops-view-vpn.ts +++ b/ts_web/elements/ops-view-vpn.ts @@ -308,7 +308,8 @@ export class OpsViewVpn extends DeesElement { name: 'Toggle', iconName: 'lucide:power', type: ['contextmenu', 'inRow'], - actionFunc: async (client: interfaces.data.IVpnClient) => { + actionFunc: async (actionData: any) => { + const client = actionData.item as interfaces.data.IVpnClient; await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, { clientId: client.clientId, enabled: !client.enabled, @@ -319,39 +320,73 @@ export class OpsViewVpn extends DeesElement { 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 }); + actionFunc: async (actionData: any) => { + const client = actionData.item as interfaces.data.IVpnClient; + const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); + + const exportConfig = async (format: 'wireguard' | 'smartvpn') => { + 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, + }); + if (response.success && response.config) { + const ext = format === 'wireguard' ? 'conf' : 'json'; + 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}.${ext}`; + a.click(); + URL.revokeObjectURL(url); + DeesToast.createAndShow({ message: `${format} 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 }); } - } catch (err: any) { - DeesToast.createAndShow({ message: err.message || 'Export failed', type: 'error', duration: 5000 }); - } + }; + + DeesModal.createAndShow({ + heading: `Export Config: ${client.clientId}`, + content: html`
Choose a config format to download.
`, + menuOptions: [ + { + name: 'WireGuard (.conf)', + iconName: 'lucide:shield', + action: async (modalArg: any) => { + await modalArg.destroy(); + await exportConfig('wireguard'); + }, + }, + { + name: 'SmartVPN (.json)', + iconName: 'lucide:braces', + action: async (modalArg: any) => { + await modalArg.destroy(); + await exportConfig('smartvpn'); + }, + }, + { + name: 'Cancel', + iconName: 'lucide:x', + action: async (modalArg: any) => await modalArg.destroy(), + }, + ], + }); }, }, { name: 'Rotate Keys', iconName: 'lucide:rotate-cw', type: ['contextmenu'], - actionFunc: async (client: interfaces.data.IVpnClient) => { + actionFunc: async (actionData: any) => { + const client = actionData.item as interfaces.data.IVpnClient; const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Rotate Client Keys', @@ -390,7 +425,8 @@ export class OpsViewVpn extends DeesElement { name: 'Delete', iconName: 'lucide:trash2', type: ['contextmenu'], - actionFunc: async (client: interfaces.data.IVpnClient) => { + actionFunc: async (actionData: any) => { + const client = actionData.item as interfaces.data.IVpnClient; const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Delete VPN Client',