Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa7206f86 | |||
| 11cce23e21 |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-30 - 11.16.0 - feat(vpn)
|
||||||
add destination-based VPN routing policy and standardize socket proxy forwarding
|
add destination-based VPN routing policy and standardize socket proxy forwarding
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.16.0",
|
"version": "11.17.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -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
|
// Disable cache/mongo for dev
|
||||||
cacheConfig: { enabled: false },
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.16.0',
|
version: '11.17.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.16.0',
|
version: '11.17.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
state,
|
state,
|
||||||
cssManager,
|
cssManager,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from './shared/css.js';
|
import { viewHostCss } from './shared/css.js';
|
||||||
@@ -188,6 +189,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>VPN</ops-sectionheading>
|
<ops-sectionheading>VPN</ops-sectionheading>
|
||||||
|
<div class="vpnContainer">
|
||||||
|
|
||||||
${this.vpnState.newClientConfig ? html`
|
${this.vpnState.newClientConfig ? html`
|
||||||
<div class="configDialog">
|
<div class="configDialog">
|
||||||
@@ -220,7 +222,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
|
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||||
|
|
||||||
${status ? html`
|
${status ? html`
|
||||||
<div class="serverInfo">
|
<div class="serverInfo">
|
||||||
@@ -258,31 +260,149 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.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`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
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',
|
name: 'Toggle',
|
||||||
iconName: 'lucide:power',
|
iconName: 'lucide:power',
|
||||||
action: async (client: interfaces.data.IVpnClient) => {
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (client: interfaces.data.IVpnClient) => {
|
||||||
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
enabled: !client.enabled,
|
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`<p>Generate new keys for "${client.clientId}"? The old keys will be invalidated and the client will need the new config to reconnect.</p>`,
|
||||||
|
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',
|
name: 'Delete',
|
||||||
iconName: 'lucide:trash2',
|
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');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
DeesModal.createAndShow({
|
DeesModal.createAndShow({
|
||||||
heading: 'Delete VPN Client',
|
heading: 'Delete VPN Client',
|
||||||
content: html`<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
|
content: html`<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'Cancel', action: async (modal: any) => modal.destroy() },
|
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
action: async (modal: any) => {
|
iconName: 'lucide:trash2',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId);
|
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`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text id="clientId" .label=${'Client ID'} .key=${'clientId'} required></dees-input-text>
|
|
||||||
<dees-input-text id="description" .label=${'Description'} .key=${'description'}></dees-input-text>
|
|
||||||
<dees-input-text id="tags" .label=${'Tags (comma-separated)'} .key=${'tags'}></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
></dees-table>
|
></dees-table>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user