@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +41 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/administration.d.ts +24 -2
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +126 -3
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts +31 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +70 -2
- package/dist/core/auth.js.map +1 -1
- package/dist/core/errors.d.ts +15 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/jwt.d.ts +2 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +22 -1
- package/dist/core/jwt.js.map +1 -1
- package/dist/db/admin-command.d.ts +12 -4
- package/dist/db/admin-command.d.ts.map +1 -1
- package/dist/db/admin-command.js +10 -1
- package/dist/db/admin-command.js.map +1 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +4 -1
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts +3 -0
- package/dist/db/admin-user-moderation.d.ts.map +1 -1
- package/dist/db/admin-user-moderation.js +6 -2
- package/dist/db/admin-user-moderation.js.map +1 -1
- package/dist/db/impersonation.d.ts +55 -0
- package/dist/db/impersonation.d.ts.map +1 -0
- package/dist/db/impersonation.js +212 -0
- package/dist/db/impersonation.js.map +1 -0
- package/dist/db/schema/admin-command-results.d.ts +16 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -1
- package/dist/db/schema/admin-command-results.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/index.d.ts +1 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +4 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +8 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.js +2 -2
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +26 -2
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +2 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +127 -19
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts +8 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +44 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/presentation/routes/session/me.d.ts.map +1 -1
- package/dist/presentation/routes/session/me.js +2 -1
- package/dist/presentation/routes/session/me.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/config.ts +4 -0
- package/src/core/administration.ts +172 -1
- package/src/core/auth.test.ts +79 -0
- package/src/core/auth.ts +98 -1
- package/src/core/errors.ts +35 -0
- package/src/core/jwt.test.ts +92 -0
- package/src/core/jwt.ts +38 -9
- package/src/db/admin-command.ts +35 -6
- package/src/db/admin-grants.ts +5 -1
- package/src/db/admin-user-moderation.ts +8 -2
- package/src/db/impersonation.ts +323 -0
- package/src/db/schema/admin-command-results.ts +18 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +12 -3
- package/src/metrics/index.ts +2 -0
- package/src/metrics/instance.ts +13 -2
- package/src/presentation/auth/bearer-token-auth.ts +4 -4
- package/src/presentation/auth/jwt-auth.test.ts +191 -0
- package/src/presentation/auth/jwt-auth.ts +19 -1
- package/src/presentation/routes/administration.test.ts +828 -3
- package/src/presentation/routes/administration.ts +130 -11
- package/src/presentation/routes/rate-limit.ts +40 -0
- package/src/presentation/routes/session/login.test.ts +5 -0
- package/src/presentation/routes/session/me.test.ts +24 -0
- package/src/presentation/routes/session/me.ts +1 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/test/routes.ts +43 -16
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AUTH_USER, adoptAdministrationActorGuard, requireAdministrationActor } from '@shipfox/api-auth-context';
|
|
3
|
+
import { adminBootstrapStateSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, impersonateResponseSchema, impersonateUserBodySchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
|
|
3
4
|
import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
|
|
4
5
|
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { requireAdminRole } from '#core/admin-role.js';
|
|
7
|
-
import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, listAdministratorGrantSummaries, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
|
|
8
|
-
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
8
|
+
import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, impersonateUser, listAdministratorGrantSummaries, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
|
|
9
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
9
10
|
import { getClientContext } from '#presentation/auth/jwt-auth.js';
|
|
10
|
-
import {
|
|
11
|
+
import { toUserDto } from '#presentation/dto/user.js';
|
|
12
|
+
import { createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
11
13
|
const idempotencyKeyMaxLength = 256;
|
|
12
14
|
function requireActorId(request) {
|
|
13
15
|
const client = getClientContext(request);
|
|
@@ -114,6 +116,40 @@ function translateAdministrationError(error) {
|
|
|
114
116
|
status: 409
|
|
115
117
|
});
|
|
116
118
|
}
|
|
119
|
+
if (error instanceof ImpersonationDisabledError) {
|
|
120
|
+
throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {
|
|
121
|
+
status: 403
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (error instanceof CannotImpersonateSelfError) {
|
|
125
|
+
throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {
|
|
126
|
+
status: 403
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (error instanceof CannotImpersonateAdministratorError) {
|
|
130
|
+
throw new ClientError('Cannot impersonate an administrator', 'cannot-impersonate-administrator', {
|
|
131
|
+
status: 403
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (error instanceof ImpersonationTargetNotActiveError) {
|
|
135
|
+
throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
|
|
136
|
+
status: 403
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// The mint primitive re-checks login eligibility (active, verified) on the
|
|
140
|
+
// row it reads, and a concurrent suspension or unverification between the
|
|
141
|
+
// in-transaction ladder read and that read surfaces these errors. Map them
|
|
142
|
+
// to the same documented client error instead of a generic 500.
|
|
143
|
+
if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
|
|
144
|
+
throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
|
|
145
|
+
status: 403
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (error instanceof ImpersonationExpiredError) {
|
|
149
|
+
throw new ClientError('Impersonation session has expired', 'impersonation-expired', {
|
|
150
|
+
status: 410
|
|
151
|
+
});
|
|
152
|
+
}
|
|
117
153
|
throw error;
|
|
118
154
|
}
|
|
119
155
|
const bootstrapRoute = defineRoute({
|
|
@@ -349,7 +385,7 @@ const revokeRoute = defineRoute({
|
|
|
349
385
|
return toAdminGrantDto(grant);
|
|
350
386
|
}
|
|
351
387
|
});
|
|
352
|
-
export const administrationRoutes = {
|
|
388
|
+
export const administrationRoutes = adoptAdministrationActorGuard({
|
|
353
389
|
prefix: '/admin/auth/admin-grants',
|
|
354
390
|
auth: AUTH_USER,
|
|
355
391
|
routes: [
|
|
@@ -358,23 +394,95 @@ export const administrationRoutes = {
|
|
|
358
394
|
grantRoute,
|
|
359
395
|
revokeRoute
|
|
360
396
|
]
|
|
361
|
-
};
|
|
362
|
-
export const administrationBootstrapRoutes = {
|
|
397
|
+
});
|
|
398
|
+
export const administrationBootstrapRoutes = adoptAdministrationActorGuard({
|
|
363
399
|
prefix: '/admin/auth',
|
|
364
400
|
auth: AUTH_USER,
|
|
365
401
|
routes: [
|
|
366
402
|
bootstrapStateRoute
|
|
367
403
|
]
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
404
|
+
});
|
|
405
|
+
function createImpersonateUserRoute(workspaces) {
|
|
406
|
+
return defineRoute({
|
|
407
|
+
method: 'POST',
|
|
408
|
+
path: '/:userId/impersonate',
|
|
409
|
+
description: 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',
|
|
410
|
+
schema: {
|
|
411
|
+
params: z.object({
|
|
412
|
+
userId: z.string().uuid()
|
|
413
|
+
}),
|
|
414
|
+
body: impersonateUserBodySchema,
|
|
415
|
+
response: {
|
|
416
|
+
200: impersonateResponseSchema
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
// The limiter runs ahead of the impersonated-session rejection: a request
|
|
420
|
+
// carrying an already-issued impersonated token must consume the
|
|
421
|
+
// `impersonate` IP and actor buckets instead of being rejected by the
|
|
422
|
+
// guard first, or marked-session probes would bypass the limiter entirely.
|
|
423
|
+
// The guard still runs before the authorization inside the command.
|
|
424
|
+
preHandler: [
|
|
425
|
+
createAuthActorRateLimitPreHandler('impersonate'),
|
|
426
|
+
(request)=>{
|
|
427
|
+
requireAdministrationActor(request);
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
errorHandler: translateAdministrationError,
|
|
432
|
+
handler: async (request)=>{
|
|
433
|
+
const client = getClientContext(request);
|
|
434
|
+
const result = await impersonateUser({
|
|
435
|
+
actorId: requireActorId(request),
|
|
436
|
+
...client?.impersonatorId ? {
|
|
437
|
+
actorImpersonatorId: client.impersonatorId
|
|
438
|
+
} : {},
|
|
439
|
+
targetUserId: request.params.userId,
|
|
440
|
+
reason: request.body.reason,
|
|
441
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
442
|
+
// Fastify's default request IDs are process-local counters that reset
|
|
443
|
+
// after a redeploy, so a replay could collide with an earlier mint's
|
|
444
|
+
// ID and suppress the required failure event in the ambiguous-COMMIT
|
|
445
|
+
// reconciliation. The correlation is a fresh process-independent UUID
|
|
446
|
+
// per invocation.
|
|
447
|
+
correlationId: randomUUID(),
|
|
448
|
+
workspaces
|
|
449
|
+
});
|
|
450
|
+
// `server_time` is the issuer's clock at response time: the banner
|
|
451
|
+
// derives its countdown and Extend availability from this anchor.
|
|
452
|
+
return {
|
|
453
|
+
token: result.token,
|
|
454
|
+
expires_at: result.expiresAt.toISOString(),
|
|
455
|
+
server_time: new Date().toISOString(),
|
|
456
|
+
impersonator_id: result.impersonatorId,
|
|
457
|
+
user: toUserDto(result.user)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
export function createAdministrationUserRoutes(workspaces) {
|
|
463
|
+
return [
|
|
464
|
+
adoptAdministrationActorGuard({
|
|
465
|
+
prefix: '/admin/auth/users',
|
|
466
|
+
auth: AUTH_USER,
|
|
467
|
+
routes: [
|
|
468
|
+
userLookupRoute,
|
|
469
|
+
suspendUserRoute,
|
|
470
|
+
reactivateUserRoute,
|
|
471
|
+
revokeUserSessionsRoute
|
|
472
|
+
]
|
|
473
|
+
}),
|
|
474
|
+
// The impersonate route mounts outside the adopted guard on purpose: its
|
|
475
|
+
// limiter must run before the impersonated-session rejection (see
|
|
476
|
+
// `createImpersonateUserRoute`), so the guard is applied positionally
|
|
477
|
+
// there. Every other route under `/admin/auth/users` keeps the adoption.
|
|
478
|
+
{
|
|
479
|
+
prefix: '/admin/auth/users',
|
|
480
|
+
auth: AUTH_USER,
|
|
481
|
+
routes: [
|
|
482
|
+
createImpersonateUserRoute(workspaces)
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
];
|
|
486
|
+
}
|
|
379
487
|
|
|
380
488
|
//# sourceMappingURL=administration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n listAdministratorGrantSummaries,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = {\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n};\n\nexport const administrationBootstrapRoutes: RouteGroup = {\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n};\n\nexport const administrationUserRoutes: RouteGroup = {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],\n};\n"],"names":["AUTH_USER","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","listAdministratorGrantSummaries","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","getClientContext","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","suspendUserRoute","params","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","administrationUserRoutes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {\n AUTH_USER,\n adoptAdministrationActorGuard,\n requireAdministrationActor,\n} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n impersonateResponseSchema,\n impersonateUserBodySchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n impersonateUser,\n listAdministratorGrantSummaries,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n EmailNotVerifiedError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\nimport {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n if (error instanceof ImpersonationDisabledError) {\n throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});\n }\n if (error instanceof CannotImpersonateSelfError) {\n throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {\n status: 403,\n });\n }\n if (error instanceof CannotImpersonateAdministratorError) {\n throw new ClientError(\n 'Cannot impersonate an administrator',\n 'cannot-impersonate-administrator',\n {status: 403},\n );\n }\n if (error instanceof ImpersonationTargetNotActiveError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n // The mint primitive re-checks login eligibility (active, verified) on the\n // row it reads, and a concurrent suspension or unverification between the\n // in-transaction ladder read and that read surfaces these errors. Map them\n // to the same documented client error instead of a generic 500.\n if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n if (error instanceof ImpersonationExpiredError) {\n throw new ClientError('Impersonation session has expired', 'impersonation-expired', {\n status: 410,\n });\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n});\n\nexport const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n});\n\nfunction createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {\n return defineRoute({\n method: 'POST',\n path: '/:userId/impersonate',\n description:\n 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: impersonateUserBodySchema,\n response: {200: impersonateResponseSchema},\n },\n // The limiter runs ahead of the impersonated-session rejection: a request\n // carrying an already-issued impersonated token must consume the\n // `impersonate` IP and actor buckets instead of being rejected by the\n // guard first, or marked-session probes would bypass the limiter entirely.\n // The guard still runs before the authorization inside the command.\n preHandler: [\n createAuthActorRateLimitPreHandler('impersonate'),\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const client = getClientContext(request);\n const result = await impersonateUser({\n actorId: requireActorId(request),\n ...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),\n targetUserId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n // Fastify's default request IDs are process-local counters that reset\n // after a redeploy, so a replay could collide with an earlier mint's\n // ID and suppress the required failure event in the ambiguous-COMMIT\n // reconciliation. The correlation is a fresh process-independent UUID\n // per invocation.\n correlationId: randomUUID(),\n workspaces,\n });\n // `server_time` is the issuer's clock at response time: the banner\n // derives its countdown and Extend availability from this anchor.\n return {\n token: result.token,\n expires_at: result.expiresAt.toISOString(),\n server_time: new Date().toISOString(),\n impersonator_id: result.impersonatorId,\n user: toUserDto(result.user),\n };\n },\n });\n}\n\nexport function createAdministrationUserRoutes(\n workspaces: WorkspacesInterModuleClient,\n): RouteGroup[] {\n return [\n adoptAdministrationActorGuard({\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],\n }),\n // The impersonate route mounts outside the adopted guard on purpose: its\n // limiter must run before the impersonated-session rejection (see\n // `createImpersonateUserRoute`), so the guard is applied positionally\n // there. Every other route under `/admin/auth/users` keeps the adoption.\n {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [createImpersonateUserRoute(workspaces)],\n },\n ];\n}\n"],"names":["randomUUID","AUTH_USER","adoptAdministrationActorGuard","requireAdministrationActor","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","impersonateResponseSchema","impersonateUserBodySchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","impersonateUser","listAdministratorGrantSummaries","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidCredentialsError","LastAdminOwnerError","UserNotFoundError","getClientContext","toUserDto","createAuthActorRateLimitPreHandler","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","suspendUserRoute","params","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","createImpersonateUserRoute","workspaces","impersonatorId","actorImpersonatorId","targetUserId","token","expires_at","expiresAt","server_time","Date","impersonator_id","createAdministrationUserRoutes"],"mappings":"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"}
|
|
@@ -7,5 +7,13 @@ export declare function createAuthRateLimitPreHandler(action: AuthRateLimitActio
|
|
|
7
7
|
Body: EmailBody;
|
|
8
8
|
}>, reply: FastifyReply) => Promise<void>;
|
|
9
9
|
export declare function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Rate limits an authenticated route by source IP and by actor. Every request
|
|
12
|
+
* consumes the IP bucket, which bounds aggregate source traffic; the actor
|
|
13
|
+
* bucket is an additional per-actor cap that prevents an actor from evading
|
|
14
|
+
* the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
|
|
15
|
+
* hook), so the user context is set.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
10
18
|
export {};
|
|
11
19
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAmC7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,mBAAmB,aACrD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAmB3E"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getUserContext } from '@shipfox/api-auth-context';
|
|
1
2
|
import { ClientError } from '@shipfox/node-fastify';
|
|
2
3
|
import { enforceRateLimit as enforceSharedRateLimit } from '@shipfox/node-rate-limit';
|
|
3
4
|
import { checkAuthRateLimit } from '#core/rate-limit.js';
|
|
@@ -39,6 +40,22 @@ const policies = {
|
|
|
39
40
|
limit: 60,
|
|
40
41
|
windowSeconds: 5 * 60
|
|
41
42
|
}
|
|
43
|
+
},
|
|
44
|
+
impersonate: {
|
|
45
|
+
// Bounds mint and probing attempts: the IP bucket bounds aggregate source
|
|
46
|
+
// traffic, and the actor bucket is a per-actor cap that prevents an actor
|
|
47
|
+
// from evading the bound by rotating IPs (every request still consumes the
|
|
48
|
+
// IP bucket first, so a shared NAT cannot exhaust it faster by pooling
|
|
49
|
+
// actors). Ladder denials by an identifiable actor role are audited as
|
|
50
|
+
// `failed` events.
|
|
51
|
+
ip: {
|
|
52
|
+
limit: 20,
|
|
53
|
+
windowSeconds: 15 * 60
|
|
54
|
+
},
|
|
55
|
+
actor: {
|
|
56
|
+
limit: 20,
|
|
57
|
+
windowSeconds: 15 * 60
|
|
58
|
+
}
|
|
42
59
|
}
|
|
43
60
|
};
|
|
44
61
|
function routeName(request) {
|
|
@@ -107,5 +124,32 @@ export function createAuthIpRateLimitPreHandler(action) {
|
|
|
107
124
|
});
|
|
108
125
|
};
|
|
109
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Rate limits an authenticated route by source IP and by actor. Every request
|
|
129
|
+
* consumes the IP bucket, which bounds aggregate source traffic; the actor
|
|
130
|
+
* bucket is an additional per-actor cap that prevents an actor from evading
|
|
131
|
+
* the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
|
|
132
|
+
* hook), so the user context is set.
|
|
133
|
+
*/ export function createAuthActorRateLimitPreHandler(action) {
|
|
134
|
+
return async (request, reply)=>{
|
|
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
|
+
}
|
|
110
154
|
|
|
111
155
|
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n"],"names":["ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"AAAA,SAAQA,WAAW,QAA+C,wBAAwB;AAC1F,SAAQC,oBAAoBC,sBAAsB,QAAO,2BAA2B;AACpF,SAIEC,kBAAkB,QACb,sBAAsB;AAE7B,MAAMC,WAGF;IACFC,OAAO;QACLC,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;QACrCC,OAAO;YAACF,OAAO;YAAIC,eAAe,KAAK;QAAE;IAC3C;IACA,cAAc;QACZF,IAAI;YAACC,OAAO;YAAIC,eAAe,KAAK;QAAE;QACtCC,OAAO;YAACF,OAAO;YAAGC,eAAe,KAAK;QAAE;IAC1C;IACAE,WAAW;QACTJ,IAAI;YAACC,OAAO;YAAGC,eAAe,KAAK;QAAE;IACvC;IACA,mBAAmB;QACjBF,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;IACAG,QAAQ;QACNL,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;AACF;AAMA,
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","impersonate","actor","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/session/me.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/session/me.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,OAAO,iDA8BlB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/presentation/routes/session/me.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {meResponseSchema} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {getCurrentAdminRole} from '#core/admin-role.js';\nimport {getCurrentUser} from '#core/auth.js';\nimport {UserNotFoundError} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\n\nexport const meRoute = defineRoute({\n method: 'GET',\n path: '/me',\n description: 'Get the signed-in user.',\n auth: AUTH_USER,\n schema: {\n response: {\n 200: meResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n throw error;\n },\n handler: async (request) => {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const result = await getCurrentUser({userId: client.userId});\n\n return {\n user: toUserDto(result.user),\n admin_role: await getCurrentAdminRole({userId: client.userId}),\n };\n },\n});\n"],"names":["AUTH_USER","meResponseSchema","ClientError","defineRoute","getCurrentAdminRole","getCurrentUser","UserNotFoundError","getClientContext","toUserDto","meRoute","method","path","description","auth","schema","response","errorHandler","error","status","handler","request","client","result","userId","user","admin_role"],"mappings":"AAAA,SAAQA,SAAS,QAAO,4BAA4B;AACpD,SAAQC,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,mBAAmB,QAAO,sBAAsB;AACxD,SAAQC,cAAc,QAAO,gBAAgB;AAC7C,SAAQC,iBAAiB,QAAO,kBAAkB;AAClD,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AAEpD,OAAO,MAAMC,UAAUN,YAAY;IACjCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMb;IACNc,QAAQ;QACNC,UAAU;YACR,KAAKd;QACP;IACF;IACAe,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,kBAAkB,aAAa;gBAACgB,QAAQ;YAAG;QACnE;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC;QACd,MAAMC,SAASd,iBAAiBa;QAChC,IAAI,CAACC,QAAQ;YACX,MAAM,IAAInB,YAAY,2BAA2B,gBAAgB;gBAACgB,QAAQ;YAAG;QAC/E;QAEA,MAAMI,SAAS,MAAMjB,eAAe;YAACkB,QAAQF,OAAOE,MAAM;QAAA;QAE1D,OAAO;YACLC,MAAMhB,UAAUc,OAAOE,IAAI;YAC3BC,YAAY,MAAMrB,oBAAoB;gBAACmB,QAAQF,OAAOE,MAAM;YAAA;
|
|
1
|
+
{"version":3,"sources":["../../../../src/presentation/routes/session/me.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {meResponseSchema} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {getCurrentAdminRole} from '#core/admin-role.js';\nimport {getCurrentUser} from '#core/auth.js';\nimport {UserNotFoundError} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\n\nexport const meRoute = defineRoute({\n method: 'GET',\n path: '/me',\n description: 'Get the signed-in user.',\n auth: AUTH_USER,\n schema: {\n response: {\n 200: meResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n throw error;\n },\n handler: async (request) => {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const result = await getCurrentUser({userId: client.userId});\n\n return {\n user: toUserDto(result.user),\n admin_role: await getCurrentAdminRole({userId: client.userId}),\n impersonator_id: client.impersonatorId,\n };\n },\n});\n"],"names":["AUTH_USER","meResponseSchema","ClientError","defineRoute","getCurrentAdminRole","getCurrentUser","UserNotFoundError","getClientContext","toUserDto","meRoute","method","path","description","auth","schema","response","errorHandler","error","status","handler","request","client","result","userId","user","admin_role","impersonator_id","impersonatorId"],"mappings":"AAAA,SAAQA,SAAS,QAAO,4BAA4B;AACpD,SAAQC,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,mBAAmB,QAAO,sBAAsB;AACxD,SAAQC,cAAc,QAAO,gBAAgB;AAC7C,SAAQC,iBAAiB,QAAO,kBAAkB;AAClD,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AAEpD,OAAO,MAAMC,UAAUN,YAAY;IACjCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMb;IACNc,QAAQ;QACNC,UAAU;YACR,KAAKd;QACP;IACF;IACAe,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,kBAAkB,aAAa;gBAACgB,QAAQ;YAAG;QACnE;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC;QACd,MAAMC,SAASd,iBAAiBa;QAChC,IAAI,CAACC,QAAQ;YACX,MAAM,IAAInB,YAAY,2BAA2B,gBAAgB;gBAACgB,QAAQ;YAAG;QAC/E;QAEA,MAAMI,SAAS,MAAMjB,eAAe;YAACkB,QAAQF,OAAOE,MAAM;QAAA;QAE1D,OAAO;YACLC,MAAMhB,UAAUc,OAAOE,IAAI;YAC3BC,YAAY,MAAMrB,oBAAoB;gBAACmB,QAAQF,OAAOE,MAAM;YAAA;YAC5DG,iBAAiBL,OAAOM,cAAc;QACxC;IACF;AACF,GAAG"}
|