feat(vpn): add per-client routing controls and bridge forwarding support for VPN clients
This commit is contained in:
@@ -13,6 +13,31 @@ import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from './shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
/**
|
||||
* Toggle form field visibility based on checkbox states.
|
||||
* Used in Create and Edit VPN client dialogs.
|
||||
*/
|
||||
function setupFormVisibility(formEl: any) {
|
||||
const show = 'flex'; // match dees-form's flex layout
|
||||
const updateVisibility = async () => {
|
||||
const data = await formEl.collectFormData();
|
||||
const contentEl = formEl.closest('.content') || formEl.parentElement;
|
||||
if (!contentEl) return;
|
||||
const hostIpGroup = contentEl.querySelector('.hostIpGroup') as HTMLElement;
|
||||
const hostIpDetails = contentEl.querySelector('.hostIpDetails') as HTMLElement;
|
||||
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
||||
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
||||
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
||||
if (hostIpGroup) hostIpGroup.style.display = data.forceDestinationSmartproxy ? 'none' : show;
|
||||
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
||||
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
||||
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
||||
if (aclGroup) aclGroup.style.display = data.allowAdditionalAcls ? show : 'none';
|
||||
};
|
||||
formEl.changeSubject.subscribe(() => updateVisibility());
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-vpn': OpsViewVpn;
|
||||
@@ -289,9 +314,18 @@ export class OpsViewVpn extends DeesElement {
|
||||
} else {
|
||||
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
||||
}
|
||||
let routingHtml;
|
||||
if (client.forceDestinationSmartproxy !== false) {
|
||||
routingHtml = html`<span class="statusBadge enabled">SmartProxy</span>`;
|
||||
} else if (client.useHostIp) {
|
||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
|
||||
} else {
|
||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
|
||||
}
|
||||
return {
|
||||
'Client ID': client.clientId,
|
||||
'Status': statusHtml,
|
||||
'Routing': routingHtml,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Tags': client.serverDefinedClientTags?.length
|
||||
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
@@ -307,13 +341,32 @@ export class OpsViewVpn extends DeesElement {
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
const createModal = 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-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
||||
<div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
|
||||
<div class="staticIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'staticIp'} .label=${'Static IP'}></dees-input-text>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${false}></dees-input-checkbox>
|
||||
<div class="vlanIdGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'}></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox>
|
||||
<div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
||||
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
@@ -333,16 +386,47 @@ export class OpsViewVpn extends DeesElement {
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
||||
|
||||
const allowAcls = data.allowAdditionalAcls ?? false;
|
||||
const destinationAllowList = allowAcls && data.destinationAllowList
|
||||
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const destinationBlockList = allowAcls && data.destinationBlockList
|
||||
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
||||
clientId: data.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
staticIp,
|
||||
forceVlan: forceVlan || undefined,
|
||||
vlanId,
|
||||
destinationAllowList,
|
||||
destinationBlockList,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Setup conditional form visibility after modal renders
|
||||
const createForm = createModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
||||
if (createForm) {
|
||||
await createForm.updateComplete;
|
||||
setupFormVisibility(createForm);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -396,6 +480,13 @@ export class OpsViewVpn extends DeesElement {
|
||||
` : ''}
|
||||
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
|
||||
${client.useHostIp ? html`
|
||||
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
|
||||
` : ''}
|
||||
<div class="infoItem"><span class="infoLabel">Allow List</span><span class="infoValue">${client.destinationAllowList?.length ? client.destinationAllowList.join(', ') : 'None'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Block List</span><span class="infoValue">${client.destinationBlockList?.length ? client.destinationBlockList.join(', ') : 'None'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Created</span><span class="infoValue">${new Date(client.createdAt).toLocaleString()}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Updated</span><span class="infoValue">${new Date(client.updatedAt).toLocaleString()}</span></div>
|
||||
</div>
|
||||
@@ -553,12 +644,41 @@ export class OpsViewVpn extends DeesElement {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const currentDescription = client.description ?? '';
|
||||
const currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
|
||||
DeesModal.createAndShow({
|
||||
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
||||
const currentUseHostIp = client.useHostIp ?? false;
|
||||
const currentUseDhcp = client.useDhcp ?? false;
|
||||
const currentStaticIp = client.staticIp ?? '';
|
||||
const currentForceVlan = client.forceVlan ?? false;
|
||||
const currentVlanId = client.vlanId != null ? String(client.vlanId) : '';
|
||||
const currentAllowList = client.destinationAllowList?.join(', ') ?? '';
|
||||
const currentBlockList = client.destinationBlockList?.join(', ') ?? '';
|
||||
const currentAllowAcls = (client.destinationAllowList?.length ?? 0) > 0
|
||||
|| (client.destinationBlockList?.length ?? 0) > 0;
|
||||
const editModal = await DeesModal.createAndShow({
|
||||
heading: `Edit: ${client.clientId}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
||||
<div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
|
||||
<div class="staticIpGroup" style="display: ${currentUseDhcp ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'staticIp'} .label=${'Static IP'} .value=${currentStaticIp}></dees-input-text>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${currentForceVlan}></dees-input-checkbox>
|
||||
<div class="vlanIdGroup" style="display: ${currentForceVlan ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'} .value=${currentVlanId}></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox>
|
||||
<div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'} .value=${currentAllowList}></dees-input-text>
|
||||
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'} .value=${currentBlockList}></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
@@ -573,16 +693,47 @@ export class OpsViewVpn extends DeesElement {
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
||||
|
||||
const allowAcls = data.allowAdditionalAcls ?? false;
|
||||
const destinationAllowList = allowAcls && data.destinationAllowList
|
||||
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
const destinationBlockList = allowAcls && data.destinationBlockList
|
||||
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
staticIp,
|
||||
forceVlan: forceVlan || undefined,
|
||||
vlanId,
|
||||
destinationAllowList,
|
||||
destinationBlockList,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Setup conditional form visibility for edit dialog
|
||||
const editForm = editModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
||||
if (editForm) {
|
||||
await editForm.updateComplete;
|
||||
setupFormVisibility(editForm);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user