@serve.zone/dcrouter 13.28.0 → 13.30.0
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 +609 -592
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +7 -0
- package/dist_ts/classes.dcrouter.js +12 -2
- package/dist_ts/config/classes.route-config-manager.js +8 -7
- 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/target-profile.handler.js +3 -1
- package/dist_ts/opsserver/handlers/users.handler.js +2 -2
- 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_interfaces/requests/admin.d.ts +38 -0
- package/dist_ts_interfaces/requests/users.d.ts +2 -5
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +17 -0
- package/dist_ts_web/appstate.js +27 -1
- package/dist_ts_web/elements/ops-dashboard.d.ts +4 -0
- package/dist_ts_web/elements/ops-dashboard.js +100 -3
- package/package.json +27 -34
- package/readme.md +15 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +20 -1
- package/ts/config/classes.route-config-manager.ts +8 -6
- package/ts/opsserver/classes.opsserver.ts +3 -0
- package/ts/opsserver/handlers/admin.handler.ts +244 -32
- package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
- package/ts/opsserver/handlers/users.handler.ts +1 -1
- package/ts/plugins.ts +7 -0
- package/ts/readme.md +1 -1
- package/ts/vpn/classes.vpn-manager.ts +56 -25
- package/ts_apiclient/readme.md +4 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +49 -0
- package/ts_web/elements/ops-dashboard.ts +100 -0
- package/ts_web/readme.md +5 -3
|
@@ -8,19 +8,33 @@ export interface IJwtData {
|
|
|
8
8
|
expiresAt: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type TAdminUser = {
|
|
12
|
+
id: string;
|
|
13
|
+
username: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
role: string;
|
|
17
|
+
status?: 'active' | 'disabled';
|
|
18
|
+
authSources?: Array<'local' | 'idp.global'>;
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
export class AdminHandler {
|
|
12
22
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
13
23
|
|
|
14
24
|
// JWT instance
|
|
15
25
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
|
16
26
|
|
|
17
|
-
//
|
|
27
|
+
// Ephemeral bootstrap users. Persisted accounts take over once an active admin exists.
|
|
18
28
|
private users = new Map<string, {
|
|
19
29
|
id: string;
|
|
20
30
|
username: string;
|
|
21
31
|
password: string;
|
|
22
32
|
role: string;
|
|
23
33
|
}>();
|
|
34
|
+
|
|
35
|
+
private accountStore?: plugins.idpSdkServer.SmartdataAccountStore;
|
|
36
|
+
private idpClient?: plugins.idpSdkServer.IdpGlobalServerClient;
|
|
37
|
+
private ownsIdpClient = false;
|
|
24
38
|
|
|
25
39
|
constructor(private opsServerRef: OpsServer) {
|
|
26
40
|
// Add this handler's router to the parent
|
|
@@ -32,6 +46,14 @@ export class AdminHandler {
|
|
|
32
46
|
this.initializeDefaultUsers();
|
|
33
47
|
this.registerHandlers();
|
|
34
48
|
}
|
|
49
|
+
|
|
50
|
+
public async stop(): Promise<void> {
|
|
51
|
+
if (this.ownsIdpClient) {
|
|
52
|
+
await this.idpClient?.stop();
|
|
53
|
+
}
|
|
54
|
+
this.idpClient = undefined;
|
|
55
|
+
this.ownsIdpClient = false;
|
|
56
|
+
}
|
|
35
57
|
|
|
36
58
|
private async initializeJwt(): Promise<void> {
|
|
37
59
|
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
|
@@ -61,54 +83,120 @@ export class AdminHandler {
|
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
/**
|
|
64
|
-
* Return a safe projection of the
|
|
86
|
+
* Return a safe projection of the active user source — excludes password fields.
|
|
65
87
|
* Used by UsersHandler to serve the admin-only listUsers endpoint.
|
|
66
88
|
*/
|
|
67
|
-
public listUsers():
|
|
89
|
+
public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
|
|
90
|
+
if (await this.hasPersistentAdminAccount()) {
|
|
91
|
+
const store = this.getAccountStore();
|
|
92
|
+
const accounts = await store!.listAccounts();
|
|
93
|
+
return accounts.map((accountArg) => this.accountToUser(accountArg));
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
return Array.from(this.users.values()).map((user) => ({
|
|
69
97
|
id: user.id,
|
|
70
98
|
username: user.username,
|
|
71
99
|
role: user.role,
|
|
72
100
|
}));
|
|
73
101
|
}
|
|
102
|
+
|
|
103
|
+
public async getBootstrapStatus(): Promise<interfaces.requests.IReq_GetAdminBootstrapStatus['response']> {
|
|
104
|
+
const dbEnabled = this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
|
|
105
|
+
const store = this.getAccountStore();
|
|
106
|
+
const dbReady = !!store;
|
|
107
|
+
const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false;
|
|
108
|
+
return {
|
|
109
|
+
dbEnabled,
|
|
110
|
+
dbReady,
|
|
111
|
+
hasPersistentAdmin,
|
|
112
|
+
needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
|
|
113
|
+
ephemeralAdminAvailable: !hasPersistentAdmin,
|
|
114
|
+
idpGlobalConfigured: this.isIdpGlobalConfigured(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public async createInitialAdminUser(optionsArg: {
|
|
119
|
+
email: string;
|
|
120
|
+
name?: string;
|
|
121
|
+
password: string;
|
|
122
|
+
enableIdpGlobalAuth?: boolean;
|
|
123
|
+
}): Promise<interfaces.requests.IReq_CreateInitialAdminUser['response']> {
|
|
124
|
+
const store = this.getAccountStore();
|
|
125
|
+
if (!store) {
|
|
126
|
+
throw new plugins.typedrequest.TypedResponseError('database is not ready');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (await store.hasActiveAdminAccount()) {
|
|
130
|
+
throw new plugins.typedrequest.TypedResponseError('initial admin already exists');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const password = String(optionsArg.password || '');
|
|
134
|
+
if (!password) {
|
|
135
|
+
throw new plugins.typedrequest.TypedResponseError('password is required');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const email = String(optionsArg.email || '').trim();
|
|
139
|
+
const authSources: Array<'local' | 'idp.global'> = ['local'];
|
|
140
|
+
if (optionsArg.enableIdpGlobalAuth) {
|
|
141
|
+
authSources.push('idp.global');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const account = await store.createAccount({
|
|
146
|
+
email,
|
|
147
|
+
name: String(optionsArg.name || '').trim() || email,
|
|
148
|
+
role: 'admin',
|
|
149
|
+
authSources,
|
|
150
|
+
password,
|
|
151
|
+
});
|
|
152
|
+
const user = this.accountToUser(account);
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
identity: await this.createIdentityForUser(user),
|
|
156
|
+
user,
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new plugins.typedrequest.TypedResponseError((error as Error).message || 'failed to create initial admin');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
74
162
|
|
|
75
163
|
private registerHandlers(): void {
|
|
164
|
+
this.typedrouter.addTypedHandler(
|
|
165
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAdminBootstrapStatus>(
|
|
166
|
+
'getAdminBootstrapStatus',
|
|
167
|
+
async (_dataArg) => this.getBootstrapStatus()
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
this.opsServerRef.adminRouter.addTypedHandler(
|
|
172
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>(
|
|
173
|
+
'createInitialAdminUser',
|
|
174
|
+
async (dataArg) => this.createInitialAdminUser({
|
|
175
|
+
email: dataArg.email,
|
|
176
|
+
name: dataArg.name,
|
|
177
|
+
password: dataArg.password,
|
|
178
|
+
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
|
|
76
183
|
// Admin Login Handler
|
|
77
184
|
this.typedrouter.addTypedHandler(
|
|
78
185
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
|
79
186
|
'adminLoginWithUsernameAndPassword',
|
|
80
187
|
async (dataArg) => {
|
|
81
188
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
189
|
+
const user = await this.authenticateUser({
|
|
190
|
+
username: dataArg.username,
|
|
191
|
+
password: dataArg.password,
|
|
192
|
+
authSource: dataArg.authSource,
|
|
193
|
+
});
|
|
91
194
|
if (!user) {
|
|
92
195
|
throw new plugins.typedrequest.TypedResponseError('login failed');
|
|
93
196
|
}
|
|
94
|
-
|
|
95
|
-
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
|
|
96
|
-
|
|
97
|
-
const jwt = await this.smartjwtInstance.createJWT({
|
|
98
|
-
userId: user.id,
|
|
99
|
-
status: 'loggedIn',
|
|
100
|
-
expiresAt: expiresAtTimestamp,
|
|
101
|
-
});
|
|
102
|
-
|
|
197
|
+
|
|
103
198
|
return {
|
|
104
|
-
identity:
|
|
105
|
-
jwt,
|
|
106
|
-
userId: user.id,
|
|
107
|
-
name: user.username,
|
|
108
|
-
expiresAt: expiresAtTimestamp,
|
|
109
|
-
role: user.role,
|
|
110
|
-
type: 'user',
|
|
111
|
-
},
|
|
199
|
+
identity: await this.createIdentityForUser(user),
|
|
112
200
|
};
|
|
113
201
|
} catch (error) {
|
|
114
202
|
if (error instanceof plugins.typedrequest.TypedResponseError) {
|
|
@@ -162,8 +250,7 @@ export class AdminHandler {
|
|
|
162
250
|
};
|
|
163
251
|
}
|
|
164
252
|
|
|
165
|
-
|
|
166
|
-
const user = this.users.get(jwtData.userId);
|
|
253
|
+
const user = await this.resolveUser(jwtData.userId);
|
|
167
254
|
if (!user) {
|
|
168
255
|
return {
|
|
169
256
|
valid: false,
|
|
@@ -175,7 +262,7 @@ export class AdminHandler {
|
|
|
175
262
|
identity: {
|
|
176
263
|
jwt: dataArg.identity.jwt,
|
|
177
264
|
userId: user.id,
|
|
178
|
-
name: user.username,
|
|
265
|
+
name: user.name || user.username,
|
|
179
266
|
expiresAt: jwtData.expiresAt,
|
|
180
267
|
role: user.role,
|
|
181
268
|
type: 'user',
|
|
@@ -224,6 +311,15 @@ export class AdminHandler {
|
|
|
224
311
|
return false;
|
|
225
312
|
}
|
|
226
313
|
|
|
314
|
+
const user = await this.resolveUser(jwtData.userId);
|
|
315
|
+
if (!user) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (dataArg.identity.role && dataArg.identity.role !== user.role) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
227
323
|
return true;
|
|
228
324
|
} catch (error) {
|
|
229
325
|
return false;
|
|
@@ -256,4 +352,120 @@ export class AdminHandler {
|
|
|
256
352
|
name: 'adminIdentityGuard',
|
|
257
353
|
}
|
|
258
354
|
);
|
|
355
|
+
|
|
356
|
+
private async authenticateUser(optionsArg: {
|
|
357
|
+
username: string;
|
|
358
|
+
password: string;
|
|
359
|
+
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
|
360
|
+
}): Promise<TAdminUser | null> {
|
|
361
|
+
if (await this.hasPersistentAdminAccount()) {
|
|
362
|
+
const store = this.getAccountStore();
|
|
363
|
+
const authService = new plugins.idpSdkServer.AccountAuthService({
|
|
364
|
+
store: store!,
|
|
365
|
+
idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
|
|
366
|
+
});
|
|
367
|
+
const result = await authService.authenticate({
|
|
368
|
+
email: optionsArg.username,
|
|
369
|
+
password: optionsArg.password,
|
|
370
|
+
authSource: optionsArg.authSource || 'auto',
|
|
371
|
+
});
|
|
372
|
+
return result ? this.accountToUser(result.account) : null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const [_, userData] of this.users) {
|
|
376
|
+
if (userData.username === optionsArg.username && userData.password === optionsArg.password) {
|
|
377
|
+
return userData;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
|
|
384
|
+
if (await this.hasPersistentAdminAccount()) {
|
|
385
|
+
const account = await this.getAccountStore()!.getAccountById(userIdArg);
|
|
386
|
+
if (!account || account.status !== 'active') {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return this.accountToUser(account);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return this.users.get(userIdArg) || null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async hasPersistentAdminAccount(): Promise<boolean> {
|
|
396
|
+
const store = this.getAccountStore();
|
|
397
|
+
return store ? store.hasActiveAdminAccount() : false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
|
|
401
|
+
if (this.opsServerRef.dcRouterRef.options.dbConfig?.enabled === false) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
|
|
405
|
+
if (!dcRouterDb?.isReady()) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (!this.accountStore) {
|
|
409
|
+
this.accountStore = new plugins.idpSdkServer.SmartdataAccountStore({
|
|
410
|
+
smartdataDb: dcRouterDb.getDb(),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return this.accountStore;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private getIdpClient(): Pick<plugins.idpSdkServer.IdpGlobalServerClient, 'loginWithEmailAndPassword' | 'stop'> | undefined {
|
|
417
|
+
const configuredClient = this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient;
|
|
418
|
+
if (configuredClient) {
|
|
419
|
+
return configuredClient;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const baseUrl = this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl || process.env.DCROUTER_IDP_GLOBAL_URL;
|
|
423
|
+
if (!baseUrl) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!this.idpClient) {
|
|
428
|
+
this.idpClient = new plugins.idpSdkServer.IdpGlobalServerClient({ baseUrl });
|
|
429
|
+
this.ownsIdpClient = true;
|
|
430
|
+
}
|
|
431
|
+
return this.idpClient;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private isIdpGlobalConfigured(): boolean {
|
|
435
|
+
return !!(
|
|
436
|
+
this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient ||
|
|
437
|
+
this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl ||
|
|
438
|
+
process.env.DCROUTER_IDP_GLOBAL_URL
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private accountToUser(accountArg: plugins.idpSdkServer.IIdpSdkAccount): TAdminUser {
|
|
443
|
+
return {
|
|
444
|
+
id: accountArg.id,
|
|
445
|
+
username: accountArg.email,
|
|
446
|
+
email: accountArg.email,
|
|
447
|
+
name: accountArg.name,
|
|
448
|
+
role: accountArg.role,
|
|
449
|
+
status: accountArg.status,
|
|
450
|
+
authSources: accountArg.authSources,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private async createIdentityForUser(userArg: TAdminUser): Promise<interfaces.data.IIdentity> {
|
|
455
|
+
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
|
|
456
|
+
const jwt = await this.smartjwtInstance.createJWT({
|
|
457
|
+
userId: userArg.id,
|
|
458
|
+
status: 'loggedIn',
|
|
459
|
+
expiresAt: expiresAtTimestamp,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
jwt,
|
|
464
|
+
userId: userArg.id,
|
|
465
|
+
name: userArg.name || userArg.username,
|
|
466
|
+
expiresAt: expiresAtTimestamp,
|
|
467
|
+
role: userArg.role,
|
|
468
|
+
type: 'user',
|
|
469
|
+
};
|
|
470
|
+
}
|
|
259
471
|
}
|
|
@@ -88,6 +88,8 @@ export class TargetProfileHandler {
|
|
|
88
88
|
routeRefs: dataArg.routeRefs,
|
|
89
89
|
createdBy: userId,
|
|
90
90
|
});
|
|
91
|
+
await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
|
92
|
+
await this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
|
91
93
|
return { success: true, id };
|
|
92
94
|
},
|
|
93
95
|
),
|
|
@@ -21,7 +21,7 @@ export class UsersHandler {
|
|
|
21
21
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListUsers>(
|
|
22
22
|
'listUsers',
|
|
23
23
|
async (_dataArg) => {
|
|
24
|
-
const users = this.opsServerRef.adminHandler.listUsers();
|
|
24
|
+
const users = await this.opsServerRef.adminHandler.listUsers();
|
|
25
25
|
return { users };
|
|
26
26
|
},
|
|
27
27
|
),
|
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';
|
package/ts/readme.md
CHANGED
|
@@ -91,7 +91,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
91
91
|
|
|
92
92
|
### Company Information
|
|
93
93
|
|
|
94
|
-
Task Venture Capital GmbH
|
|
94
|
+
Task Venture Capital GmbH
|
|
95
95
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
96
96
|
|
|
97
97
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
@@ -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;
|
package/ts_apiclient/readme.md
CHANGED
|
@@ -27,7 +27,7 @@ const client = new DcRouterApiClient({
|
|
|
27
27
|
baseUrl: 'https://dcrouter.example.com',
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
await client.login('admin', '
|
|
30
|
+
await client.login('admin@example.com', 'strong-password');
|
|
31
31
|
|
|
32
32
|
const { routes, warnings } = await client.routes.list();
|
|
33
33
|
console.log(routes.length, warnings.length);
|
|
@@ -43,13 +43,13 @@ await route.toggle(true);
|
|
|
43
43
|
|
|
44
44
|
## Authentication
|
|
45
45
|
|
|
46
|
-
The client supports session login and API-token authentication.
|
|
46
|
+
The client supports persisted-admin session login and API-token authentication. Initial admin creation is a bootstrap flow exposed by the Ops dashboard and raw TypedRequest contracts; after a persisted admin exists, use that account with `login()`.
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
49
|
const sessionClient = new DcRouterApiClient({
|
|
50
50
|
baseUrl: 'https://dcrouter.example.com',
|
|
51
51
|
});
|
|
52
|
-
await sessionClient.login('admin', '
|
|
52
|
+
await sessionClient.login('admin@example.com', 'strong-password');
|
|
53
53
|
|
|
54
54
|
const tokenClient = new DcRouterApiClient({
|
|
55
55
|
baseUrl: 'https://dcrouter.example.com',
|
|
@@ -153,7 +153,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
153
153
|
|
|
154
154
|
### Company Information
|
|
155
155
|
|
|
156
|
-
Task Venture Capital GmbH
|
|
156
|
+
Task Venture Capital GmbH
|
|
157
157
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
158
158
|
|
|
159
159
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
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;
|
|
@@ -312,7 +314,11 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
|
|
|
312
314
|
export interface IUser {
|
|
313
315
|
id: string;
|
|
314
316
|
username: string;
|
|
317
|
+
email?: string;
|
|
318
|
+
name?: string;
|
|
315
319
|
role: string;
|
|
320
|
+
status?: 'active' | 'disabled';
|
|
321
|
+
authSources?: Array<'local' | 'idp.global'>;
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
export interface IUsersState {
|
|
@@ -351,6 +357,7 @@ const getActionContext = (): IActionContext => {
|
|
|
351
357
|
export const loginAction = loginStatePart.createAction<{
|
|
352
358
|
username: string;
|
|
353
359
|
password: string;
|
|
360
|
+
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
|
354
361
|
}>(async (statePartArg, dataArg): Promise<ILoginState> => {
|
|
355
362
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
356
363
|
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
|
@@ -360,6 +367,7 @@ export const loginAction = loginStatePart.createAction<{
|
|
|
360
367
|
const response = await typedRequest.fire({
|
|
361
368
|
username: dataArg.username,
|
|
362
369
|
password: dataArg.password,
|
|
370
|
+
authSource: dataArg.authSource,
|
|
363
371
|
});
|
|
364
372
|
|
|
365
373
|
if (response.identity) {
|
|
@@ -375,6 +383,47 @@ export const loginAction = loginStatePart.createAction<{
|
|
|
375
383
|
}
|
|
376
384
|
});
|
|
377
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
|
+
|
|
378
427
|
// Logout Action — always clears state, even if identity is expired/missing
|
|
379
428
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
|
380
429
|
const context = getActionContext();
|