@shipfox/api-auth 15.0.0 → 17.0.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.
Files changed (101) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +41 -0
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +4 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/administration.d.ts +24 -2
  9. package/dist/core/administration.d.ts.map +1 -1
  10. package/dist/core/administration.js +126 -3
  11. package/dist/core/administration.js.map +1 -1
  12. package/dist/core/auth.d.ts +31 -0
  13. package/dist/core/auth.d.ts.map +1 -1
  14. package/dist/core/auth.js +70 -2
  15. package/dist/core/auth.js.map +1 -1
  16. package/dist/core/errors.d.ts +15 -0
  17. package/dist/core/errors.d.ts.map +1 -1
  18. package/dist/core/errors.js +30 -0
  19. package/dist/core/errors.js.map +1 -1
  20. package/dist/core/jwt.d.ts +2 -0
  21. package/dist/core/jwt.d.ts.map +1 -1
  22. package/dist/core/jwt.js +22 -1
  23. package/dist/core/jwt.js.map +1 -1
  24. package/dist/db/admin-command.d.ts +12 -4
  25. package/dist/db/admin-command.d.ts.map +1 -1
  26. package/dist/db/admin-command.js +10 -1
  27. package/dist/db/admin-command.js.map +1 -1
  28. package/dist/db/admin-grants.d.ts.map +1 -1
  29. package/dist/db/admin-grants.js +4 -1
  30. package/dist/db/admin-grants.js.map +1 -1
  31. package/dist/db/admin-user-moderation.d.ts +3 -0
  32. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  33. package/dist/db/admin-user-moderation.js +6 -2
  34. package/dist/db/admin-user-moderation.js.map +1 -1
  35. package/dist/db/impersonation.d.ts +55 -0
  36. package/dist/db/impersonation.d.ts.map +1 -0
  37. package/dist/db/impersonation.js +212 -0
  38. package/dist/db/impersonation.js.map +1 -0
  39. package/dist/db/schema/admin-command-results.d.ts +16 -0
  40. package/dist/db/schema/admin-command-results.d.ts.map +1 -1
  41. package/dist/db/schema/admin-command-results.js.map +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/metrics/index.d.ts +1 -1
  47. package/dist/metrics/index.d.ts.map +1 -1
  48. package/dist/metrics/index.js +1 -1
  49. package/dist/metrics/index.js.map +1 -1
  50. package/dist/metrics/instance.d.ts +4 -2
  51. package/dist/metrics/instance.d.ts.map +1 -1
  52. package/dist/metrics/instance.js +8 -0
  53. package/dist/metrics/instance.js.map +1 -1
  54. package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
  55. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
  56. package/dist/presentation/auth/bearer-token-auth.js +2 -2
  57. package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
  58. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  59. package/dist/presentation/auth/jwt-auth.js +26 -2
  60. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  61. package/dist/presentation/routes/administration.d.ts +2 -1
  62. package/dist/presentation/routes/administration.d.ts.map +1 -1
  63. package/dist/presentation/routes/administration.js +127 -19
  64. package/dist/presentation/routes/administration.js.map +1 -1
  65. package/dist/presentation/routes/rate-limit.d.ts +8 -0
  66. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  67. package/dist/presentation/routes/rate-limit.js +44 -0
  68. package/dist/presentation/routes/rate-limit.js.map +1 -1
  69. package/dist/presentation/routes/session/me.d.ts.map +1 -1
  70. package/dist/presentation/routes/session/me.js +2 -1
  71. package/dist/presentation/routes/session/me.js.map +1 -1
  72. package/dist/tsconfig.test.tsbuildinfo +1 -1
  73. package/package.json +5 -5
  74. package/src/config.ts +4 -0
  75. package/src/core/administration.ts +172 -1
  76. package/src/core/auth.test.ts +79 -0
  77. package/src/core/auth.ts +98 -1
  78. package/src/core/errors.ts +35 -0
  79. package/src/core/jwt.test.ts +92 -0
  80. package/src/core/jwt.ts +38 -9
  81. package/src/db/admin-command.ts +35 -6
  82. package/src/db/admin-grants.ts +5 -1
  83. package/src/db/admin-user-moderation.ts +8 -2
  84. package/src/db/impersonation.ts +323 -0
  85. package/src/db/schema/admin-command-results.ts +18 -0
  86. package/src/index.test.ts +1 -1
  87. package/src/index.ts +12 -3
  88. package/src/metrics/index.ts +2 -0
  89. package/src/metrics/instance.ts +13 -2
  90. package/src/presentation/auth/bearer-token-auth.ts +4 -4
  91. package/src/presentation/auth/jwt-auth.test.ts +191 -0
  92. package/src/presentation/auth/jwt-auth.ts +19 -1
  93. package/src/presentation/routes/administration.test.ts +828 -3
  94. package/src/presentation/routes/administration.ts +130 -11
  95. package/src/presentation/routes/rate-limit.ts +40 -0
  96. package/src/presentation/routes/session/login.test.ts +5 -0
  97. package/src/presentation/routes/session/me.test.ts +24 -0
  98. package/src/presentation/routes/session/me.ts +1 -0
  99. package/src/presentation/routes/session/refresh.test.ts +11 -0
  100. package/test/routes.ts +43 -16
  101. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,4 +1,9 @@
1
- import {AUTH_USER} from '@shipfox/api-auth-context';
1
+ import {randomUUID} from 'node:crypto';
2
+ import {
3
+ AUTH_USER,
4
+ adoptAdministrationActorGuard,
5
+ requireAdministrationActor,
6
+ } from '@shipfox/api-auth-context';
2
7
  import {
3
8
  adminBootstrapStateSchema,
4
9
  administratorUserLookupQuerySchema,
@@ -8,6 +13,8 @@ import {
8
13
  bootstrapAdminOwnerResponseSchema,
9
14
  grantAdminRoleBodySchema,
10
15
  grantAdminRoleResponseSchema,
16
+ impersonateResponseSchema,
17
+ impersonateUserBodySchema,
11
18
  listAdminGrantsQuerySchema,
12
19
  listAdminGrantsResponseSchema,
13
20
  reactivateAdministratorUserBodySchema,
@@ -17,6 +24,7 @@ import {
17
24
  revokeAdministratorUserSessionsResponseSchema,
18
25
  suspendAdministratorUserBodySchema,
19
26
  } from '@shipfox/api-auth-dto';
27
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
20
28
  import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
21
29
  import {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';
22
30
  import type {FastifyRequest} from 'fastify';
@@ -27,6 +35,7 @@ import {
27
35
  findAdministratorUserSummary,
28
36
  getAdminBootstrapState,
29
37
  grantAdministratorRole,
38
+ impersonateUser,
30
39
  listAdministratorGrantSummaries,
31
40
  reactivateAdministratorUser,
32
41
  revokeAdministratorGrant,
@@ -44,12 +53,20 @@ import {
44
53
  AdminGrantNotFoundError,
45
54
  AdminIdempotencyKeyReuseError,
46
55
  AdminRoleRequiredError,
56
+ CannotImpersonateAdministratorError,
57
+ CannotImpersonateSelfError,
58
+ EmailNotVerifiedError,
59
+ ImpersonationDisabledError,
60
+ ImpersonationExpiredError,
61
+ ImpersonationTargetNotActiveError,
47
62
  InvalidAdminBootstrapTokenError,
63
+ InvalidCredentialsError,
48
64
  LastAdminOwnerError,
49
65
  UserNotFoundError,
50
66
  } from '#core/errors.js';
51
67
  import {getClientContext} from '#presentation/auth/jwt-auth.js';
52
- import {createAuthIpRateLimitPreHandler} from './rate-limit.js';
68
+ import {toUserDto} from '#presentation/dto/user.js';
69
+ import {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';
53
70
 
54
71
  const idempotencyKeyMaxLength = 256;
55
72
 
@@ -159,6 +176,40 @@ function translateAdministrationError(error: unknown): never {
159
176
  {status: 409},
160
177
  );
161
178
  }
179
+ if (error instanceof ImpersonationDisabledError) {
180
+ throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});
181
+ }
182
+ if (error instanceof CannotImpersonateSelfError) {
183
+ throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {
184
+ status: 403,
185
+ });
186
+ }
187
+ if (error instanceof CannotImpersonateAdministratorError) {
188
+ throw new ClientError(
189
+ 'Cannot impersonate an administrator',
190
+ 'cannot-impersonate-administrator',
191
+ {status: 403},
192
+ );
193
+ }
194
+ if (error instanceof ImpersonationTargetNotActiveError) {
195
+ throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
196
+ status: 403,
197
+ });
198
+ }
199
+ // The mint primitive re-checks login eligibility (active, verified) on the
200
+ // row it reads, and a concurrent suspension or unverification between the
201
+ // in-transaction ladder read and that read surfaces these errors. Map them
202
+ // to the same documented client error instead of a generic 500.
203
+ if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
204
+ throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
205
+ status: 403,
206
+ });
207
+ }
208
+ if (error instanceof ImpersonationExpiredError) {
209
+ throw new ClientError('Impersonation session has expired', 'impersonation-expired', {
210
+ status: 410,
211
+ });
212
+ }
162
213
  throw error;
163
214
  }
164
215
 
@@ -362,20 +413,88 @@ const revokeRoute = defineRoute({
362
413
  },
363
414
  });
364
415
 
365
- export const administrationRoutes: RouteGroup = {
416
+ export const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({
366
417
  prefix: '/admin/auth/admin-grants',
367
418
  auth: AUTH_USER,
368
419
  routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],
369
- };
420
+ });
370
421
 
371
- export const administrationBootstrapRoutes: RouteGroup = {
422
+ export const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({
372
423
  prefix: '/admin/auth',
373
424
  auth: AUTH_USER,
374
425
  routes: [bootstrapStateRoute],
375
- };
426
+ });
376
427
 
377
- export const administrationUserRoutes: RouteGroup = {
378
- prefix: '/admin/auth/users',
379
- auth: AUTH_USER,
380
- routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],
381
- };
428
+ function createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {
429
+ return defineRoute({
430
+ method: 'POST',
431
+ path: '/:userId/impersonate',
432
+ description:
433
+ 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',
434
+ schema: {
435
+ params: z.object({userId: z.string().uuid()}),
436
+ body: impersonateUserBodySchema,
437
+ response: {200: impersonateResponseSchema},
438
+ },
439
+ // The limiter runs ahead of the impersonated-session rejection: a request
440
+ // carrying an already-issued impersonated token must consume the
441
+ // `impersonate` IP and actor buckets instead of being rejected by the
442
+ // guard first, or marked-session probes would bypass the limiter entirely.
443
+ // The guard still runs before the authorization inside the command.
444
+ preHandler: [
445
+ createAuthActorRateLimitPreHandler('impersonate'),
446
+ (request) => {
447
+ requireAdministrationActor(request);
448
+ return undefined;
449
+ },
450
+ ],
451
+ errorHandler: translateAdministrationError,
452
+ handler: async (request) => {
453
+ const client = getClientContext(request);
454
+ const result = await impersonateUser({
455
+ actorId: requireActorId(request),
456
+ ...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),
457
+ targetUserId: request.params.userId,
458
+ reason: request.body.reason,
459
+ idempotencyKey: requireIdempotencyKey(request),
460
+ // Fastify's default request IDs are process-local counters that reset
461
+ // after a redeploy, so a replay could collide with an earlier mint's
462
+ // ID and suppress the required failure event in the ambiguous-COMMIT
463
+ // reconciliation. The correlation is a fresh process-independent UUID
464
+ // per invocation.
465
+ correlationId: randomUUID(),
466
+ workspaces,
467
+ });
468
+ // `server_time` is the issuer's clock at response time: the banner
469
+ // derives its countdown and Extend availability from this anchor.
470
+ return {
471
+ token: result.token,
472
+ expires_at: result.expiresAt.toISOString(),
473
+ server_time: new Date().toISOString(),
474
+ impersonator_id: result.impersonatorId,
475
+ user: toUserDto(result.user),
476
+ };
477
+ },
478
+ });
479
+ }
480
+
481
+ export function createAdministrationUserRoutes(
482
+ workspaces: WorkspacesInterModuleClient,
483
+ ): RouteGroup[] {
484
+ return [
485
+ adoptAdministrationActorGuard({
486
+ prefix: '/admin/auth/users',
487
+ auth: AUTH_USER,
488
+ routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],
489
+ }),
490
+ // The impersonate route mounts outside the adopted guard on purpose: its
491
+ // limiter must run before the impersonated-session rejection (see
492
+ // `createImpersonateUserRoute`), so the guard is applied positionally
493
+ // there. Every other route under `/admin/auth/users` keeps the adoption.
494
+ {
495
+ prefix: '/admin/auth/users',
496
+ auth: AUTH_USER,
497
+ routes: [createImpersonateUserRoute(workspaces)],
498
+ },
499
+ ];
500
+ }
@@ -1,3 +1,4 @@
1
+ import {getUserContext} from '@shipfox/api-auth-context';
1
2
  import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';
2
3
  import {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';
3
4
  import {
@@ -28,6 +29,16 @@ const policies: Record<
28
29
  lookup: {
29
30
  ip: {limit: 60, windowSeconds: 5 * 60},
30
31
  },
32
+ impersonate: {
33
+ // Bounds mint and probing attempts: the IP bucket bounds aggregate source
34
+ // traffic, and the actor bucket is a per-actor cap that prevents an actor
35
+ // from evading the bound by rotating IPs (every request still consumes the
36
+ // IP bucket first, so a shared NAT cannot exhaust it faster by pooling
37
+ // actors). Ladder denials by an identifiable actor role are audited as
38
+ // `failed` events.
39
+ ip: {limit: 20, windowSeconds: 15 * 60},
40
+ actor: {limit: 20, windowSeconds: 15 * 60},
41
+ },
31
42
  };
32
43
 
33
44
  interface EmailBody {
@@ -111,3 +122,32 @@ export function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {
111
122
  });
112
123
  };
113
124
  }
125
+
126
+ /**
127
+ * Rate limits an authenticated route by source IP and by actor. Every request
128
+ * consumes the IP bucket, which bounds aggregate source traffic; the actor
129
+ * bucket is an additional per-actor cap that prevents an actor from evading
130
+ * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
131
+ * hook), so the user context is set.
132
+ */
133
+ export function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {
134
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
135
+ await enforceRateLimit({
136
+ request,
137
+ reply,
138
+ action,
139
+ scope: 'ip',
140
+ identifier: request.ip,
141
+ });
142
+ const client = getUserContext(request);
143
+ if (client) {
144
+ await enforceRateLimit({
145
+ request,
146
+ reply,
147
+ action,
148
+ scope: 'actor',
149
+ identifier: client.userId,
150
+ });
151
+ }
152
+ };
153
+ }
@@ -1,8 +1,10 @@
1
1
  import type {FastifyInstance} from 'fastify';
2
+ import {verifyUserToken} from '#core/jwt.js';
2
3
  import {
3
4
  createAuthTestApp,
4
5
  listMembershipsByUserMock,
5
6
  login,
7
+ ROUTE_TEST_SECRET,
6
8
  resetCapturedMail,
7
9
  signup,
8
10
  uniqueEmail,
@@ -36,6 +38,9 @@ describe('POST /auth/login', () => {
36
38
  expect(res.json().token).toBeDefined();
37
39
  expect(res.json().user.email).toBe(email);
38
40
  expect(res.json().admin_role).toBeNull();
41
+ expect(res.json().impersonator_id).toBeUndefined();
42
+ const claims = await verifyUserToken({token: res.json().token, secret: ROUTE_TEST_SECRET});
43
+ expect(claims.impersonatorId).toBeUndefined();
39
44
  expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
40
45
  expect(res.headers['set-cookie']).toContain('HttpOnly');
41
46
  expect(res.headers['set-cookie']).toContain('Secure');
@@ -35,6 +35,30 @@ describe('GET /auth/me', () => {
35
35
  expect(res.statusCode).toBe(200);
36
36
  expect(res.json().user.email).toBe(account.email);
37
37
  expect(res.json().admin_role).toBeNull();
38
+ expect(res.json().impersonator_id).toBeUndefined();
39
+ });
40
+
41
+ test('returns impersonator_id for a marked session token', async () => {
42
+ const account = await createVerifiedSession('me-impersonated');
43
+ const impersonatorId = crypto.randomUUID();
44
+ const token = await signUserToken({
45
+ userId: account.userId,
46
+ impersonatorId,
47
+ email: account.email,
48
+ memberships: [],
49
+ secret: ROUTE_TEST_SECRET,
50
+ expiresIn: '15m',
51
+ });
52
+
53
+ const res = await app.inject({
54
+ method: 'GET',
55
+ url: '/auth/me',
56
+ headers: {authorization: `Bearer ${token}`},
57
+ });
58
+
59
+ expect(res.statusCode).toBe(200);
60
+ expect(res.json().user.email).toBe(account.email);
61
+ expect(res.json().impersonator_id).toBe(impersonatorId);
38
62
  });
39
63
 
40
64
  test('returns the current Auth-owned role for dashboard presentation', async () => {
@@ -34,6 +34,7 @@ export const meRoute = defineRoute({
34
34
  return {
35
35
  user: toUserDto(result.user),
36
36
  admin_role: await getCurrentAdminRole({userId: client.userId}),
37
+ impersonator_id: client.impersonatorId,
37
38
  };
38
39
  },
39
40
  });
@@ -1,8 +1,10 @@
1
1
  import type {FastifyInstance} from 'fastify';
2
+ import {verifyUserToken} from '#core/jwt.js';
2
3
  import {
3
4
  cookieHeader,
4
5
  createAuthTestApp,
5
6
  listMembershipsByUserMock,
7
+ ROUTE_TEST_SECRET,
6
8
  resetCapturedMail,
7
9
  signupVerifyLogin,
8
10
  } from '#test/routes.js';
@@ -34,6 +36,15 @@ describe('POST /auth/refresh', () => {
34
36
  expect(res.statusCode).toBe(200);
35
37
  expect(res.json().token).toBeDefined();
36
38
  expect(res.json().user.email).toBe(account.email);
39
+ // The refresh contract stays unmarked: the response never exposes
40
+ // impersonator_id and rotation re-issues an ordinary access token.
41
+ // refreshAccessToken signs from (user, memberships, sessionId) and never
42
+ // sees the access token, so a marker cannot survive rotation here (ADR 0014
43
+ // keeps impersonated sessions access-token-only; refresh restores the
44
+ // administrator's session).
45
+ expect(res.json().impersonator_id).toBeUndefined();
46
+ const claims = await verifyUserToken({token: res.json().token, secret: ROUTE_TEST_SECRET});
47
+ expect(claims.impersonatorId).toBeUndefined();
37
48
  expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
38
49
  expect(res.headers['set-cookie']).toContain('HttpOnly');
39
50
  expect(res.headers['set-cookie']).toContain('Secure');
package/test/routes.ts CHANGED
@@ -11,7 +11,7 @@ import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
11
11
  import {
12
12
  administrationBootstrapRoutes,
13
13
  administrationRoutes,
14
- administrationUserRoutes,
14
+ createAdministrationUserRoutes,
15
15
  } from '#presentation/routes/administration.js';
16
16
  import {buildAuthRoutes} from '#presentation/routes/index.js';
17
17
 
@@ -21,6 +21,20 @@ const testConfig = vi.hoisted(
21
21
  challenges: Map<string, string>;
22
22
  mailer: Mailer;
23
23
  clientBaseUrl: string;
24
+ authConfig: {
25
+ ADMIN_BOOTSTRAP_TOKEN: string;
26
+ AUTH_JWT_EXPIRES_IN: string;
27
+ AUTH_IMPERSONATION_ENABLED: boolean;
28
+ AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: number;
29
+ AUTH_REFRESH_ROTATION_GRACE_SECONDS: number;
30
+ AUTH_REFRESH_COOKIE_NAME: string;
31
+ AUTH_PASSWORD_ENABLED: boolean;
32
+ AUTH_SIGNUP_GATE_ENABLED: boolean;
33
+ AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: string;
34
+ AUTH_SIGNUP_ALLOWED_EMAILS: string;
35
+ AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: string | undefined;
36
+ CLIENT_BASE_URL: string;
37
+ };
24
38
  } => {
25
39
  const captured: MailMessage[] = [];
26
40
  const mailer: Mailer = {
@@ -29,11 +43,26 @@ const testConfig = vi.hoisted(
29
43
  return Promise.resolve();
30
44
  },
31
45
  };
46
+ const authConfig = {
47
+ ADMIN_BOOTSTRAP_TOKEN: 'test-bootstrap-token',
48
+ AUTH_JWT_EXPIRES_IN: '15m',
49
+ AUTH_IMPERSONATION_ENABLED: true,
50
+ AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
51
+ AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
52
+ AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
53
+ AUTH_PASSWORD_ENABLED: true,
54
+ AUTH_SIGNUP_GATE_ENABLED: false,
55
+ AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: '',
56
+ AUTH_SIGNUP_ALLOWED_EMAILS: '',
57
+ AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: undefined,
58
+ CLIENT_BASE_URL: 'https://app.example.test',
59
+ };
32
60
  return {
33
61
  captured,
34
62
  challenges: new Map(),
35
63
  mailer,
36
- clientBaseUrl: 'https://app.example.test',
64
+ clientBaseUrl: authConfig.CLIENT_BASE_URL,
65
+ authConfig,
37
66
  };
38
67
  },
39
68
  );
@@ -52,19 +81,7 @@ const workspaceTestDoubles = vi.hoisted(() => {
52
81
  const workspaces = workspaceTestDoubles as unknown as WorkspacesInterModuleClient;
53
82
 
54
83
  vi.mock('#config.js', () => ({
55
- config: {
56
- ADMIN_BOOTSTRAP_TOKEN: 'test-bootstrap-token',
57
- AUTH_JWT_EXPIRES_IN: '15m',
58
- AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
59
- AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
60
- AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
61
- AUTH_PASSWORD_ENABLED: true,
62
- AUTH_SIGNUP_GATE_ENABLED: false,
63
- AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: '',
64
- AUTH_SIGNUP_ALLOWED_EMAILS: '',
65
- AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: undefined,
66
- CLIENT_BASE_URL: testConfig.clientBaseUrl,
67
- },
84
+ config: testConfig.authConfig,
68
85
  mailer: testConfig.mailer,
69
86
  }));
70
87
 
@@ -92,6 +109,16 @@ export function resetCapturedMail(): void {
92
109
  listMembershipsByUserMock.mockResolvedValue({memberships: []});
93
110
  }
94
111
 
112
+ /** Flips `AUTH_IMPERSONATION_ENABLED` for the shared route-test config mock. */
113
+ export function setImpersonationEnabled(enabled: boolean): void {
114
+ testConfig.authConfig.AUTH_IMPERSONATION_ENABLED = enabled;
115
+ }
116
+
117
+ /** Flips `AUTH_JWT_EXPIRES_IN` for the shared route-test config mock. */
118
+ export function setAuthJwtExpiresIn(expiresIn: string): void {
119
+ testConfig.authConfig.AUTH_JWT_EXPIRES_IN = expiresIn;
120
+ }
121
+
95
122
  export function capturedMail(): MailMessage[] {
96
123
  return testConfig.captured;
97
124
  }
@@ -162,7 +189,7 @@ export async function createAuthTestApp(params?: {
162
189
  buildAuthRoutes(true, workspaces),
163
190
  administrationBootstrapRoutes,
164
191
  administrationRoutes,
165
- administrationUserRoutes,
192
+ ...createAdministrationUserRoutes(workspaces),
166
193
  ],
167
194
  swagger: false,
168
195
  };