@serve.zone/dcrouter 13.27.1 → 13.29.1
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/.smartconfig.json +32 -10
- package/dist_serve/bundle.js +930 -799
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -1
- package/dist_ts/classes.dcrouter.js +22 -7
- package/dist_ts/config/classes.gateway-client-manager.d.ts +22 -0
- package/dist_ts/config/classes.gateway-client-manager.js +101 -0
- package/dist_ts/config/classes.route-config-manager.js +8 -7
- package/dist_ts/config/index.d.ts +1 -0
- package/dist_ts/config/index.js +2 -1
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +18 -0
- package/dist_ts/db/documents/classes.gateway-client.doc.js +133 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/opsserver/classes.opsserver.js +4 -1
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +21 -6
- package/dist_ts/opsserver/handlers/admin.handler.js +188 -29
- package/dist_ts/opsserver/handlers/certificate.handler.js +5 -1
- package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
- package/dist_ts/opsserver/handlers/users.handler.js +2 -2
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +4 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +146 -16
- package/dist_ts/plugins.d.ts +2 -0
- package/dist_ts/plugins.js +4 -1
- package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -0
- package/dist_ts/vpn/classes.vpn-manager.js +41 -20
- package/dist_ts_apiclient/classes.workhoster.d.ts +1 -0
- package/dist_ts_apiclient/classes.workhoster.js +5 -1
- package/dist_ts_interfaces/data/workhoster.d.ts +28 -3
- package/dist_ts_interfaces/requests/admin.d.ts +38 -0
- package/dist_ts_interfaces/requests/users.d.ts +2 -5
- package/dist_ts_interfaces/requests/workhoster.d.ts +83 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +46 -0
- package/dist_ts_web/appstate.js +105 -1
- package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -1
- package/dist_ts_web/elements/access/ops-view-gatewayclients.d.ts +15 -0
- package/dist_ts_web/elements/access/ops-view-gatewayclients.js +293 -0
- package/dist_ts_web/elements/domains/ops-view-certificates.d.ts +6 -0
- package/dist_ts_web/elements/domains/ops-view-certificates.js +155 -13
- package/dist_ts_web/elements/network/ops-view-routes.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.d.ts +4 -0
- package/dist_ts_web/elements/ops-dashboard.js +102 -3
- package/dist_ts_web/router.js +3 -3
- package/package.json +15 -22
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +30 -6
- package/ts/config/classes.gateway-client-manager.ts +117 -0
- package/ts/config/classes.route-config-manager.ts +8 -6
- package/ts/config/index.ts +2 -1
- package/ts/db/documents/classes.gateway-client.doc.ts +54 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/opsserver/classes.opsserver.ts +3 -0
- package/ts/opsserver/handlers/admin.handler.ts +244 -32
- package/ts/opsserver/handlers/certificate.handler.ts +5 -0
- package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
- package/ts/opsserver/handlers/users.handler.ts +1 -1
- package/ts/opsserver/handlers/workhoster.handler.ts +191 -17
- package/ts/plugins.ts +7 -0
- package/ts/vpn/classes.vpn-manager.ts +56 -25
- package/ts_apiclient/classes.workhoster.ts +8 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +160 -0
- package/ts_web/elements/access/ops-view-apitokens.ts +1 -0
- package/ts_web/elements/access/ops-view-gatewayclients.ts +250 -0
- package/ts_web/elements/domains/ops-view-certificates.ts +166 -11
- package/ts_web/elements/network/ops-view-routes.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +102 -0
- package/ts_web/router.ts +2 -2
|
@@ -45,6 +45,16 @@ export class WorkHosterHandler {
|
|
|
45
45
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
private async requireAdmin(request: { identity?: interfaces.data.IIdentity }): Promise<string> {
|
|
49
|
+
if (request.identity?.jwt) {
|
|
50
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
|
51
|
+
identity: request.identity,
|
|
52
|
+
});
|
|
53
|
+
if (isAdmin) return request.identity.userId;
|
|
54
|
+
}
|
|
55
|
+
throw new plugins.typedrequest.TypedResponseError('admin identity required');
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
private registerHandlers(): void {
|
|
49
59
|
this.typedrouter.addTypedHandler(
|
|
50
60
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayCapabilities>(
|
|
@@ -56,6 +66,122 @@ export class WorkHosterHandler {
|
|
|
56
66
|
),
|
|
57
67
|
);
|
|
58
68
|
|
|
69
|
+
this.typedrouter.addTypedHandler(
|
|
70
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientContext>(
|
|
71
|
+
'getGatewayClientContext',
|
|
72
|
+
async (dataArg) => {
|
|
73
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
74
|
+
return {
|
|
75
|
+
context: this.getGatewayClientContext(auth),
|
|
76
|
+
capabilities: this.getGatewayCapabilities(),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
this.typedrouter.addTypedHandler(
|
|
83
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListGatewayClients>(
|
|
84
|
+
'listGatewayClients',
|
|
85
|
+
async (dataArg) => {
|
|
86
|
+
await this.requireAdmin(dataArg);
|
|
87
|
+
return { gatewayClients: await this.listManagedGatewayClients() };
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.typedrouter.addTypedHandler(
|
|
93
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClient>(
|
|
94
|
+
'createGatewayClient',
|
|
95
|
+
async (dataArg) => {
|
|
96
|
+
const userId = await this.requireAdmin(dataArg);
|
|
97
|
+
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
98
|
+
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
99
|
+
try {
|
|
100
|
+
const gatewayClient = await manager.createClient({
|
|
101
|
+
id: dataArg.id,
|
|
102
|
+
type: dataArg.type,
|
|
103
|
+
name: dataArg.name,
|
|
104
|
+
description: dataArg.description,
|
|
105
|
+
hostnamePatterns: dataArg.hostnamePatterns,
|
|
106
|
+
allowedRouteTargets: dataArg.allowedRouteTargets,
|
|
107
|
+
capabilities: dataArg.capabilities,
|
|
108
|
+
createdBy: userId,
|
|
109
|
+
});
|
|
110
|
+
return { success: true, gatewayClient };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { success: false, message: (error as Error).message };
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
this.typedrouter.addTypedHandler(
|
|
119
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateGatewayClient>(
|
|
120
|
+
'updateGatewayClient',
|
|
121
|
+
async (dataArg) => {
|
|
122
|
+
await this.requireAdmin(dataArg);
|
|
123
|
+
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
124
|
+
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
125
|
+
const gatewayClient = await manager.updateClient(dataArg.id, {
|
|
126
|
+
name: dataArg.name,
|
|
127
|
+
description: dataArg.description,
|
|
128
|
+
hostnamePatterns: dataArg.hostnamePatterns,
|
|
129
|
+
allowedRouteTargets: dataArg.allowedRouteTargets,
|
|
130
|
+
capabilities: dataArg.capabilities,
|
|
131
|
+
enabled: dataArg.enabled,
|
|
132
|
+
});
|
|
133
|
+
return gatewayClient
|
|
134
|
+
? { success: true, gatewayClient }
|
|
135
|
+
: { success: false, message: 'Gateway client not found' };
|
|
136
|
+
},
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
this.typedrouter.addTypedHandler(
|
|
141
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteGatewayClient>(
|
|
142
|
+
'deleteGatewayClient',
|
|
143
|
+
async (dataArg) => {
|
|
144
|
+
await this.requireAdmin(dataArg);
|
|
145
|
+
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
146
|
+
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
147
|
+
const success = await manager.deleteClient(dataArg.id);
|
|
148
|
+
return { success, message: success ? undefined : 'Gateway client not found' };
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
this.typedrouter.addTypedHandler(
|
|
154
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClientToken>(
|
|
155
|
+
'createGatewayClientToken',
|
|
156
|
+
async (dataArg) => {
|
|
157
|
+
const userId = await this.requireAdmin(dataArg);
|
|
158
|
+
const gatewayClient = await this.opsServerRef.dcRouterRef.gatewayClientManager?.getClient(dataArg.gatewayClientId);
|
|
159
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
160
|
+
if (!gatewayClient || !gatewayClient.enabled) {
|
|
161
|
+
return { success: false, message: 'Gateway client not found or disabled' };
|
|
162
|
+
}
|
|
163
|
+
if (!tokenManager) {
|
|
164
|
+
return { success: false, message: 'Token management not initialized' };
|
|
165
|
+
}
|
|
166
|
+
const result = await tokenManager.createToken(
|
|
167
|
+
dataArg.name?.trim() || `${gatewayClient.name} Token`,
|
|
168
|
+
['gateway-clients:read', 'gateway-clients:write'],
|
|
169
|
+
dataArg.expiresInDays ?? null,
|
|
170
|
+
userId,
|
|
171
|
+
{
|
|
172
|
+
role: 'gatewayClient',
|
|
173
|
+
scopes: ['gateway-clients:read', 'gateway-clients:write'],
|
|
174
|
+
gatewayClient: { type: gatewayClient.type, id: gatewayClient.id },
|
|
175
|
+
hostnamePatterns: gatewayClient.hostnamePatterns,
|
|
176
|
+
allowedRouteTargets: gatewayClient.allowedRouteTargets,
|
|
177
|
+
capabilities: gatewayClient.capabilities,
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
|
181
|
+
},
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
59
185
|
this.typedrouter.addTypedHandler(
|
|
60
186
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDomains>(
|
|
61
187
|
'getGatewayClientDomains',
|
|
@@ -183,6 +309,30 @@ export class WorkHosterHandler {
|
|
|
183
309
|
};
|
|
184
310
|
}
|
|
185
311
|
|
|
312
|
+
private getGatewayClientContext(auth: TAuthContext): interfaces.data.IGatewayClientContext {
|
|
313
|
+
const policy = auth.token?.policy;
|
|
314
|
+
const role = auth.isAdmin ? 'admin' : policy?.role || 'operator';
|
|
315
|
+
return {
|
|
316
|
+
role,
|
|
317
|
+
scopes: auth.token?.scopes || ['*'],
|
|
318
|
+
gatewayClient: policy?.gatewayClient,
|
|
319
|
+
hostnamePatterns: policy?.hostnamePatterns || [],
|
|
320
|
+
allowedRouteTargets: policy?.allowedRouteTargets || [],
|
|
321
|
+
capabilities: policy?.capabilities || {},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async listManagedGatewayClients(): Promise<interfaces.data.IGatewayClient[]> {
|
|
326
|
+
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
327
|
+
if (!manager) return [];
|
|
328
|
+
const clients = await manager.listClients();
|
|
329
|
+
const tokens = this.opsServerRef.dcRouterRef.apiTokenManager?.listTokens() || [];
|
|
330
|
+
return clients.map((client) => ({
|
|
331
|
+
...client,
|
|
332
|
+
tokenCount: tokens.filter((token) => token.policy?.gatewayClient?.id === client.id).length,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
186
336
|
private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string {
|
|
187
337
|
return [
|
|
188
338
|
ownership.workHosterType,
|
|
@@ -212,15 +362,38 @@ export class WorkHosterHandler {
|
|
|
212
362
|
return policyClient.id;
|
|
213
363
|
}
|
|
214
364
|
|
|
215
|
-
private
|
|
365
|
+
private resolveGatewayClientOwnership(
|
|
366
|
+
auth: TAuthContext,
|
|
367
|
+
ownership: interfaces.data.IGatewayClientOwnership,
|
|
368
|
+
): Required<interfaces.data.IGatewayClientOwnership> {
|
|
216
369
|
const policy = auth.token?.policy;
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
370
|
+
if (policy?.role === 'gatewayClient') {
|
|
371
|
+
if (!policy.gatewayClient) {
|
|
372
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
|
|
373
|
+
}
|
|
374
|
+
if (ownership.gatewayClientType && ownership.gatewayClientType !== policy.gatewayClient.type) {
|
|
375
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
376
|
+
}
|
|
377
|
+
if (ownership.gatewayClientId && ownership.gatewayClientId !== policy.gatewayClient.id) {
|
|
378
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
gatewayClientType: policy.gatewayClient.type,
|
|
382
|
+
gatewayClientId: policy.gatewayClient.id,
|
|
383
|
+
appId: ownership.appId,
|
|
384
|
+
hostname: ownership.hostname,
|
|
385
|
+
};
|
|
220
386
|
}
|
|
221
|
-
|
|
222
|
-
|
|
387
|
+
|
|
388
|
+
if (!ownership.gatewayClientType || !ownership.gatewayClientId) {
|
|
389
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client ownership is missing type or id');
|
|
223
390
|
}
|
|
391
|
+
return ownership as Required<interfaces.data.IGatewayClientOwnership>;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private assertGatewayClientOwnership(auth: TAuthContext, ownership: Required<interfaces.data.IGatewayClientOwnership>): void {
|
|
395
|
+
const policy = auth.token?.policy;
|
|
396
|
+
if (!policy || policy.role !== 'gatewayClient') return;
|
|
224
397
|
if (!this.matchesHostnamePatterns(ownership.hostname, policy.hostnamePatterns || [])) {
|
|
225
398
|
throw new plugins.typedrequest.TypedResponseError('hostname is outside token policy');
|
|
226
399
|
}
|
|
@@ -403,7 +576,8 @@ export class WorkHosterHandler {
|
|
|
403
576
|
enabled?: boolean,
|
|
404
577
|
deleteRoute?: boolean,
|
|
405
578
|
): Promise<interfaces.data.IGatewayClientRouteSyncResult> {
|
|
406
|
-
this.
|
|
579
|
+
const resolvedOwnership = this.resolveGatewayClientOwnership(auth, ownership);
|
|
580
|
+
this.assertGatewayClientOwnership(auth, resolvedOwnership);
|
|
407
581
|
this.assertRouteTargetsAllowed(auth, route);
|
|
408
582
|
|
|
409
583
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
@@ -411,7 +585,7 @@ export class WorkHosterHandler {
|
|
|
411
585
|
return { success: false, message: 'Route management not initialized' };
|
|
412
586
|
}
|
|
413
587
|
|
|
414
|
-
const externalKey = this.buildGatewayClientExternalKey(
|
|
588
|
+
const externalKey = this.buildGatewayClientExternalKey(resolvedOwnership);
|
|
415
589
|
const existingRoute = manager.findApiRouteByExternalKey(externalKey);
|
|
416
590
|
|
|
417
591
|
if (deleteRoute) {
|
|
@@ -430,15 +604,15 @@ export class WorkHosterHandler {
|
|
|
430
604
|
|
|
431
605
|
const metadata: interfaces.data.IRouteMetadata = {
|
|
432
606
|
ownerType: 'gatewayClient',
|
|
433
|
-
gatewayClientType:
|
|
434
|
-
gatewayClientId:
|
|
435
|
-
gatewayClientAppId:
|
|
436
|
-
workHosterType:
|
|
437
|
-
workHosterId:
|
|
438
|
-
workAppId:
|
|
607
|
+
gatewayClientType: resolvedOwnership.gatewayClientType,
|
|
608
|
+
gatewayClientId: resolvedOwnership.gatewayClientId,
|
|
609
|
+
gatewayClientAppId: resolvedOwnership.appId,
|
|
610
|
+
workHosterType: resolvedOwnership.gatewayClientType,
|
|
611
|
+
workHosterId: resolvedOwnership.gatewayClientId,
|
|
612
|
+
workAppId: resolvedOwnership.appId,
|
|
439
613
|
externalKey,
|
|
440
614
|
};
|
|
441
|
-
const normalizedRoute = this.normalizeGatewayClientRoute(route,
|
|
615
|
+
const normalizedRoute = this.normalizeGatewayClientRoute(route, resolvedOwnership, externalKey);
|
|
442
616
|
|
|
443
617
|
if (existingRoute) {
|
|
444
618
|
const result = await manager.updateRoute(existingRoute.id, {
|
|
@@ -455,7 +629,7 @@ export class WorkHosterHandler {
|
|
|
455
629
|
return { success: true, action: 'created', routeId };
|
|
456
630
|
}
|
|
457
631
|
|
|
458
|
-
private buildGatewayClientExternalKey(ownership: interfaces.data.IGatewayClientOwnership): string {
|
|
632
|
+
private buildGatewayClientExternalKey(ownership: Required<interfaces.data.IGatewayClientOwnership>): string {
|
|
459
633
|
return [
|
|
460
634
|
ownership.gatewayClientType,
|
|
461
635
|
ownership.gatewayClientId,
|
|
@@ -478,7 +652,7 @@ export class WorkHosterHandler {
|
|
|
478
652
|
|
|
479
653
|
private normalizeGatewayClientRoute(
|
|
480
654
|
route: interfaces.data.IDcRouterRouteConfig,
|
|
481
|
-
ownership: interfaces.data.IGatewayClientOwnership
|
|
655
|
+
ownership: Required<interfaces.data.IGatewayClientOwnership>,
|
|
482
656
|
externalKey: string,
|
|
483
657
|
): interfaces.data.IDcRouterRouteConfig {
|
|
484
658
|
const normalizedRoute = { ...route };
|
package/ts/plugins.ts
CHANGED
|
@@ -41,6 +41,13 @@ export {
|
|
|
41
41
|
typedsocket,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// @idp.global scope
|
|
45
|
+
import * as idpSdkServer from '@idp.global/sdk/server';
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
idpSdkServer,
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
// @push.rocks scope
|
|
45
52
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
46
53
|
import * as qenv from '@push.rocks/qenv';
|
|
@@ -111,6 +111,7 @@ export class VpnManager {
|
|
|
111
111
|
|
|
112
112
|
const subnet = this.getSubnet();
|
|
113
113
|
const wgListenPort = this.config.wgListenPort ?? 51820;
|
|
114
|
+
const serverEndpoint = this.getWireGuardServerEndpoint();
|
|
114
115
|
|
|
115
116
|
const desiredForwardingMode = this.getDesiredForwardingMode(anyClientUsesHostIp);
|
|
116
117
|
if (anyClientUsesHostIp && desiredForwardingMode === 'hybrid') {
|
|
@@ -133,21 +134,19 @@ export class VpnManager {
|
|
|
133
134
|
: { default: 'forceTarget' as const, target: '127.0.0.1' };
|
|
134
135
|
|
|
135
136
|
const serverConfig: plugins.smartvpn.IVpnServerConfig = {
|
|
136
|
-
listenAddr: '
|
|
137
|
+
listenAddr: '127.0.0.1:0', // Required by smartvpn, unused in wireguard-only mode
|
|
137
138
|
privateKey: this.serverKeys.noisePrivateKey,
|
|
138
139
|
publicKey: this.serverKeys.noisePublicKey,
|
|
139
140
|
subnet,
|
|
140
141
|
dns: this.config.dns,
|
|
141
142
|
forwardingMode: forwardingMode as any,
|
|
142
|
-
transportMode: '
|
|
143
|
+
transportMode: 'wireguard',
|
|
143
144
|
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
|
144
145
|
wgListenPort,
|
|
145
146
|
clients: clientEntries,
|
|
146
147
|
socketForwardProxyProtocol: !isBridge,
|
|
147
148
|
destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
|
|
148
|
-
serverEndpoint
|
|
149
|
-
? `${this.config.serverEndpoint}:${wgListenPort}`
|
|
150
|
-
: undefined,
|
|
149
|
+
serverEndpoint,
|
|
151
150
|
clientAllowedIPs: [subnet],
|
|
152
151
|
// Bridge-specific config
|
|
153
152
|
...(isBridge ? {
|
|
@@ -187,7 +186,7 @@ export class VpnManager {
|
|
|
187
186
|
} catch {
|
|
188
187
|
// Ignore stop errors
|
|
189
188
|
}
|
|
190
|
-
this.vpnServer.stop();
|
|
189
|
+
await this.vpnServer.stop();
|
|
191
190
|
this.vpnServer = undefined;
|
|
192
191
|
}
|
|
193
192
|
this.resolvedForwardingMode = undefined;
|
|
@@ -244,14 +243,10 @@ export class VpnManager {
|
|
|
244
243
|
vlanId: doc.vlanId,
|
|
245
244
|
});
|
|
246
245
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
/AllowedIPs\s*=\s*.+/,
|
|
252
|
-
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
246
|
+
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
|
247
|
+
bundle.wireguardConfig,
|
|
248
|
+
doc.targetProfileIds || [],
|
|
249
|
+
);
|
|
255
250
|
|
|
256
251
|
// Persist client entry (including WG private key for export/QR)
|
|
257
252
|
doc.clientId = bundle.entry.clientId;
|
|
@@ -381,9 +376,13 @@ export class VpnManager {
|
|
|
381
376
|
public async rotateClientKey(clientId: string): Promise<plugins.smartvpn.IClientConfigBundle> {
|
|
382
377
|
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
383
378
|
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
|
379
|
+
const client = this.clients.get(clientId);
|
|
380
|
+
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
|
381
|
+
bundle.wireguardConfig,
|
|
382
|
+
client?.targetProfileIds || [],
|
|
383
|
+
);
|
|
384
384
|
|
|
385
385
|
// Update persisted entry with new keys (including private key for export/QR)
|
|
386
|
-
const client = this.clients.get(clientId);
|
|
387
386
|
if (client) {
|
|
388
387
|
client.noisePublicKey = bundle.entry.publicKey;
|
|
389
388
|
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
|
@@ -414,15 +413,7 @@ export class VpnManager {
|
|
|
414
413
|
);
|
|
415
414
|
}
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
if (this.config.getClientAllowedIPs) {
|
|
419
|
-
const profileIds = persisted?.targetProfileIds || [];
|
|
420
|
-
const allowedIPs = await this.config.getClientAllowedIPs(profileIds);
|
|
421
|
-
config = config.replace(
|
|
422
|
-
/AllowedIPs\s*=\s*.+/,
|
|
423
|
-
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
|
424
|
-
);
|
|
425
|
-
}
|
|
416
|
+
config = await this.rewriteWireGuardAllowedIPs(config, persisted?.targetProfileIds || []);
|
|
426
417
|
}
|
|
427
418
|
|
|
428
419
|
return config;
|
|
@@ -515,6 +506,46 @@ export class VpnManager {
|
|
|
515
506
|
}
|
|
516
507
|
}
|
|
517
508
|
|
|
509
|
+
private getWireGuardServerEndpoint(): string {
|
|
510
|
+
const endpoint = this.config.serverEndpoint?.trim();
|
|
511
|
+
if (!endpoint) {
|
|
512
|
+
throw new Error('vpnConfig.serverEndpoint is required when VPN is enabled');
|
|
513
|
+
}
|
|
514
|
+
if (endpoint.includes('://') || endpoint.includes('/')) {
|
|
515
|
+
throw new Error('vpnConfig.serverEndpoint must be a host or host:port, not a URL');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const host = endpoint.includes(':') ? endpoint.split(':')[0] : endpoint;
|
|
519
|
+
const lowerHost = host.toLowerCase();
|
|
520
|
+
if (
|
|
521
|
+
lowerHost === 'localhost'
|
|
522
|
+
|| lowerHost === '0.0.0.0'
|
|
523
|
+
|| lowerHost.startsWith('127.')
|
|
524
|
+
) {
|
|
525
|
+
throw new Error('vpnConfig.serverEndpoint must be reachable by VPN clients');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return endpoint.includes(':')
|
|
529
|
+
? endpoint
|
|
530
|
+
: `${endpoint}:${this.config.wgListenPort ?? 51820}`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private async rewriteWireGuardAllowedIPs(
|
|
534
|
+
wireguardConfig: string,
|
|
535
|
+
targetProfileIds: string[],
|
|
536
|
+
): Promise<string> {
|
|
537
|
+
if (!this.config.getClientAllowedIPs) return wireguardConfig;
|
|
538
|
+
|
|
539
|
+
const allowedIPs = await this.config.getClientAllowedIPs(targetProfileIds);
|
|
540
|
+
const effectiveAllowedIPs = allowedIPs.length ? allowedIPs : [this.getSubnet()];
|
|
541
|
+
const allowedLine = `AllowedIPs = ${effectiveAllowedIPs.join(', ')}`;
|
|
542
|
+
|
|
543
|
+
if (/^AllowedIPs\s*=.*$/m.test(wireguardConfig)) {
|
|
544
|
+
return wireguardConfig.replace(/^AllowedIPs\s*=.*$/m, allowedLine);
|
|
545
|
+
}
|
|
546
|
+
return `${wireguardConfig.trimEnd()}\n${allowedLine}\n`;
|
|
547
|
+
}
|
|
548
|
+
|
|
518
549
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
519
550
|
|
|
520
551
|
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
|
@@ -532,7 +563,7 @@ export class VpnManager {
|
|
|
532
563
|
|
|
533
564
|
const noiseKeys = await tempServer.generateKeypair();
|
|
534
565
|
const wgKeys = await tempServer.generateWgKeypair();
|
|
535
|
-
tempServer.stop();
|
|
566
|
+
await tempServer.stop();
|
|
536
567
|
|
|
537
568
|
const doc = stored || new VpnServerKeysDoc();
|
|
538
569
|
doc.noisePrivateKey = noiseKeys.privateKey;
|
|
@@ -12,6 +12,14 @@ export class WorkHosterManager {
|
|
|
12
12
|
return response.capabilities;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
public async getGatewayClientContext(): Promise<interfaces.data.IGatewayClientContext> {
|
|
16
|
+
const response = await this.clientRef.request<interfaces.requests.IReq_GetGatewayClientContext>(
|
|
17
|
+
'getGatewayClientContext',
|
|
18
|
+
this.clientRef.buildRequestPayload() as any,
|
|
19
|
+
);
|
|
20
|
+
return response.context;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
public async getDomains(): Promise<interfaces.data.IWorkHosterDomain[]> {
|
|
16
24
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetWorkHosterDomains>(
|
|
17
25
|
'getWorkHosterDomains',
|
package/ts_web/appstate.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ILoginState {
|
|
|
10
10
|
isLoggedIn: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type IAdminBootstrapStatus = interfaces.requests.IReq_GetAdminBootstrapStatus['response'];
|
|
14
|
+
|
|
13
15
|
export interface IStatsState {
|
|
14
16
|
serverStats: interfaces.data.IServerStats | null;
|
|
15
17
|
emailStats: interfaces.data.IEmailStats | null;
|
|
@@ -285,6 +287,7 @@ export interface IRouteManagementState {
|
|
|
285
287
|
mergedRoutes: interfaces.data.IMergedRoute[];
|
|
286
288
|
warnings: interfaces.data.IRouteWarning[];
|
|
287
289
|
apiTokens: interfaces.data.IApiTokenInfo[];
|
|
290
|
+
gatewayClients: interfaces.data.IGatewayClient[];
|
|
288
291
|
isLoading: boolean;
|
|
289
292
|
error: string | null;
|
|
290
293
|
lastUpdated: number;
|
|
@@ -296,6 +299,7 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
|
|
|
296
299
|
mergedRoutes: [],
|
|
297
300
|
warnings: [],
|
|
298
301
|
apiTokens: [],
|
|
302
|
+
gatewayClients: [],
|
|
299
303
|
isLoading: false,
|
|
300
304
|
error: null,
|
|
301
305
|
lastUpdated: 0,
|
|
@@ -310,7 +314,11 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
|
|
|
310
314
|
export interface IUser {
|
|
311
315
|
id: string;
|
|
312
316
|
username: string;
|
|
317
|
+
email?: string;
|
|
318
|
+
name?: string;
|
|
313
319
|
role: string;
|
|
320
|
+
status?: 'active' | 'disabled';
|
|
321
|
+
authSources?: Array<'local' | 'idp.global'>;
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
export interface IUsersState {
|
|
@@ -349,6 +357,7 @@ const getActionContext = (): IActionContext => {
|
|
|
349
357
|
export const loginAction = loginStatePart.createAction<{
|
|
350
358
|
username: string;
|
|
351
359
|
password: string;
|
|
360
|
+
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
|
352
361
|
}>(async (statePartArg, dataArg): Promise<ILoginState> => {
|
|
353
362
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
354
363
|
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
|
@@ -358,6 +367,7 @@ export const loginAction = loginStatePart.createAction<{
|
|
|
358
367
|
const response = await typedRequest.fire({
|
|
359
368
|
username: dataArg.username,
|
|
360
369
|
password: dataArg.password,
|
|
370
|
+
authSource: dataArg.authSource,
|
|
361
371
|
});
|
|
362
372
|
|
|
363
373
|
if (response.identity) {
|
|
@@ -373,6 +383,47 @@ export const loginAction = loginStatePart.createAction<{
|
|
|
373
383
|
}
|
|
374
384
|
});
|
|
375
385
|
|
|
386
|
+
export async function getAdminBootstrapStatus(): Promise<IAdminBootstrapStatus> {
|
|
387
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
388
|
+
interfaces.requests.IReq_GetAdminBootstrapStatus
|
|
389
|
+
>('/typedrequest', 'getAdminBootstrapStatus');
|
|
390
|
+
|
|
391
|
+
return request.fire({});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function createInitialAdminUser(optionsArg: {
|
|
395
|
+
email: string;
|
|
396
|
+
name?: string;
|
|
397
|
+
password: string;
|
|
398
|
+
enableIdpGlobalAuth?: boolean;
|
|
399
|
+
}) {
|
|
400
|
+
const context = getActionContext();
|
|
401
|
+
if (!context.identity) {
|
|
402
|
+
throw new Error('No identity available for admin bootstrap');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
406
|
+
interfaces.requests.IReq_CreateInitialAdminUser
|
|
407
|
+
>('/typedrequest', 'createInitialAdminUser');
|
|
408
|
+
|
|
409
|
+
const response = await request.fire({
|
|
410
|
+
identity: context.identity,
|
|
411
|
+
email: optionsArg.email,
|
|
412
|
+
name: optionsArg.name,
|
|
413
|
+
password: optionsArg.password,
|
|
414
|
+
enableIdpGlobalAuth: optionsArg.enableIdpGlobalAuth,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (response.identity) {
|
|
418
|
+
loginStatePart.setState({
|
|
419
|
+
identity: response.identity,
|
|
420
|
+
isLoggedIn: true,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return response;
|
|
425
|
+
}
|
|
426
|
+
|
|
376
427
|
// Logout Action — always clears state, even if identity is expired/missing
|
|
377
428
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
|
378
429
|
const context = getActionContext();
|
|
@@ -2477,6 +2528,115 @@ export const fetchApiTokensAction = routeManagementStatePart.createAction(async
|
|
|
2477
2528
|
}
|
|
2478
2529
|
});
|
|
2479
2530
|
|
|
2531
|
+
export const fetchGatewayClientsAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
|
|
2532
|
+
const context = getActionContext();
|
|
2533
|
+
const currentState = statePartArg.getState()!;
|
|
2534
|
+
if (!context.identity) return currentState;
|
|
2535
|
+
|
|
2536
|
+
try {
|
|
2537
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2538
|
+
interfaces.requests.IReq_ListGatewayClients
|
|
2539
|
+
>('/typedrequest', 'listGatewayClients');
|
|
2540
|
+
const response = await request.fire({ identity: context.identity });
|
|
2541
|
+
return {
|
|
2542
|
+
...currentState,
|
|
2543
|
+
gatewayClients: response.gatewayClients,
|
|
2544
|
+
error: null,
|
|
2545
|
+
lastUpdated: Date.now(),
|
|
2546
|
+
};
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
return {
|
|
2549
|
+
...currentState,
|
|
2550
|
+
error: error instanceof Error ? error.message : 'Failed to fetch gateway clients',
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
export async function createGatewayClient(data: {
|
|
2556
|
+
id?: string;
|
|
2557
|
+
type: interfaces.data.IGatewayClient['type'];
|
|
2558
|
+
name: string;
|
|
2559
|
+
description?: string;
|
|
2560
|
+
hostnamePatterns?: string[];
|
|
2561
|
+
allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
|
|
2562
|
+
}) {
|
|
2563
|
+
const context = getActionContext();
|
|
2564
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2565
|
+
interfaces.requests.IReq_CreateGatewayClient
|
|
2566
|
+
>('/typedrequest', 'createGatewayClient');
|
|
2567
|
+
return request.fire({
|
|
2568
|
+
identity: context.identity!,
|
|
2569
|
+
capabilities: {
|
|
2570
|
+
readDomains: true,
|
|
2571
|
+
readDnsRecords: true,
|
|
2572
|
+
syncRoutes: true,
|
|
2573
|
+
syncDnsRecords: false,
|
|
2574
|
+
requestCertificates: false,
|
|
2575
|
+
},
|
|
2576
|
+
...data,
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
export const updateGatewayClientAction = routeManagementStatePart.createAction<{
|
|
2581
|
+
id: string;
|
|
2582
|
+
name?: string;
|
|
2583
|
+
description?: string;
|
|
2584
|
+
hostnamePatterns?: string[];
|
|
2585
|
+
allowedRouteTargets?: interfaces.data.IGatewayClient['allowedRouteTargets'];
|
|
2586
|
+
enabled?: boolean;
|
|
2587
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
|
2588
|
+
const context = getActionContext();
|
|
2589
|
+
const currentState = statePartArg.getState()!;
|
|
2590
|
+
try {
|
|
2591
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2592
|
+
interfaces.requests.IReq_UpdateGatewayClient
|
|
2593
|
+
>('/typedrequest', 'updateGatewayClient');
|
|
2594
|
+
await request.fire({ identity: context.identity!, ...dataArg });
|
|
2595
|
+
return await actionContext!.dispatch(fetchGatewayClientsAction, null);
|
|
2596
|
+
} catch (error) {
|
|
2597
|
+
return {
|
|
2598
|
+
...currentState,
|
|
2599
|
+
error: error instanceof Error ? error.message : 'Failed to update gateway client',
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2604
|
+
export const deleteGatewayClientAction = routeManagementStatePart.createAction<string>(
|
|
2605
|
+
async (statePartArg, gatewayClientId, actionContext): Promise<IRouteManagementState> => {
|
|
2606
|
+
const context = getActionContext();
|
|
2607
|
+
const currentState = statePartArg.getState()!;
|
|
2608
|
+
try {
|
|
2609
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2610
|
+
interfaces.requests.IReq_DeleteGatewayClient
|
|
2611
|
+
>('/typedrequest', 'deleteGatewayClient');
|
|
2612
|
+
await request.fire({ identity: context.identity!, id: gatewayClientId });
|
|
2613
|
+
return await actionContext!.dispatch(fetchGatewayClientsAction, null);
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
return {
|
|
2616
|
+
...currentState,
|
|
2617
|
+
error: error instanceof Error ? error.message : 'Failed to delete gateway client',
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
},
|
|
2621
|
+
);
|
|
2622
|
+
|
|
2623
|
+
export async function createGatewayClientToken(
|
|
2624
|
+
gatewayClientId: string,
|
|
2625
|
+
name?: string,
|
|
2626
|
+
expiresInDays?: number | null,
|
|
2627
|
+
) {
|
|
2628
|
+
const context = getActionContext();
|
|
2629
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2630
|
+
interfaces.requests.IReq_CreateGatewayClientToken
|
|
2631
|
+
>('/typedrequest', 'createGatewayClientToken');
|
|
2632
|
+
return request.fire({
|
|
2633
|
+
identity: context.identity!,
|
|
2634
|
+
gatewayClientId,
|
|
2635
|
+
name,
|
|
2636
|
+
expiresInDays,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2480
2640
|
// Users (read-only list)
|
|
2481
2641
|
export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
|
|
2482
2642
|
const context = getActionContext();
|