diff --git a/changelog.md b/changelog.md index 8484f45..ba25349 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 11.20.0 - feat(vpn-ui) +add QR code export for WireGuard client configurations + +- adds a QR code action for newly created WireGuard configs in the VPN operations view +- adds a QR code export option for existing VPN clients alongside file downloads +- introduces qrcode and @types/qrcode dependencies and exposes the plugin for web UI use + ## 2026-03-30 - 11.19.1 - fix(vpn) configure SmartVPN client exports with explicit server endpoint and split-tunnel allowed IPs diff --git a/package.json b/package.json index d472bf2..66d6347 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,9 @@ "@serve.zone/interfaces": "^5.3.0", "@serve.zone/remoteingress": "^4.15.3", "@tsclass/tsclass": "^9.5.0", + "@types/qrcode": "^1.5.6", "lru-cache": "^11.2.7", + "qrcode": "^1.5.4", "uuid": "^13.0.0" }, "keywords": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0500786..673d782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,9 +113,15 @@ importers: '@tsclass/tsclass': specifier: ^9.5.0 version: 9.5.0 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 lru-cache: specifier: ^11.2.7 version: 11.2.7 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -2047,6 +2053,9 @@ packages: '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/randomatic@3.1.5': resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==} @@ -2301,6 +2310,10 @@ packages: camel-case@3.0.0: resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -2341,6 +2354,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2417,6 +2433,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} + engines: {node: '>=0.10.0'} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2470,6 +2490,9 @@ packages: devtools-protocol@0.0.1581282: resolution: {integrity: sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -3589,6 +3612,10 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + pngjs@6.0.0: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} @@ -3710,6 +3737,11 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -3780,6 +3812,9 @@ packages: resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -3835,6 +3870,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4167,6 +4205,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4222,6 +4263,9 @@ packages: xterm@5.3.0: resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4231,6 +4275,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4239,6 +4287,10 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -7444,6 +7496,10 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.5.0 + '@types/randomatic@3.1.5': {} '@types/relateurl@0.2.33': {} @@ -7688,6 +7744,8 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + camelcase@5.3.1: {} + camelcase@6.3.0: {} ccount@2.0.1: {} @@ -7718,6 +7776,12 @@ snapshots: cli-width@4.1.0: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -7792,6 +7856,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -7838,6 +7904,8 @@ snapshots: devtools-protocol@0.0.1581282: {} + dijkstrajs@1.0.3: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -9216,6 +9284,8 @@ snapshots: dependencies: find-up: 4.1.0 + pngjs@5.0.0: {} + pngjs@6.0.0: {} pngjs@7.0.0: {} @@ -9401,6 +9471,12 @@ snapshots: pvutils@1.1.5: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -9499,6 +9575,8 @@ snapshots: require-directory@2.1.1: {} + require-main-filename@2.0.0: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -9556,6 +9634,8 @@ snapshots: semver@7.7.4: {} + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9947,6 +10027,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -9988,14 +10070,35 @@ snapshots: xterm@5.3.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yaml@2.8.3: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 073e8b4..5bb7f4f 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.19.1', + version: '11.20.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 073e8b4..5bb7f4f 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.19.1', + version: '11.20.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 cc592ee..a443a08 100644 --- a/ts_web/elements/ops-view-vpn.ts +++ b/ts_web/elements/ops-view-vpn.ts @@ -216,6 +216,29 @@ export class OpsViewVpn extends DeesElement { URL.revokeObjectURL(url); }} >Download .conf + { + const dataUrl = await plugins.qrcode.toDataURL( + this.vpnState.newClientConfig!, + { width: 400, margin: 2 } + ); + const { DeesModal } = await import('@design.estate/dees-catalog'); + DeesModal.createAndShow({ + heading: 'WireGuard QR Code', + content: html` +
+ +

+ Scan with the WireGuard app on your phone +

+
+ `, + menuOptions: [ + { name: 'Close', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() }, + ], + }); + }} + >Show QR Code
appstate.vpnStatePart.dispatchAction(appstate.clearNewClientConfigAction, null)} >Dismiss @@ -352,6 +375,43 @@ export class OpsViewVpn extends DeesElement { } }; + const showQrCode = async () => { + 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 dataUrl = await plugins.qrcode.toDataURL( + response.config, + { width: 400, margin: 2 } + ); + DeesModal.createAndShow({ + heading: `QR Code: ${client.clientId}`, + content: html` +
+ +

+ Scan with the WireGuard app on your phone +

+
+ `, + menuOptions: [ + { name: 'Close', iconName: 'lucide:x', action: async (m: any) => await m.destroy() }, + ], + }); + } else { + DeesToast.createAndShow({ message: response.message || 'Export failed', type: 'error', duration: 5000 }); + } + } catch (err: any) { + DeesToast.createAndShow({ message: err.message || 'QR generation failed', type: 'error', duration: 5000 }); + } + }; + DeesModal.createAndShow({ heading: `Export Config: ${client.clientId}`, content: html`

Choose a config format to download.

`, @@ -372,6 +432,14 @@ export class OpsViewVpn extends DeesElement { await exportConfig('smartvpn'); }, }, + { + name: 'QR Code (WireGuard)', + iconName: 'lucide:qr-code', + action: async (modalArg: any) => { + await modalArg.destroy(); + await showQrCode(); + }, + }, { name: 'Cancel', iconName: 'lucide:x', diff --git a/ts_web/plugins.ts b/ts_web/plugins.ts index 6bfa9f8..e75ae59 100644 --- a/ts_web/plugins.ts +++ b/ts_web/plugins.ts @@ -8,11 +8,15 @@ import * as szCatalog from '@serve.zone/catalog'; // TypedSocket for real-time push communication import * as typedsocket from '@api.global/typedsocket'; +// QR code generation for WireGuard configs +import * as qrcode from 'qrcode'; + export { deesElement, deesCatalog, szCatalog, typedsocket, + qrcode, } // domtools gives us TypedRequest and other utilities