@serve.zone/dcrouter 13.17.3 → 13.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +128 -128
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +5 -1
- package/dist_ts/classes.dcrouter.js +31 -15
- package/dist_ts/config/classes.route-config-manager.d.ts +7 -2
- package/dist_ts/config/classes.route-config-manager.js +64 -38
- package/dist_ts/config/classes.target-profile-manager.d.ts +12 -1
- package/dist_ts/config/classes.target-profile-manager.js +98 -11
- package/dist_ts/dns/manager.dns.js +3 -3
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +1 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +6 -9
- package/dist_ts/vpn/classes.vpn-manager.d.ts +11 -2
- package/dist_ts/vpn/classes.vpn-manager.js +120 -64
- package/dist_ts_interfaces/data/target-profile.d.ts +1 -1
- package/dist_ts_migrations/index.js +25 -18
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-targetprofiles.d.ts +4 -0
- package/dist_ts_web/elements/network/ops-view-targetprofiles.js +46 -9
- package/dist_ts_web/elements/network/ops-view-vpn.d.ts +6 -7
- package/dist_ts_web/elements/network/ops-view-vpn.js +39 -34
- package/package.json +6 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +37 -13
- package/ts/config/classes.route-config-manager.ts +80 -36
- package/ts/config/classes.target-profile-manager.ts +129 -6
- package/ts/dns/manager.dns.ts +2 -2
- package/ts/opsserver/handlers/certificate.handler.ts +4 -5
- package/ts/vpn/classes.vpn-manager.ts +146 -60
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-targetprofiles.ts +57 -8
- package/ts_web/elements/network/ops-view-vpn.ts +43 -32
|
@@ -55,6 +55,8 @@ export class VpnManager {
|
|
|
55
55
|
private vpnServer?: plugins.smartvpn.VpnServer;
|
|
56
56
|
private clients: Map<string, VpnClientDoc> = new Map();
|
|
57
57
|
private serverKeys?: VpnServerKeysDoc;
|
|
58
|
+
private resolvedForwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
|
59
|
+
private forwardingModeOverride?: 'socket' | 'bridge' | 'hybrid';
|
|
58
60
|
|
|
59
61
|
constructor(config: IVpnManagerConfig) {
|
|
60
62
|
this.config = config;
|
|
@@ -88,6 +90,7 @@ export class VpnManager {
|
|
|
88
90
|
if (client.useHostIp) {
|
|
89
91
|
anyClientUsesHostIp = true;
|
|
90
92
|
}
|
|
93
|
+
this.normalizeClientRoutingSettings(client);
|
|
91
94
|
const entry: plugins.smartvpn.IClientEntry = {
|
|
92
95
|
clientId: client.clientId,
|
|
93
96
|
publicKey: client.noisePublicKey,
|
|
@@ -97,13 +100,12 @@ export class VpnManager {
|
|
|
97
100
|
assignedIp: client.assignedIp,
|
|
98
101
|
expiresAt: client.expiresAt,
|
|
99
102
|
security: this.buildClientSecurity(client),
|
|
103
|
+
useHostIp: client.useHostIp,
|
|
104
|
+
useDhcp: client.useDhcp,
|
|
105
|
+
staticIp: client.staticIp,
|
|
106
|
+
forceVlan: client.forceVlan,
|
|
107
|
+
vlanId: client.vlanId,
|
|
100
108
|
};
|
|
101
|
-
// Pass per-client bridge fields if present (for hybrid/bridge mode)
|
|
102
|
-
if (client.useHostIp !== undefined) (entry as any).useHostIp = client.useHostIp;
|
|
103
|
-
if (client.useDhcp !== undefined) (entry as any).useDhcp = client.useDhcp;
|
|
104
|
-
if (client.staticIp !== undefined) (entry as any).staticIp = client.staticIp;
|
|
105
|
-
if (client.forceVlan !== undefined) (entry as any).forceVlan = client.forceVlan;
|
|
106
|
-
if (client.vlanId !== undefined) (entry as any).vlanId = client.vlanId;
|
|
107
109
|
clientEntries.push(entry);
|
|
108
110
|
}
|
|
109
111
|
|
|
@@ -112,13 +114,15 @@ export class VpnManager {
|
|
|
112
114
|
|
|
113
115
|
// Auto-detect hybrid mode: if any persisted client uses host IP and mode is
|
|
114
116
|
// 'socket' (or unset), upgrade to 'hybrid' so the daemon can handle both
|
|
115
|
-
let configuredMode = this.config.forwardingMode ?? 'socket';
|
|
117
|
+
let configuredMode = this.forwardingModeOverride ?? this.config.forwardingMode ?? 'socket';
|
|
116
118
|
if (anyClientUsesHostIp && configuredMode === 'socket') {
|
|
117
119
|
configuredMode = 'hybrid';
|
|
118
120
|
logger.log('info', 'VPN: Auto-upgrading forwarding mode to hybrid (client with useHostIp detected)');
|
|
119
121
|
}
|
|
120
122
|
const forwardingMode = configuredMode === 'hybrid' ? 'hybrid' : configuredMode;
|
|
121
123
|
const isBridge = forwardingMode === 'bridge';
|
|
124
|
+
this.resolvedForwardingMode = forwardingMode;
|
|
125
|
+
this.forwardingModeOverride = undefined;
|
|
122
126
|
|
|
123
127
|
// Create and start VpnServer
|
|
124
128
|
this.vpnServer = new plugins.smartvpn.VpnServer({
|
|
@@ -143,7 +147,7 @@ export class VpnManager {
|
|
|
143
147
|
wgListenPort,
|
|
144
148
|
clients: clientEntries,
|
|
145
149
|
socketForwardProxyProtocol: !isBridge,
|
|
146
|
-
destinationPolicy: this.
|
|
150
|
+
destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
|
|
147
151
|
serverEndpoint: this.config.serverEndpoint
|
|
148
152
|
? `${this.config.serverEndpoint}:${wgListenPort}`
|
|
149
153
|
: undefined,
|
|
@@ -189,6 +193,7 @@ export class VpnManager {
|
|
|
189
193
|
this.vpnServer.stop();
|
|
190
194
|
this.vpnServer = undefined;
|
|
191
195
|
}
|
|
196
|
+
this.resolvedForwardingMode = undefined;
|
|
192
197
|
logger.log('info', 'VPN server stopped');
|
|
193
198
|
}
|
|
194
199
|
|
|
@@ -213,14 +218,38 @@ export class VpnManager {
|
|
|
213
218
|
throw new Error('VPN server not running');
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
await this.ensureForwardingModeForHostIpClient(opts.useHostIp === true);
|
|
222
|
+
|
|
223
|
+
const doc = new VpnClientDoc();
|
|
224
|
+
doc.clientId = opts.clientId;
|
|
225
|
+
doc.enabled = true;
|
|
226
|
+
doc.targetProfileIds = opts.targetProfileIds;
|
|
227
|
+
doc.description = opts.description;
|
|
228
|
+
doc.destinationAllowList = opts.destinationAllowList;
|
|
229
|
+
doc.destinationBlockList = opts.destinationBlockList;
|
|
230
|
+
doc.useHostIp = opts.useHostIp;
|
|
231
|
+
doc.useDhcp = opts.useDhcp;
|
|
232
|
+
doc.staticIp = opts.staticIp;
|
|
233
|
+
doc.forceVlan = opts.forceVlan;
|
|
234
|
+
doc.vlanId = opts.vlanId;
|
|
235
|
+
doc.createdAt = Date.now();
|
|
236
|
+
doc.updatedAt = Date.now();
|
|
237
|
+
this.normalizeClientRoutingSettings(doc);
|
|
238
|
+
|
|
216
239
|
const bundle = await this.vpnServer.createClient({
|
|
217
|
-
clientId:
|
|
218
|
-
description:
|
|
240
|
+
clientId: doc.clientId,
|
|
241
|
+
description: doc.description,
|
|
242
|
+
security: this.buildClientSecurity(doc),
|
|
243
|
+
useHostIp: doc.useHostIp,
|
|
244
|
+
useDhcp: doc.useDhcp,
|
|
245
|
+
staticIp: doc.staticIp,
|
|
246
|
+
forceVlan: doc.forceVlan,
|
|
247
|
+
vlanId: doc.vlanId,
|
|
219
248
|
});
|
|
220
249
|
|
|
221
250
|
// Override AllowedIPs with per-client values based on target profiles
|
|
222
251
|
if (this.config.getClientAllowedIPs && bundle.wireguardConfig) {
|
|
223
|
-
const allowedIPs = await this.config.getClientAllowedIPs(
|
|
252
|
+
const allowedIPs = await this.config.getClientAllowedIPs(doc.targetProfileIds || []);
|
|
224
253
|
bundle.wireguardConfig = bundle.wireguardConfig.replace(
|
|
225
254
|
/AllowedIPs\s*=\s*.+/,
|
|
226
255
|
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
|
@@ -228,40 +257,16 @@ export class VpnManager {
|
|
|
228
257
|
}
|
|
229
258
|
|
|
230
259
|
// Persist client entry (including WG private key for export/QR)
|
|
231
|
-
const doc = new VpnClientDoc();
|
|
232
260
|
doc.clientId = bundle.entry.clientId;
|
|
233
261
|
doc.enabled = bundle.entry.enabled ?? true;
|
|
234
|
-
doc.targetProfileIds = opts.targetProfileIds;
|
|
235
262
|
doc.description = bundle.entry.description;
|
|
236
263
|
doc.assignedIp = bundle.entry.assignedIp;
|
|
237
264
|
doc.noisePublicKey = bundle.entry.publicKey;
|
|
238
265
|
doc.wgPublicKey = bundle.entry.wgPublicKey || '';
|
|
239
266
|
doc.wgPrivateKey = bundle.secrets?.wgPrivateKey
|
|
240
267
|
|| bundle.wireguardConfig?.match(/PrivateKey\s*=\s*(.+)/)?.[1]?.trim();
|
|
241
|
-
doc.createdAt = Date.now();
|
|
242
268
|
doc.updatedAt = Date.now();
|
|
243
269
|
doc.expiresAt = bundle.entry.expiresAt;
|
|
244
|
-
if (opts.destinationAllowList !== undefined) {
|
|
245
|
-
doc.destinationAllowList = opts.destinationAllowList;
|
|
246
|
-
}
|
|
247
|
-
if (opts.destinationBlockList !== undefined) {
|
|
248
|
-
doc.destinationBlockList = opts.destinationBlockList;
|
|
249
|
-
}
|
|
250
|
-
if (opts.useHostIp !== undefined) {
|
|
251
|
-
doc.useHostIp = opts.useHostIp;
|
|
252
|
-
}
|
|
253
|
-
if (opts.useDhcp !== undefined) {
|
|
254
|
-
doc.useDhcp = opts.useDhcp;
|
|
255
|
-
}
|
|
256
|
-
if (opts.staticIp !== undefined) {
|
|
257
|
-
doc.staticIp = opts.staticIp;
|
|
258
|
-
}
|
|
259
|
-
if (opts.forceVlan !== undefined) {
|
|
260
|
-
doc.forceVlan = opts.forceVlan;
|
|
261
|
-
}
|
|
262
|
-
if (opts.vlanId !== undefined) {
|
|
263
|
-
doc.vlanId = opts.vlanId;
|
|
264
|
-
}
|
|
265
270
|
this.clients.set(doc.clientId, doc);
|
|
266
271
|
try {
|
|
267
272
|
await this.persistClient(doc);
|
|
@@ -276,12 +281,6 @@ export class VpnManager {
|
|
|
276
281
|
throw err;
|
|
277
282
|
}
|
|
278
283
|
|
|
279
|
-
// Sync per-client security to the running daemon
|
|
280
|
-
const security = this.buildClientSecurity(doc);
|
|
281
|
-
if (security.destinationPolicy) {
|
|
282
|
-
await this.vpnServer!.updateClient(doc.clientId, { security });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
284
|
this.config.onClientChanged?.();
|
|
286
285
|
return bundle;
|
|
287
286
|
}
|
|
@@ -364,13 +363,13 @@ export class VpnManager {
|
|
|
364
363
|
if (update.staticIp !== undefined) client.staticIp = update.staticIp;
|
|
365
364
|
if (update.forceVlan !== undefined) client.forceVlan = update.forceVlan;
|
|
366
365
|
if (update.vlanId !== undefined) client.vlanId = update.vlanId;
|
|
366
|
+
this.normalizeClientRoutingSettings(client);
|
|
367
367
|
client.updatedAt = Date.now();
|
|
368
368
|
await this.persistClient(client);
|
|
369
369
|
|
|
370
|
-
// Sync per-client security to the running daemon
|
|
371
370
|
if (this.vpnServer) {
|
|
372
|
-
|
|
373
|
-
await this.vpnServer.updateClient(clientId,
|
|
371
|
+
await this.ensureForwardingModeForHostIpClient(client.useHostIp === true);
|
|
372
|
+
await this.vpnServer.updateClient(clientId, this.buildClientRuntimeUpdate(client));
|
|
374
373
|
}
|
|
375
374
|
|
|
376
375
|
this.config.onClientChanged?.();
|
|
@@ -478,26 +477,28 @@ export class VpnManager {
|
|
|
478
477
|
|
|
479
478
|
/**
|
|
480
479
|
* Build per-client security settings for the smartvpn daemon.
|
|
481
|
-
*
|
|
482
|
-
* TargetProfile direct IP:port targets bypass SmartProxy via allowList.
|
|
480
|
+
* TargetProfile direct IP:port targets extend the effective allow-list.
|
|
483
481
|
*/
|
|
484
482
|
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
|
485
483
|
const security: plugins.smartvpn.IClientSecurity = {};
|
|
484
|
+
const basePolicy = this.getBaseDestinationPolicy(client);
|
|
486
485
|
|
|
487
|
-
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
|
|
488
486
|
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
487
|
+
const mergedAllowList = this.mergeDestinationLists(
|
|
488
|
+
basePolicy.allowList,
|
|
489
|
+
client.destinationAllowList,
|
|
490
|
+
profileDirectTargets,
|
|
491
|
+
);
|
|
492
|
+
const mergedBlockList = this.mergeDestinationLists(
|
|
493
|
+
basePolicy.blockList,
|
|
494
|
+
client.destinationBlockList,
|
|
495
|
+
);
|
|
495
496
|
|
|
496
497
|
security.destinationPolicy = {
|
|
497
|
-
default:
|
|
498
|
-
target: '
|
|
498
|
+
default: basePolicy.default,
|
|
499
|
+
target: basePolicy.default === 'forceTarget' ? basePolicy.target : undefined,
|
|
499
500
|
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
|
500
|
-
blockList:
|
|
501
|
+
blockList: mergedBlockList.length ? mergedBlockList : undefined,
|
|
501
502
|
};
|
|
502
503
|
|
|
503
504
|
return security;
|
|
@@ -510,10 +511,7 @@ export class VpnManager {
|
|
|
510
511
|
public async refreshAllClientSecurity(): Promise<void> {
|
|
511
512
|
if (!this.vpnServer) return;
|
|
512
513
|
for (const client of this.clients.values()) {
|
|
513
|
-
|
|
514
|
-
if (security.destinationPolicy) {
|
|
515
|
-
await this.vpnServer.updateClient(client.clientId, { security });
|
|
516
|
-
}
|
|
514
|
+
await this.vpnServer.updateClient(client.clientId, this.buildClientRuntimeUpdate(client));
|
|
517
515
|
}
|
|
518
516
|
}
|
|
519
517
|
|
|
@@ -550,6 +548,7 @@ export class VpnManager {
|
|
|
550
548
|
private async loadPersistedClients(): Promise<void> {
|
|
551
549
|
const docs = await VpnClientDoc.findAll();
|
|
552
550
|
for (const doc of docs) {
|
|
551
|
+
this.normalizeClientRoutingSettings(doc);
|
|
553
552
|
this.clients.set(doc.clientId, doc);
|
|
554
553
|
}
|
|
555
554
|
if (this.clients.size > 0) {
|
|
@@ -557,6 +556,93 @@ export class VpnManager {
|
|
|
557
556
|
}
|
|
558
557
|
}
|
|
559
558
|
|
|
559
|
+
private getResolvedForwardingMode(): 'socket' | 'bridge' | 'hybrid' {
|
|
560
|
+
return this.resolvedForwardingMode
|
|
561
|
+
?? this.forwardingModeOverride
|
|
562
|
+
?? this.config.forwardingMode
|
|
563
|
+
?? 'socket';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private getDefaultDestinationPolicy(
|
|
567
|
+
forwardingMode: 'socket' | 'bridge' | 'hybrid',
|
|
568
|
+
useHostIp = false,
|
|
569
|
+
): plugins.smartvpn.IDestinationPolicy {
|
|
570
|
+
if (forwardingMode === 'bridge' || (forwardingMode === 'hybrid' && useHostIp)) {
|
|
571
|
+
return { default: 'allow' };
|
|
572
|
+
}
|
|
573
|
+
return { default: 'forceTarget', target: '127.0.0.1' };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private getServerDestinationPolicy(
|
|
577
|
+
forwardingMode: 'socket' | 'bridge' | 'hybrid',
|
|
578
|
+
fallbackPolicy = this.getDefaultDestinationPolicy(forwardingMode),
|
|
579
|
+
): plugins.smartvpn.IDestinationPolicy {
|
|
580
|
+
return this.config.destinationPolicy ?? fallbackPolicy;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private getBaseDestinationPolicy(client: Pick<VpnClientDoc, 'useHostIp'>): plugins.smartvpn.IDestinationPolicy {
|
|
584
|
+
if (this.config.destinationPolicy) {
|
|
585
|
+
return { ...this.config.destinationPolicy };
|
|
586
|
+
}
|
|
587
|
+
return this.getDefaultDestinationPolicy(this.getResolvedForwardingMode(), client.useHostIp === true);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private mergeDestinationLists(...lists: Array<string[] | undefined>): string[] {
|
|
591
|
+
const merged = new Set<string>();
|
|
592
|
+
for (const list of lists) {
|
|
593
|
+
for (const entry of list || []) {
|
|
594
|
+
merged.add(entry);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return [...merged];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private normalizeClientRoutingSettings(
|
|
601
|
+
client: Pick<VpnClientDoc, 'useHostIp' | 'useDhcp' | 'staticIp' | 'forceVlan' | 'vlanId'>,
|
|
602
|
+
): void {
|
|
603
|
+
client.useHostIp = client.useHostIp === true;
|
|
604
|
+
|
|
605
|
+
if (!client.useHostIp) {
|
|
606
|
+
client.useDhcp = false;
|
|
607
|
+
client.staticIp = undefined;
|
|
608
|
+
client.forceVlan = false;
|
|
609
|
+
client.vlanId = undefined;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
client.useDhcp = client.useDhcp === true;
|
|
614
|
+
if (client.useDhcp) {
|
|
615
|
+
client.staticIp = undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
client.forceVlan = client.forceVlan === true;
|
|
619
|
+
if (!client.forceVlan) {
|
|
620
|
+
client.vlanId = undefined;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private buildClientRuntimeUpdate(client: VpnClientDoc): Partial<plugins.smartvpn.IClientEntry> {
|
|
625
|
+
return {
|
|
626
|
+
description: client.description,
|
|
627
|
+
security: this.buildClientSecurity(client),
|
|
628
|
+
useHostIp: client.useHostIp,
|
|
629
|
+
useDhcp: client.useDhcp,
|
|
630
|
+
staticIp: client.staticIp,
|
|
631
|
+
forceVlan: client.forceVlan,
|
|
632
|
+
vlanId: client.vlanId,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private async ensureForwardingModeForHostIpClient(useHostIp: boolean): Promise<void> {
|
|
637
|
+
if (!useHostIp || !this.vpnServer) return;
|
|
638
|
+
if (this.getResolvedForwardingMode() !== 'socket') return;
|
|
639
|
+
|
|
640
|
+
logger.log('info', 'VPN: Restarting server in hybrid mode to support a host-IP client');
|
|
641
|
+
this.forwardingModeOverride = 'hybrid';
|
|
642
|
+
await this.stop();
|
|
643
|
+
await this.start();
|
|
644
|
+
}
|
|
645
|
+
|
|
560
646
|
private async persistClient(client: VpnClientDoc): Promise<void> {
|
|
561
647
|
await client.save();
|
|
562
648
|
}
|
|
@@ -95,7 +95,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
95
95
|
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)}`
|
|
96
96
|
: '-',
|
|
97
97
|
'Route Refs': profile.routeRefs?.length
|
|
98
|
-
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)}`
|
|
98
|
+
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${this.formatRouteRef(r)}</span>`)}`
|
|
99
99
|
: '-',
|
|
100
100
|
Created: new Date(profile.createdAt).toLocaleDateString(),
|
|
101
101
|
})}
|
|
@@ -149,12 +149,57 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
149
149
|
`;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
private
|
|
152
|
+
private getRouteChoices() {
|
|
153
153
|
const routeState = appstate.routeManagementStatePart.getState();
|
|
154
154
|
const routes = routeState?.mergedRoutes || [];
|
|
155
155
|
return routes
|
|
156
|
-
.filter((mr) => mr.route.name)
|
|
157
|
-
.map((mr) => ({
|
|
156
|
+
.filter((mr) => mr.route.name && mr.id)
|
|
157
|
+
.map((mr) => ({
|
|
158
|
+
routeId: mr.id!,
|
|
159
|
+
routeName: mr.route.name!,
|
|
160
|
+
label: `${mr.route.name} (${mr.id})`,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getRouteCandidates() {
|
|
165
|
+
return this.getRouteChoices().map((route) => ({ viewKey: route.label }));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private resolveRouteRefsToLabels(routeRefs?: string[]): string[] | undefined {
|
|
169
|
+
if (!routeRefs?.length) return undefined;
|
|
170
|
+
|
|
171
|
+
const routeChoices = this.getRouteChoices();
|
|
172
|
+
const routeById = new Map(routeChoices.map((route) => [route.routeId, route.label]));
|
|
173
|
+
const routeByName = new Map<string, string[]>();
|
|
174
|
+
|
|
175
|
+
for (const route of routeChoices) {
|
|
176
|
+
const labels = routeByName.get(route.routeName) || [];
|
|
177
|
+
labels.push(route.label);
|
|
178
|
+
routeByName.set(route.routeName, labels);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return routeRefs.map((routeRef) => {
|
|
182
|
+
const routeLabel = routeById.get(routeRef);
|
|
183
|
+
if (routeLabel) return routeLabel;
|
|
184
|
+
|
|
185
|
+
const labelsForName = routeByName.get(routeRef) || [];
|
|
186
|
+
if (labelsForName.length === 1) return labelsForName[0];
|
|
187
|
+
|
|
188
|
+
return routeRef;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private resolveRouteLabelsToRefs(routeRefs: string[]): string[] {
|
|
193
|
+
if (!routeRefs.length) return [];
|
|
194
|
+
|
|
195
|
+
const labelToId = new Map(
|
|
196
|
+
this.getRouteChoices().map((route) => [route.label, route.routeId]),
|
|
197
|
+
);
|
|
198
|
+
return routeRefs.map((routeRef) => labelToId.get(routeRef) || routeRef);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private formatRouteRef(routeRef: string): string {
|
|
202
|
+
return this.resolveRouteRefsToLabels([routeRef])?.[0] || routeRef;
|
|
158
203
|
}
|
|
159
204
|
|
|
160
205
|
private async ensureRoutesLoaded() {
|
|
@@ -203,7 +248,9 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
203
248
|
};
|
|
204
249
|
})
|
|
205
250
|
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
|
206
|
-
const routeRefs
|
|
251
|
+
const routeRefs = this.resolveRouteLabelsToRefs(
|
|
252
|
+
Array.isArray(data.routeRefs) ? data.routeRefs : [],
|
|
253
|
+
);
|
|
207
254
|
|
|
208
255
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.createTargetProfileAction, {
|
|
209
256
|
name: String(data.name),
|
|
@@ -222,7 +269,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
222
269
|
private async showEditProfileDialog(profile: interfaces.data.ITargetProfile) {
|
|
223
270
|
const currentDomains = profile.domains || [];
|
|
224
271
|
const currentTargets = profile.targets?.map(t => `${t.ip}:${t.port}`) || [];
|
|
225
|
-
const currentRouteRefs = profile.routeRefs || [];
|
|
272
|
+
const currentRouteRefs = this.resolveRouteRefsToLabels(profile.routeRefs) || [];
|
|
226
273
|
|
|
227
274
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
228
275
|
await this.ensureRoutesLoaded();
|
|
@@ -261,7 +308,9 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
261
308
|
};
|
|
262
309
|
})
|
|
263
310
|
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
|
264
|
-
const routeRefs
|
|
311
|
+
const routeRefs = this.resolveRouteLabelsToRefs(
|
|
312
|
+
Array.isArray(data.routeRefs) ? data.routeRefs : [],
|
|
313
|
+
);
|
|
265
314
|
|
|
266
315
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.updateTargetProfileAction, {
|
|
267
316
|
id: profile.id,
|
|
@@ -336,7 +385,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
336
385
|
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Route Refs</div>
|
|
337
386
|
<div style="font-size: 14px; margin-top: 4px;">
|
|
338
387
|
${profile.routeRefs?.length
|
|
339
|
-
? profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)
|
|
388
|
+
? profile.routeRefs.map(r => html`<span class="tagBadge">${this.formatRouteRef(r)}</span>`)
|
|
340
389
|
: '-'}
|
|
341
390
|
</div>
|
|
342
391
|
</div>
|
|
@@ -28,7 +28,7 @@ function setupFormVisibility(formEl: any) {
|
|
|
28
28
|
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
|
29
29
|
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
|
30
30
|
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
|
31
|
-
if (hostIpGroup) hostIpGroup.style.display = show;
|
|
31
|
+
if (hostIpGroup) hostIpGroup.style.display = show;
|
|
32
32
|
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
|
33
33
|
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
|
34
34
|
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
|
@@ -390,7 +390,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
390
390
|
if (!form) return;
|
|
391
391
|
const data = await form.collectFormData();
|
|
392
392
|
if (!data.clientId) return;
|
|
393
|
-
const targetProfileIds = this.
|
|
393
|
+
const targetProfileIds = this.resolveProfileLabelsToIds(
|
|
394
394
|
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
|
|
395
395
|
);
|
|
396
396
|
|
|
@@ -414,10 +414,10 @@ export class OpsViewVpn extends DeesElement {
|
|
|
414
414
|
description: data.description || undefined,
|
|
415
415
|
targetProfileIds,
|
|
416
416
|
|
|
417
|
-
useHostIp
|
|
418
|
-
useDhcp
|
|
417
|
+
useHostIp,
|
|
418
|
+
useDhcp,
|
|
419
419
|
staticIp,
|
|
420
|
-
forceVlan
|
|
420
|
+
forceVlan,
|
|
421
421
|
vlanId,
|
|
422
422
|
destinationAllowList,
|
|
423
423
|
destinationBlockList,
|
|
@@ -485,7 +485,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
485
485
|
<div class="infoItem"><span class="infoLabel">Transport</span><span class="infoValue">${conn.transport}</span></div>
|
|
486
486
|
` : ''}
|
|
487
487
|
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
|
488
|
-
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.
|
|
488
|
+
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToLabels(client.targetProfileIds)?.join(', ') || '-'}</span></div>
|
|
489
489
|
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.useHostIp ? 'Host IP' : 'SmartProxy'}</span></div>
|
|
490
490
|
${client.useHostIp ? html`
|
|
491
491
|
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
|
@@ -649,7 +649,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
649
649
|
const client = actionData.item as interfaces.data.IVpnClient;
|
|
650
650
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
651
651
|
const currentDescription = client.description ?? '';
|
|
652
|
-
const currentTargetProfileNames = this.
|
|
652
|
+
const currentTargetProfileNames = this.resolveProfileIdsToLabels(client.targetProfileIds) || [];
|
|
653
653
|
const profileCandidates = this.getTargetProfileCandidates();
|
|
654
654
|
const currentUseHostIp = client.useHostIp ?? false;
|
|
655
655
|
const currentUseDhcp = client.useDhcp ?? false;
|
|
@@ -695,7 +695,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
695
695
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
696
696
|
if (!form) return;
|
|
697
697
|
const data = await form.collectFormData();
|
|
698
|
-
const targetProfileIds = this.
|
|
698
|
+
const targetProfileIds = this.resolveProfileLabelsToIds(
|
|
699
699
|
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
|
|
700
700
|
);
|
|
701
701
|
|
|
@@ -719,10 +719,10 @@ export class OpsViewVpn extends DeesElement {
|
|
|
719
719
|
description: data.description || undefined,
|
|
720
720
|
targetProfileIds,
|
|
721
721
|
|
|
722
|
-
useHostIp
|
|
723
|
-
useDhcp
|
|
722
|
+
useHostIp,
|
|
723
|
+
useDhcp,
|
|
724
724
|
staticIp,
|
|
725
|
-
forceVlan
|
|
725
|
+
forceVlan,
|
|
726
726
|
vlanId,
|
|
727
727
|
destinationAllowList,
|
|
728
728
|
destinationBlockList,
|
|
@@ -811,41 +811,52 @@ export class OpsViewVpn extends DeesElement {
|
|
|
811
811
|
}
|
|
812
812
|
|
|
813
813
|
/**
|
|
814
|
-
* Build
|
|
815
|
-
* viewKey = profile name (displayed), payload = { id } (carried for resolution).
|
|
814
|
+
* Build stable profile labels for list inputs.
|
|
816
815
|
*/
|
|
817
|
-
private
|
|
816
|
+
private getTargetProfileChoices() {
|
|
818
817
|
const profileState = appstate.targetProfilesStatePart.getState();
|
|
819
818
|
const profiles = profileState?.profiles || [];
|
|
820
|
-
|
|
819
|
+
const nameCounts = new Map<string, number>();
|
|
820
|
+
|
|
821
|
+
for (const profile of profiles) {
|
|
822
|
+
nameCounts.set(profile.name, (nameCounts.get(profile.name) || 0) + 1);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return profiles.map((profile) => ({
|
|
826
|
+
id: profile.id,
|
|
827
|
+
label: (nameCounts.get(profile.name) || 0) > 1
|
|
828
|
+
? `${profile.name} (${profile.id})`
|
|
829
|
+
: profile.name,
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private getTargetProfileCandidates() {
|
|
834
|
+
return this.getTargetProfileChoices().map((profile) => ({ viewKey: profile.label }));
|
|
821
835
|
}
|
|
822
836
|
|
|
823
837
|
/**
|
|
824
|
-
* Convert profile IDs to
|
|
838
|
+
* Convert profile IDs to form labels (for populating edit form values).
|
|
825
839
|
*/
|
|
826
|
-
private
|
|
840
|
+
private resolveProfileIdsToLabels(ids?: string[]): string[] | undefined {
|
|
827
841
|
if (!ids?.length) return undefined;
|
|
828
|
-
const
|
|
829
|
-
const
|
|
842
|
+
const choices = this.getTargetProfileChoices();
|
|
843
|
+
const labelsById = new Map(choices.map((profile) => [profile.id, profile.label]));
|
|
830
844
|
return ids.map((id) => {
|
|
831
|
-
|
|
832
|
-
return profile?.name || id;
|
|
845
|
+
return labelsById.get(id) || id;
|
|
833
846
|
});
|
|
834
847
|
}
|
|
835
848
|
|
|
836
849
|
/**
|
|
837
|
-
* Convert profile
|
|
838
|
-
* Uses the dees-input-list candidates' payload when available.
|
|
850
|
+
* Convert profile form labels back to IDs.
|
|
839
851
|
*/
|
|
840
|
-
private
|
|
841
|
-
if (!
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
})
|
|
852
|
+
private resolveProfileLabelsToIds(labels: string[]): string[] {
|
|
853
|
+
if (!labels.length) return [];
|
|
854
|
+
|
|
855
|
+
const labelsToIds = new Map(
|
|
856
|
+
this.getTargetProfileChoices().map((profile) => [profile.label, profile.id]),
|
|
857
|
+
);
|
|
858
|
+
return labels
|
|
859
|
+
.map((label) => labelsToIds.get(label))
|
|
849
860
|
.filter((id): id is string => !!id);
|
|
850
861
|
}
|
|
851
862
|
}
|