@serve.zone/dcrouter 13.31.0 → 13.32.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 (80) hide show
  1. package/dist_serve/bundle.js +721 -721
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
  4. package/dist_ts/config/classes.route-config-manager.js +28 -3
  5. package/dist_ts/config/classes.target-profile-manager.d.ts +1 -0
  6. package/dist_ts/config/classes.target-profile-manager.js +11 -8
  7. package/dist_ts/opsserver/classes.opsserver.d.ts +4 -2
  8. package/dist_ts/opsserver/classes.opsserver.js +2 -11
  9. package/dist_ts/opsserver/handlers/acme-config.handler.js +7 -24
  10. package/dist_ts/opsserver/handlers/admin.handler.d.ts +3 -1
  11. package/dist_ts/opsserver/handlers/admin.handler.js +95 -110
  12. package/dist_ts/opsserver/handlers/api-token.handler.js +28 -2
  13. package/dist_ts/opsserver/handlers/certificate.handler.js +7 -24
  14. package/dist_ts/opsserver/handlers/config.handler.js +3 -1
  15. package/dist_ts/opsserver/handlers/dns-provider.handler.js +7 -24
  16. package/dist_ts/opsserver/handlers/dns-record.handler.js +7 -24
  17. package/dist_ts/opsserver/handlers/domain.handler.js +7 -24
  18. package/dist_ts/opsserver/handlers/email-domain.handler.js +7 -24
  19. package/dist_ts/opsserver/handlers/email-ops.handler.js +8 -1
  20. package/dist_ts/opsserver/handlers/logs.handler.js +4 -1
  21. package/dist_ts/opsserver/handlers/network-target.handler.js +7 -24
  22. package/dist_ts/opsserver/handlers/radius.handler.js +32 -1
  23. package/dist_ts/opsserver/handlers/remoteingress.handler.js +24 -1
  24. package/dist_ts/opsserver/handlers/route-management.handler.js +7 -26
  25. package/dist_ts/opsserver/handlers/security.handler.js +32 -7
  26. package/dist_ts/opsserver/handlers/source-profile.handler.js +7 -24
  27. package/dist_ts/opsserver/handlers/stats.handler.js +8 -1
  28. package/dist_ts/opsserver/handlers/target-profile.handler.js +7 -24
  29. package/dist_ts/opsserver/handlers/users.handler.js +33 -13
  30. package/dist_ts/opsserver/handlers/vpn.handler.js +34 -1
  31. package/dist_ts/opsserver/handlers/workhoster.handler.js +16 -35
  32. package/dist_ts/opsserver/helpers/auth.d.ts +21 -0
  33. package/dist_ts/opsserver/helpers/auth.js +63 -0
  34. package/dist_ts_interfaces/data/route-management.d.ts +2 -1
  35. package/dist_ts_interfaces/data/route-management.js +48 -2
  36. package/dist_ts_interfaces/requests/api-tokens.d.ts +10 -5
  37. package/dist_ts_interfaces/requests/combined.stats.d.ts +2 -1
  38. package/dist_ts_interfaces/requests/config.d.ts +2 -1
  39. package/dist_ts_interfaces/requests/email-ops.d.ts +6 -3
  40. package/dist_ts_interfaces/requests/logs.d.ts +4 -2
  41. package/dist_ts_interfaces/requests/radius.d.ts +24 -12
  42. package/dist_ts_interfaces/requests/remoteingress.d.ts +14 -7
  43. package/dist_ts_interfaces/requests/security-policy.d.ts +16 -8
  44. package/dist_ts_interfaces/requests/stats.d.ts +18 -9
  45. package/dist_ts_interfaces/requests/users.d.ts +6 -3
  46. package/dist_ts_interfaces/requests/vpn.d.ts +22 -11
  47. package/dist_ts_interfaces/requests/workhoster.d.ts +10 -5
  48. package/dist_ts_migrations/index.js +3 -1
  49. package/dist_ts_web/00_commitinfo_data.js +1 -1
  50. package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -21
  51. package/package.json +4 -4
  52. package/ts/00_commitinfo_data.ts +1 -1
  53. package/ts/config/classes.route-config-manager.ts +35 -2
  54. package/ts/config/classes.target-profile-manager.ts +10 -7
  55. package/ts/opsserver/classes.opsserver.ts +3 -14
  56. package/ts/opsserver/handlers/acme-config.handler.ts +6 -23
  57. package/ts/opsserver/handlers/admin.handler.ts +109 -123
  58. package/ts/opsserver/handlers/api-token.handler.ts +27 -1
  59. package/ts/opsserver/handlers/certificate.handler.ts +6 -23
  60. package/ts/opsserver/handlers/config.handler.ts +2 -0
  61. package/ts/opsserver/handlers/dns-provider.handler.ts +6 -23
  62. package/ts/opsserver/handlers/dns-record.handler.ts +6 -23
  63. package/ts/opsserver/handlers/domain.handler.ts +6 -23
  64. package/ts/opsserver/handlers/email-domain.handler.ts +6 -23
  65. package/ts/opsserver/handlers/email-ops.handler.ts +7 -0
  66. package/ts/opsserver/handlers/logs.handler.ts +3 -0
  67. package/ts/opsserver/handlers/network-target.handler.ts +6 -23
  68. package/ts/opsserver/handlers/radius.handler.ts +31 -0
  69. package/ts/opsserver/handlers/remoteingress.handler.ts +23 -0
  70. package/ts/opsserver/handlers/route-management.handler.ts +6 -25
  71. package/ts/opsserver/handlers/security.handler.ts +31 -6
  72. package/ts/opsserver/handlers/source-profile.handler.ts +6 -23
  73. package/ts/opsserver/handlers/stats.handler.ts +7 -0
  74. package/ts/opsserver/handlers/target-profile.handler.ts +6 -23
  75. package/ts/opsserver/handlers/users.handler.ts +32 -12
  76. package/ts/opsserver/handlers/vpn.handler.ts +33 -0
  77. package/ts/opsserver/handlers/workhoster.handler.ts +18 -33
  78. package/ts/opsserver/helpers/auth.ts +91 -0
  79. package/ts_web/00_commitinfo_data.ts +1 -1
  80. package/ts_web/elements/access/ops-view-apitokens.ts +1 -20
@@ -608,9 +608,23 @@ export class RouteConfigManager {
608
608
  routeId?: string,
609
609
  ): plugins.smartproxy.IRouteConfig {
610
610
  const dcRoute = route as IDcRouterRouteConfig;
611
- if (!dcRoute.vpnOnly) return route;
612
-
613
611
  const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
612
+
613
+ if (!dcRoute.vpnOnly) {
614
+ const existingAllowList = route.security?.ipAllowList;
615
+ if (!Array.isArray(existingAllowList) || existingAllowList.length === 0 || vpnEntries.length === 0) {
616
+ return route;
617
+ }
618
+
619
+ return {
620
+ ...route,
621
+ security: {
622
+ ...route.security,
623
+ ipAllowList: this.mergeIpAllowEntries(existingAllowList as TIpAllowEntry[], vpnEntries),
624
+ },
625
+ };
626
+ }
627
+
614
628
  const existingBlockList = route.security?.ipBlockList || [];
615
629
  const ipBlockList = vpnEntries.length
616
630
  ? existingBlockList
@@ -625,4 +639,23 @@ export class RouteConfigManager {
625
639
  },
626
640
  };
627
641
  }
642
+
643
+ private mergeIpAllowEntries(
644
+ existingEntries: TIpAllowEntry[],
645
+ vpnEntries: TIpAllowEntry[],
646
+ ): TIpAllowEntry[] {
647
+ const merged: TIpAllowEntry[] = [];
648
+ const seen = new Set<string>();
649
+
650
+ for (const entry of [...existingEntries, ...vpnEntries]) {
651
+ const key = typeof entry === 'string'
652
+ ? `ip:${entry}`
653
+ : `domain:${entry.ip}:${[...entry.domains].sort().join(',')}`;
654
+ if (seen.has(key)) continue;
655
+ seen.add(key);
656
+ merged.push(entry);
657
+ }
658
+
659
+ return merged;
660
+ }
628
661
  }
@@ -217,7 +217,7 @@ export class TargetProfileManager {
217
217
  allRoutes: Map<string, IRoute> = new Map(),
218
218
  ): Array<string | { ip: string; domains: string[] }> {
219
219
  const entries: Array<string | { ip: string; domains: string[] }> = [];
220
- const routeDomains: string[] = (route.match as any)?.domains || [];
220
+ const routeDomains = this.getRouteDomains(route);
221
221
  const routeNameIndex = this.buildRouteNameIndex(allRoutes);
222
222
 
223
223
  for (const client of clients) {
@@ -298,11 +298,8 @@ export class TargetProfileManager {
298
298
  profile,
299
299
  routeNameIndex,
300
300
  )) {
301
- const routeDomains = (route.route.match as any)?.domains;
302
- if (Array.isArray(routeDomains)) {
303
- for (const d of routeDomains) {
304
- domains.add(d);
305
- }
301
+ for (const d of this.getRouteDomains(route.route as IDcRouterRouteConfig)) {
302
+ domains.add(d);
306
303
  }
307
304
  }
308
305
  }
@@ -327,7 +324,7 @@ export class TargetProfileManager {
327
324
  profile: ITargetProfile,
328
325
  routeNameIndex: Map<string, string[]>,
329
326
  ): boolean {
330
- const routeDomains: string[] = (route.match as any)?.domains || [];
327
+ const routeDomains = this.getRouteDomains(route);
331
328
  const result = this.routeMatchesProfileDetailed(
332
329
  route,
333
330
  routeId,
@@ -425,6 +422,12 @@ export class TargetProfileManager {
425
422
  return false;
426
423
  }
427
424
 
425
+ private getRouteDomains(route: IDcRouterRouteConfig): string[] {
426
+ const domains = (route.match as any)?.domains;
427
+ if (!domains) return [];
428
+ return Array.isArray(domains) ? domains : [domains];
429
+ }
430
+
428
431
  private normalizeRouteRefs(routeRefs?: string[]): string[] | undefined {
429
432
  const allRoutes = this.getAllRoutes?.() || new Map<string, IRoute>();
430
433
  return this.normalizeRouteRefsAgainstRoutes(routeRefs, allRoutes, 'strict');
@@ -3,7 +3,6 @@ import * as plugins from '../plugins.js';
3
3
  import * as paths from '../paths.js';
4
4
  import * as handlers from './handlers/index.js';
5
5
  import * as interfaces from '../../ts_interfaces/index.js';
6
- import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js';
7
6
 
8
7
  export class OpsServer {
9
8
  public dcRouterRef: DcRouter;
@@ -12,9 +11,9 @@ export class OpsServer {
12
11
  // Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
13
12
  public typedrouter = new plugins.typedrequest.TypedRouter();
14
13
 
15
- // Auth-enforced routers middleware validates identity before any handler runs
16
- public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
17
- public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
14
+ // Grouped routers. Handlers enforce auth explicitly with per-endpoint scopes.
15
+ public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
16
+ public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity?: interfaces.data.IIdentity; apiToken?: string } }>();
18
17
 
19
18
  // Handler instances
20
19
  public adminHandler!: handlers.AdminHandler;
@@ -72,16 +71,6 @@ export class OpsServer {
72
71
  this.adminHandler = new handlers.AdminHandler(this);
73
72
  await this.adminHandler.initialize();
74
73
 
75
- // viewRouter middleware: requires valid identity (any logged-in user)
76
- this.viewRouter.addMiddleware(async (typedRequest) => {
77
- await requireValidIdentity(this.adminHandler, typedRequest.request);
78
- });
79
-
80
- // adminRouter middleware: requires admin identity
81
- this.adminRouter.addMiddleware(async (typedRequest) => {
82
- await requireAdminIdentity(this.adminHandler, typedRequest.request);
83
- });
84
-
85
74
  // Connect auth routers to the main typedrouter
86
75
  this.typedrouter.addTypedRouter(this.viewRouter);
87
76
  this.typedrouter.addTypedRouter(this.adminRouter);
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
+ import { requireOpsAuth } from '../helpers/auth.js';
4
5
 
5
6
  /**
6
7
  * CRUD handler for the singleton `AcmeConfigDoc`.
@@ -20,29 +21,11 @@ export class AcmeConfigHandler {
20
21
  request: { identity?: interfaces.data.IIdentity; apiToken?: string },
21
22
  requiredScope?: interfaces.data.TApiTokenScope,
22
23
  ): Promise<string> {
23
- if (request.identity?.jwt) {
24
- try {
25
- const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
26
- identity: request.identity,
27
- });
28
- if (isAdmin) return request.identity.userId;
29
- } catch { /* fall through */ }
30
- }
31
-
32
- if (request.apiToken) {
33
- const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
34
- if (tokenManager) {
35
- const token = await tokenManager.validateToken(request.apiToken);
36
- if (token) {
37
- if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
38
- return token.createdBy;
39
- }
40
- throw new plugins.typedrequest.TypedResponseError('insufficient scope');
41
- }
42
- }
43
- }
44
-
45
- throw new plugins.typedrequest.TypedResponseError('unauthorized');
24
+ const auth = await requireOpsAuth(this.opsServerRef, request, {
25
+ scope: requiredScope,
26
+ requireAdminIdentity: requiredScope?.endsWith(':write'),
27
+ });
28
+ return auth.userId;
46
29
  }
47
30
 
48
31
  private registerHandlers(): void {
@@ -24,7 +24,8 @@ export class AdminHandler {
24
24
  // JWT instance
25
25
  public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
26
26
 
27
- // Ephemeral bootstrap users. Persisted accounts take over once an active admin exists.
27
+ // Ephemeral bootstrap users. DB-backed instances may use these only until the
28
+ // database is ready and the first persistent admin account has been created.
28
29
  private users = new Map<string, {
29
30
  id: string;
30
31
  username: string;
@@ -87,9 +88,12 @@ export class AdminHandler {
87
88
  * Used by UsersHandler to serve the admin-only listUsers endpoint.
88
89
  */
89
90
  public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
90
- if (await this.hasPersistentAdminAccount()) {
91
- const store = this.getAccountStore();
92
- const accounts = await store!.listAccounts();
91
+ const accountState = await this.getPersistentAccountState();
92
+ if (accountState.dbEnabled && !accountState.dbReady) {
93
+ throw new plugins.typedrequest.TypedResponseError('database is not ready');
94
+ }
95
+ if (accountState.hasPersistentAdmin) {
96
+ const accounts = await accountState.store!.listAccounts();
93
97
  return accounts.map((accountArg) => this.accountToUser(accountArg));
94
98
  }
95
99
 
@@ -101,16 +105,14 @@ export class AdminHandler {
101
105
  }
102
106
 
103
107
  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
+ const accountState = await this.getPersistentAccountState();
109
+ const bootstrapAvailable = !accountState.dbEnabled || (accountState.dbReady && !accountState.hasPersistentAdmin);
108
110
  return {
109
- dbEnabled,
110
- dbReady,
111
- hasPersistentAdmin,
112
- needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
113
- ephemeralAdminAvailable: !hasPersistentAdmin,
111
+ dbEnabled: accountState.dbEnabled,
112
+ dbReady: accountState.dbReady,
113
+ hasPersistentAdmin: accountState.hasPersistentAdmin,
114
+ needsBootstrap: accountState.dbEnabled && accountState.dbReady && !accountState.hasPersistentAdmin,
115
+ ephemeralAdminAvailable: bootstrapAvailable,
114
116
  idpGlobalConfigured: this.isIdpGlobalConfigured(),
115
117
  };
116
118
  }
@@ -258,12 +260,18 @@ export class AdminHandler {
258
260
  this.opsServerRef.adminRouter.addTypedHandler(
259
261
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateInitialAdminUser>(
260
262
  'createInitialAdminUser',
261
- async (dataArg) => this.createInitialAdminUser({
262
- email: dataArg.email,
263
- name: dataArg.name,
264
- password: dataArg.password,
265
- enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
266
- })
263
+ async (dataArg) => {
264
+ const isAdmin = await this.adminIdentityGuard.exec({ identity: dataArg.identity });
265
+ if (!isAdmin) {
266
+ throw new plugins.typedrequest.TypedResponseError('admin identity required');
267
+ }
268
+ return this.createInitialAdminUser({
269
+ email: dataArg.email,
270
+ name: dataArg.name,
271
+ password: dataArg.password,
272
+ enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
273
+ });
274
+ }
267
275
  )
268
276
  );
269
277
 
@@ -300,8 +308,10 @@ export class AdminHandler {
300
308
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
301
309
  'adminLogout',
302
310
  async (dataArg) => {
303
- // In a real implementation, you might want to blacklist the JWT
304
- // For now, just return success
311
+ const identity = await this.validateIdentity(dataArg.identity);
312
+ if (!identity) {
313
+ throw new plugins.typedrequest.TypedResponseError('identity is not valid');
314
+ }
305
315
  return {
306
316
  success: true,
307
317
  };
@@ -314,52 +324,8 @@ export class AdminHandler {
314
324
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
315
325
  'verifyIdentity',
316
326
  async (dataArg) => {
317
- if (!dataArg.identity?.jwt) {
318
- return {
319
- valid: false,
320
- };
321
- }
322
-
323
- try {
324
- const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
325
-
326
- // Check if expired
327
- if (jwtData.expiresAt < Date.now()) {
328
- return {
329
- valid: false,
330
- };
331
- }
332
-
333
- // Check if logged in
334
- if (jwtData.status !== 'loggedIn') {
335
- return {
336
- valid: false,
337
- };
338
- }
339
-
340
- const user = await this.resolveUser(jwtData.userId);
341
- if (!user) {
342
- return {
343
- valid: false,
344
- };
345
- }
346
-
347
- return {
348
- valid: true,
349
- identity: {
350
- jwt: dataArg.identity.jwt,
351
- userId: user.id,
352
- name: user.name || user.username,
353
- expiresAt: jwtData.expiresAt,
354
- role: user.role,
355
- type: 'user',
356
- },
357
- };
358
- } catch (error) {
359
- return {
360
- valid: false,
361
- };
362
- }
327
+ const identity = await this.validateIdentity(dataArg.identity);
328
+ return identity ? { valid: true, identity } : { valid: false };
363
329
  }
364
330
  )
365
331
  );
@@ -372,45 +338,7 @@ export class AdminHandler {
372
338
  identity: interfaces.data.IIdentity;
373
339
  }>(
374
340
  async (dataArg) => {
375
- if (!dataArg.identity?.jwt) {
376
- return false;
377
- }
378
-
379
- try {
380
- const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
381
-
382
- // Check expiration
383
- if (jwtData.expiresAt < Date.now()) {
384
- return false;
385
- }
386
-
387
- // Check status
388
- if (jwtData.status !== 'loggedIn') {
389
- return false;
390
- }
391
-
392
- // Verify data hasn't been tampered with
393
- if (dataArg.identity.expiresAt !== jwtData.expiresAt) {
394
- return false;
395
- }
396
-
397
- if (dataArg.identity.userId !== jwtData.userId) {
398
- return false;
399
- }
400
-
401
- const user = await this.resolveUser(jwtData.userId);
402
- if (!user) {
403
- return false;
404
- }
405
-
406
- if (dataArg.identity.role && dataArg.identity.role !== user.role) {
407
- return false;
408
- }
409
-
410
- return true;
411
- } catch (error) {
412
- return false;
413
- }
341
+ return Boolean(await this.validateIdentity(dataArg.identity));
414
342
  },
415
343
  {
416
344
  failedHint: 'identity is not valid',
@@ -425,14 +353,8 @@ export class AdminHandler {
425
353
  identity: interfaces.data.IIdentity;
426
354
  }>(
427
355
  async (dataArg) => {
428
- // First check if identity is valid
429
- const isValid = await this.validIdentityGuard.exec(dataArg);
430
- if (!isValid) {
431
- return false;
432
- }
433
-
434
- // Check if user has admin role
435
- return dataArg.identity.role === 'admin';
356
+ const identity = await this.validateIdentity(dataArg.identity);
357
+ return identity?.role === 'admin';
436
358
  },
437
359
  {
438
360
  failedHint: 'user is not admin',
@@ -440,15 +362,62 @@ export class AdminHandler {
440
362
  }
441
363
  );
442
364
 
365
+ public async validateIdentity(
366
+ identityArg?: interfaces.data.IIdentity,
367
+ ): Promise<interfaces.data.IIdentity | null> {
368
+ if (!identityArg?.jwt) {
369
+ return null;
370
+ }
371
+
372
+ try {
373
+ const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt);
374
+ if (jwtData.expiresAt < Date.now()) {
375
+ return null;
376
+ }
377
+ if (jwtData.status !== 'loggedIn') {
378
+ return null;
379
+ }
380
+ if (identityArg.expiresAt !== jwtData.expiresAt) {
381
+ return null;
382
+ }
383
+ if (identityArg.userId !== jwtData.userId) {
384
+ return null;
385
+ }
386
+
387
+ const user = await this.resolveUser(jwtData.userId);
388
+ if (!user) {
389
+ return null;
390
+ }
391
+ if (identityArg.role && identityArg.role !== user.role) {
392
+ return null;
393
+ }
394
+
395
+ return {
396
+ jwt: identityArg.jwt,
397
+ userId: user.id,
398
+ name: user.name || user.username,
399
+ expiresAt: jwtData.expiresAt,
400
+ role: user.role,
401
+ type: 'user',
402
+ };
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+
443
408
  private async authenticateUser(optionsArg: {
444
409
  username: string;
445
410
  password: string;
446
411
  authSource?: interfaces.requests.TAdminLoginAuthSource;
447
412
  }): Promise<TAdminUser | null> {
448
- if (await this.hasPersistentAdminAccount()) {
449
- const store = this.getAccountStore();
413
+ const accountState = await this.getPersistentAccountState();
414
+ if (accountState.dbEnabled && !accountState.dbReady) {
415
+ throw new plugins.typedrequest.TypedResponseError('database is not ready');
416
+ }
417
+
418
+ if (accountState.hasPersistentAdmin) {
450
419
  const authService = new plugins.idpSdkServer.AccountAuthService({
451
- store: store!,
420
+ store: accountState.store!,
452
421
  idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
453
422
  });
454
423
  const result = await authService.authenticate({
@@ -468,8 +437,13 @@ export class AdminHandler {
468
437
  }
469
438
 
470
439
  private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
471
- if (await this.hasPersistentAdminAccount()) {
472
- const account = await this.getAccountStore()!.getAccountById(userIdArg);
440
+ const accountState = await this.getPersistentAccountState();
441
+ if (accountState.dbEnabled && !accountState.dbReady) {
442
+ return null;
443
+ }
444
+
445
+ if (accountState.hasPersistentAdmin) {
446
+ const account = await accountState.store!.getAccountById(userIdArg);
473
447
  if (!account || account.status !== 'active') {
474
448
  return null;
475
449
  }
@@ -479,13 +453,25 @@ export class AdminHandler {
479
453
  return this.users.get(userIdArg) || null;
480
454
  }
481
455
 
482
- private async hasPersistentAdminAccount(): Promise<boolean> {
483
- const store = this.getAccountStore();
484
- return store ? store.hasActiveAdminAccount() : false;
456
+ private async getPersistentAccountState(): Promise<{
457
+ dbEnabled: boolean;
458
+ dbReady: boolean;
459
+ store: plugins.idpSdkServer.SmartdataAccountStore | null;
460
+ hasPersistentAdmin: boolean;
461
+ }> {
462
+ const dbEnabled = this.isPersistenceEnabled();
463
+ const store = dbEnabled ? this.getAccountStore() : null;
464
+ const dbReady = !!store;
465
+ const hasPersistentAdmin = store ? await store.hasActiveAdminAccount() : false;
466
+ return { dbEnabled, dbReady, store, hasPersistentAdmin };
467
+ }
468
+
469
+ private isPersistenceEnabled(): boolean {
470
+ return this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
485
471
  }
486
472
 
487
473
  private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
488
- if (this.opsServerRef.dcRouterRef.options.dbConfig?.enabled === false) {
474
+ if (!this.isPersistenceEnabled()) {
489
475
  return null;
490
476
  }
491
477
  const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
+ import { requireOpsAuth } from '../helpers/auth.js';
4
5
 
5
6
  export class ApiTokenHandler {
6
7
  constructor(private opsServerRef: OpsServer) {
@@ -17,6 +18,11 @@ export class ApiTokenHandler {
17
18
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
18
19
  'createApiToken',
19
20
  async (dataArg) => {
21
+ const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
22
+ scope: 'tokens:manage',
23
+ requireAdminIdentity: true,
24
+ requireAdminToken: true,
25
+ });
20
26
  const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
21
27
  if (!manager) {
22
28
  return { success: false, message: 'Token management not initialized' };
@@ -25,7 +31,7 @@ export class ApiTokenHandler {
25
31
  dataArg.name,
26
32
  dataArg.scopes,
27
33
  dataArg.expiresInDays ?? null,
28
- dataArg.identity.userId,
34
+ auth.userId,
29
35
  dataArg.policy,
30
36
  );
31
37
  return { success: true, tokenId: result.id, tokenValue: result.rawToken };
@@ -38,6 +44,11 @@ export class ApiTokenHandler {
38
44
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
39
45
  'listApiTokens',
40
46
  async (dataArg) => {
47
+ await requireOpsAuth(this.opsServerRef, dataArg, {
48
+ scope: 'tokens:read',
49
+ requireAdminIdentity: true,
50
+ requireAdminToken: true,
51
+ });
41
52
  const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
42
53
  if (!manager) {
43
54
  return { tokens: [] };
@@ -52,6 +63,11 @@ export class ApiTokenHandler {
52
63
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
53
64
  'revokeApiToken',
54
65
  async (dataArg) => {
66
+ await requireOpsAuth(this.opsServerRef, dataArg, {
67
+ scope: 'tokens:manage',
68
+ requireAdminIdentity: true,
69
+ requireAdminToken: true,
70
+ });
55
71
  const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
56
72
  if (!manager) {
57
73
  return { success: false, message: 'Token management not initialized' };
@@ -67,6 +83,11 @@ export class ApiTokenHandler {
67
83
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
68
84
  'rollApiToken',
69
85
  async (dataArg) => {
86
+ await requireOpsAuth(this.opsServerRef, dataArg, {
87
+ scope: 'tokens:manage',
88
+ requireAdminIdentity: true,
89
+ requireAdminToken: true,
90
+ });
70
91
  const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
71
92
  if (!manager) {
72
93
  return { success: false, message: 'Token management not initialized' };
@@ -85,6 +106,11 @@ export class ApiTokenHandler {
85
106
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
86
107
  'toggleApiToken',
87
108
  async (dataArg) => {
109
+ await requireOpsAuth(this.opsServerRef, dataArg, {
110
+ scope: 'tokens:manage',
111
+ requireAdminIdentity: true,
112
+ requireAdminToken: true,
113
+ });
88
114
  const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
89
115
  if (!manager) {
90
116
  return { success: false, message: 'Token management not initialized' };
@@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
4
  import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js';
5
5
  import { logger } from '../../logger.js';
6
+ import { requireOpsAuth } from '../helpers/auth.js';
6
7
 
7
8
  /**
8
9
  * Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from
@@ -37,29 +38,11 @@ export class CertificateHandler {
37
38
  request: { identity?: interfaces.data.IIdentity; apiToken?: string },
38
39
  requiredScope?: interfaces.data.TApiTokenScope,
39
40
  ): Promise<string> {
40
- if (request.identity?.jwt) {
41
- try {
42
- const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
43
- identity: request.identity,
44
- });
45
- if (isAdmin) return request.identity.userId;
46
- } catch { /* fall through */ }
47
- }
48
-
49
- if (request.apiToken) {
50
- const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
51
- if (tokenManager) {
52
- const token = await tokenManager.validateToken(request.apiToken);
53
- if (token) {
54
- if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
55
- return token.createdBy;
56
- }
57
- throw new plugins.typedrequest.TypedResponseError('insufficient scope');
58
- }
59
- }
60
- }
61
-
62
- throw new plugins.typedrequest.TypedResponseError('unauthorized');
41
+ const auth = await requireOpsAuth(this.opsServerRef, request, {
42
+ scope: requiredScope,
43
+ requireAdminIdentity: requiredScope?.endsWith(':write'),
44
+ });
45
+ return auth.userId;
63
46
  }
64
47
 
65
48
  private registerHandlers(): void {
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import * as paths from '../../paths.js';
3
3
  import type { OpsServer } from '../classes.opsserver.js';
4
4
  import * as interfaces from '../../../ts_interfaces/index.js';
5
+ import { requireOpsAuth } from '../helpers/auth.js';
5
6
 
6
7
  export class ConfigHandler {
7
8
  constructor(private opsServerRef: OpsServer) {
@@ -17,6 +18,7 @@ export class ConfigHandler {
17
18
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
18
19
  'getConfiguration',
19
20
  async (dataArg, toolsArg) => {
21
+ await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config:read' });
20
22
  const config = await this.getConfiguration();
21
23
  return {
22
24
  config,
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
+ import { requireOpsAuth } from '../helpers/auth.js';
4
5
 
5
6
  /**
6
7
  * CRUD + connection-test handlers for DnsProviderDoc.
@@ -20,29 +21,11 @@ export class DnsProviderHandler {
20
21
  request: { identity?: interfaces.data.IIdentity; apiToken?: string },
21
22
  requiredScope?: interfaces.data.TApiTokenScope,
22
23
  ): Promise<string> {
23
- if (request.identity?.jwt) {
24
- try {
25
- const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
26
- identity: request.identity,
27
- });
28
- if (isAdmin) return request.identity.userId;
29
- } catch { /* fall through */ }
30
- }
31
-
32
- if (request.apiToken) {
33
- const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
34
- if (tokenManager) {
35
- const token = await tokenManager.validateToken(request.apiToken);
36
- if (token) {
37
- if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
38
- return token.createdBy;
39
- }
40
- throw new plugins.typedrequest.TypedResponseError('insufficient scope');
41
- }
42
- }
43
- }
44
-
45
- throw new plugins.typedrequest.TypedResponseError('unauthorized');
24
+ const auth = await requireOpsAuth(this.opsServerRef, request, {
25
+ scope: requiredScope,
26
+ requireAdminIdentity: requiredScope?.endsWith(':write'),
27
+ });
28
+ return auth.userId;
46
29
  }
47
30
 
48
31
  private registerHandlers(): void {