@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.
Files changed (69) hide show
  1. package/.smartconfig.json +32 -10
  2. package/dist_serve/bundle.js +930 -799
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +9 -1
  5. package/dist_ts/classes.dcrouter.js +22 -7
  6. package/dist_ts/config/classes.gateway-client-manager.d.ts +22 -0
  7. package/dist_ts/config/classes.gateway-client-manager.js +101 -0
  8. package/dist_ts/config/classes.route-config-manager.js +8 -7
  9. package/dist_ts/config/index.d.ts +1 -0
  10. package/dist_ts/config/index.js +2 -1
  11. package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +18 -0
  12. package/dist_ts/db/documents/classes.gateway-client.doc.js +133 -0
  13. package/dist_ts/db/documents/index.d.ts +1 -0
  14. package/dist_ts/db/documents/index.js +2 -1
  15. package/dist_ts/opsserver/classes.opsserver.js +4 -1
  16. package/dist_ts/opsserver/handlers/admin.handler.d.ts +21 -6
  17. package/dist_ts/opsserver/handlers/admin.handler.js +188 -29
  18. package/dist_ts/opsserver/handlers/certificate.handler.js +5 -1
  19. package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
  20. package/dist_ts/opsserver/handlers/users.handler.js +2 -2
  21. package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +4 -0
  22. package/dist_ts/opsserver/handlers/workhoster.handler.js +146 -16
  23. package/dist_ts/plugins.d.ts +2 -0
  24. package/dist_ts/plugins.js +4 -1
  25. package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -0
  26. package/dist_ts/vpn/classes.vpn-manager.js +41 -20
  27. package/dist_ts_apiclient/classes.workhoster.d.ts +1 -0
  28. package/dist_ts_apiclient/classes.workhoster.js +5 -1
  29. package/dist_ts_interfaces/data/workhoster.d.ts +28 -3
  30. package/dist_ts_interfaces/requests/admin.d.ts +38 -0
  31. package/dist_ts_interfaces/requests/users.d.ts +2 -5
  32. package/dist_ts_interfaces/requests/workhoster.d.ts +83 -1
  33. package/dist_ts_web/00_commitinfo_data.js +1 -1
  34. package/dist_ts_web/appstate.d.ts +46 -0
  35. package/dist_ts_web/appstate.js +105 -1
  36. package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -1
  37. package/dist_ts_web/elements/access/ops-view-gatewayclients.d.ts +15 -0
  38. package/dist_ts_web/elements/access/ops-view-gatewayclients.js +293 -0
  39. package/dist_ts_web/elements/domains/ops-view-certificates.d.ts +6 -0
  40. package/dist_ts_web/elements/domains/ops-view-certificates.js +155 -13
  41. package/dist_ts_web/elements/network/ops-view-routes.js +2 -1
  42. package/dist_ts_web/elements/ops-dashboard.d.ts +4 -0
  43. package/dist_ts_web/elements/ops-dashboard.js +102 -3
  44. package/dist_ts_web/router.js +3 -3
  45. package/package.json +15 -22
  46. package/ts/00_commitinfo_data.ts +1 -1
  47. package/ts/classes.dcrouter.ts +30 -6
  48. package/ts/config/classes.gateway-client-manager.ts +117 -0
  49. package/ts/config/classes.route-config-manager.ts +8 -6
  50. package/ts/config/index.ts +2 -1
  51. package/ts/db/documents/classes.gateway-client.doc.ts +54 -0
  52. package/ts/db/documents/index.ts +1 -0
  53. package/ts/opsserver/classes.opsserver.ts +3 -0
  54. package/ts/opsserver/handlers/admin.handler.ts +244 -32
  55. package/ts/opsserver/handlers/certificate.handler.ts +5 -0
  56. package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
  57. package/ts/opsserver/handlers/users.handler.ts +1 -1
  58. package/ts/opsserver/handlers/workhoster.handler.ts +191 -17
  59. package/ts/plugins.ts +7 -0
  60. package/ts/vpn/classes.vpn-manager.ts +56 -25
  61. package/ts_apiclient/classes.workhoster.ts +8 -0
  62. package/ts_web/00_commitinfo_data.ts +1 -1
  63. package/ts_web/appstate.ts +160 -0
  64. package/ts_web/elements/access/ops-view-apitokens.ts +1 -0
  65. package/ts_web/elements/access/ops-view-gatewayclients.ts +250 -0
  66. package/ts_web/elements/domains/ops-view-certificates.ts +166 -11
  67. package/ts_web/elements/network/ops-view-routes.ts +1 -0
  68. package/ts_web/elements/ops-dashboard.ts +102 -0
  69. package/ts_web/router.ts +2 -2
@@ -0,0 +1,54 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { IApiTokenPolicy, TGatewayClientType } from '../../../ts_interfaces/data/route-management.js';
4
+
5
+ const getDb = () => DcRouterDb.getInstance().getDb();
6
+
7
+ @plugins.smartdata.Collection(() => getDb())
8
+ export class GatewayClientDoc extends plugins.smartdata.SmartDataDbDoc<GatewayClientDoc, GatewayClientDoc> {
9
+ @plugins.smartdata.unI()
10
+ @plugins.smartdata.svDb()
11
+ public id!: string;
12
+
13
+ @plugins.smartdata.svDb()
14
+ public type!: TGatewayClientType;
15
+
16
+ @plugins.smartdata.svDb()
17
+ public name: string = '';
18
+
19
+ @plugins.smartdata.svDb()
20
+ public description?: string;
21
+
22
+ @plugins.smartdata.svDb()
23
+ public hostnamePatterns: string[] = [];
24
+
25
+ @plugins.smartdata.svDb()
26
+ public allowedRouteTargets: NonNullable<IApiTokenPolicy['allowedRouteTargets']> = [];
27
+
28
+ @plugins.smartdata.svDb()
29
+ public capabilities: NonNullable<IApiTokenPolicy['capabilities']> = {};
30
+
31
+ @plugins.smartdata.svDb()
32
+ public enabled: boolean = true;
33
+
34
+ @plugins.smartdata.svDb()
35
+ public createdAt!: number;
36
+
37
+ @plugins.smartdata.svDb()
38
+ public updatedAt!: number;
39
+
40
+ @plugins.smartdata.svDb()
41
+ public createdBy!: string;
42
+
43
+ constructor() {
44
+ super();
45
+ }
46
+
47
+ public static async findById(id: string): Promise<GatewayClientDoc | null> {
48
+ return await GatewayClientDoc.getInstance({ id });
49
+ }
50
+
51
+ public static async findAll(): Promise<GatewayClientDoc[]> {
52
+ return await GatewayClientDoc.getInstances({});
53
+ }
54
+ }
@@ -8,6 +8,7 @@ export * from './classes.security-policy-audit.doc.js';
8
8
  // Config document classes
9
9
  export * from './classes.route.doc.js';
10
10
  export * from './classes.api-token.doc.js';
11
+ export * from './classes.gateway-client.doc.js';
11
12
  export * from './classes.source-profile.doc.js';
12
13
  export * from './classes.target-profile.doc.js';
13
14
  export * from './classes.network-target.doc.js';
@@ -113,6 +113,9 @@ export class OpsServer {
113
113
  }
114
114
 
115
115
  public async stop() {
116
+ if (this.adminHandler) {
117
+ await this.adminHandler.stop();
118
+ }
116
119
  // Clean up log handler streams and push destination before stopping the server
117
120
  if (this.logsHandler) {
118
121
  this.logsHandler.cleanup();
@@ -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
- // Simple in-memory user storage (in production, use proper database)
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 users Map — excludes password fields.
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(): Array<{ id: string; username: string; role: string }> {
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
- // Find user by username and password
83
- let user: { id: string; username: string; password: string; role: string } | null = null;
84
- for (const [_, userData] of this.users) {
85
- if (userData.username === dataArg.username && userData.password === dataArg.password) {
86
- user = userData;
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
- // Find user
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
  }
@@ -307,6 +307,11 @@ export class CertificateHandler {
307
307
  }
308
308
  }
309
309
 
310
+ if (backoffInfo && status !== 'valid' && status !== 'expiring') {
311
+ status = 'failed';
312
+ error = error || backoffInfo.lastError;
313
+ }
314
+
310
315
  certificates.push({
311
316
  domain,
312
317
  routeNames: info.routeNames,
@@ -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
  ),