@open-mercato/enterprise 0.6.4-develop.4371.1.8f3030407e → 0.6.4

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 (79) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +1 -1
  3. package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +2 -2
  4. package/dist/modules/security/api/enforcement/[id]/route.js +35 -1
  5. package/dist/modules/security/api/enforcement/[id]/route.js.map +2 -2
  6. package/dist/modules/security/api/enforcement/_shared.js +63 -1
  7. package/dist/modules/security/api/enforcement/_shared.js.map +3 -3
  8. package/dist/modules/security/api/enforcement/compliance/route.js +12 -3
  9. package/dist/modules/security/api/enforcement/compliance/route.js.map +2 -2
  10. package/dist/modules/security/api/enforcement/route.js +25 -2
  11. package/dist/modules/security/api/enforcement/route.js.map +2 -2
  12. package/dist/modules/security/api/mfa/prepare/route.js +1 -1
  13. package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
  14. package/dist/modules/security/api/mfa/recovery/route.js +1 -1
  15. package/dist/modules/security/api/mfa/recovery/route.js.map +2 -2
  16. package/dist/modules/security/api/mfa/verify/route.js +1 -1
  17. package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
  18. package/dist/modules/security/api/users/[id]/mfa/reset/route.js +6 -1
  19. package/dist/modules/security/api/users/[id]/mfa/reset/route.js.map +2 -2
  20. package/dist/modules/security/api/users/[id]/mfa/status/route.js +13 -2
  21. package/dist/modules/security/api/users/[id]/mfa/status/route.js.map +2 -2
  22. package/dist/modules/security/api/users/_shared.js +56 -1
  23. package/dist/modules/security/api/users/_shared.js.map +2 -2
  24. package/dist/modules/security/api/users/mfa/compliance/route.js +17 -7
  25. package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
  26. package/dist/modules/security/commands/createEnforcementPolicy.js +6 -1
  27. package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
  28. package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -1
  29. package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
  30. package/dist/modules/security/commands/resetUserMfa.js +6 -1
  31. package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
  32. package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -1
  33. package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
  34. package/dist/modules/security/services/MfaAdminService.js +22 -5
  35. package/dist/modules/security/services/MfaAdminService.js.map +2 -2
  36. package/dist/modules/security/services/MfaEnforcementService.js +28 -6
  37. package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
  38. package/dist/modules/security/services/MfaVerificationService.js +30 -10
  39. package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
  40. package/dist/modules/security/services/SudoChallengeService.js +14 -3
  41. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  42. package/dist/modules/sso/api/callback/oidc/route.js +2 -2
  43. package/dist/modules/sso/api/callback/oidc/route.js.map +2 -2
  44. package/dist/modules/sso/i18n/de.json +2 -0
  45. package/dist/modules/sso/i18n/en.json +2 -0
  46. package/dist/modules/sso/i18n/es.json +2 -0
  47. package/dist/modules/sso/i18n/pl.json +2 -0
  48. package/dist/modules/sso/lib/errors.js +21 -0
  49. package/dist/modules/sso/lib/errors.js.map +7 -0
  50. package/dist/modules/sso/services/accountLinkingService.js +2 -1
  51. package/dist/modules/sso/services/accountLinkingService.js.map +2 -2
  52. package/package.json +7 -8
  53. package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +1 -1
  54. package/src/modules/security/api/enforcement/[id]/route.ts +50 -1
  55. package/src/modules/security/api/enforcement/_shared.ts +83 -2
  56. package/src/modules/security/api/enforcement/compliance/route.ts +10 -1
  57. package/src/modules/security/api/enforcement/route.ts +30 -2
  58. package/src/modules/security/api/mfa/prepare/route.ts +1 -1
  59. package/src/modules/security/api/mfa/recovery/route.ts +1 -1
  60. package/src/modules/security/api/mfa/verify/route.ts +1 -1
  61. package/src/modules/security/api/users/[id]/mfa/reset/route.ts +6 -1
  62. package/src/modules/security/api/users/[id]/mfa/status/route.ts +13 -2
  63. package/src/modules/security/api/users/_shared.ts +69 -1
  64. package/src/modules/security/api/users/mfa/compliance/route.ts +16 -7
  65. package/src/modules/security/commands/createEnforcementPolicy.ts +6 -1
  66. package/src/modules/security/commands/deleteEnforcementPolicy.ts +6 -1
  67. package/src/modules/security/commands/resetUserMfa.ts +6 -1
  68. package/src/modules/security/commands/updateEnforcementPolicy.ts +6 -1
  69. package/src/modules/security/services/MfaAdminService.ts +29 -6
  70. package/src/modules/security/services/MfaEnforcementService.ts +42 -2
  71. package/src/modules/security/services/MfaVerificationService.ts +33 -10
  72. package/src/modules/security/services/SudoChallengeService.ts +16 -11
  73. package/src/modules/sso/api/callback/oidc/route.ts +2 -2
  74. package/src/modules/sso/i18n/de.json +2 -0
  75. package/src/modules/sso/i18n/en.json +2 -0
  76. package/src/modules/sso/i18n/es.json +2 -0
  77. package/src/modules/sso/i18n/pl.json +2 -0
  78. package/src/modules/sso/lib/errors.ts +35 -0
  79. package/src/modules/sso/services/accountLinkingService.ts +2 -1
@@ -1,8 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { Organization, Tenant } from "@open-mercato/core/modules/directory/data/entities";
3
- import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { CrudHttpError, forbidden } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
+ import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
7
+ import { EnforcementScope } from "../../data/entities.js";
6
8
  import { localizeSecurityApiBody, securityApiError } from "../i18n.js";
7
9
  async function resolveEnforcementContext(req) {
8
10
  const auth = await getAuthFromRequest(req);
@@ -24,6 +26,64 @@ async function resolveEnforcementContext(req) {
24
26
  enforcementService: container.resolve("mfaEnforcementService")
25
27
  };
26
28
  }
29
+ function normalizeNullableString(value) {
30
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
31
+ }
32
+ function normalizeOrganizationList(values) {
33
+ if (values === null || values === void 0) return null;
34
+ if (!Array.isArray(values)) return null;
35
+ const result = [];
36
+ for (const value of values) {
37
+ if (typeof value !== "string") continue;
38
+ const trimmed = value.trim();
39
+ if (trimmed) result.push(trimmed);
40
+ }
41
+ return result;
42
+ }
43
+ async function resolveActorContext(ctx) {
44
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
45
+ return {
46
+ tenantId: normalizeNullableString(ctx.auth.tenantId),
47
+ isSuperAdmin
48
+ };
49
+ }
50
+ async function assertActorOwnsOrganization(ctx, organizationId) {
51
+ const rbacService = ctx.container.resolve("rbacService");
52
+ const acl = await rbacService.loadAcl(ctx.auth.sub, {
53
+ tenantId: normalizeNullableString(ctx.auth.tenantId),
54
+ organizationId: normalizeNullableString(ctx.auth.orgId)
55
+ });
56
+ const organizations = normalizeOrganizationList(acl?.organizations);
57
+ if (organizations === null || organizations.includes("__all__")) return;
58
+ if (!organizations.includes(organizationId)) {
59
+ throw forbidden("Not authorized to target this organization.");
60
+ }
61
+ }
62
+ async function assertActorOwnsEnforcementScope(ctx, scope, scopeId) {
63
+ if (scope === EnforcementScope.PLATFORM) {
64
+ const isSuperAdmin2 = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
65
+ if (!isSuperAdmin2) {
66
+ throw forbidden("Platform scope requires platform administrator privileges.");
67
+ }
68
+ return;
69
+ }
70
+ if (scope === EnforcementScope.TENANT) {
71
+ await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, scopeId);
72
+ return;
73
+ }
74
+ const normalizedScopeId = normalizeNullableString(scopeId);
75
+ if (!normalizedScopeId) {
76
+ throw new CrudHttpError(400, { error: "organisation scopeId must use '<tenantId>:<organizationId>' format" });
77
+ }
78
+ const [tenantId, organizationId] = normalizedScopeId.split(":");
79
+ if (!tenantId || !organizationId) {
80
+ throw new CrudHttpError(400, { error: "organisation scopeId must use '<tenantId>:<organizationId>' format" });
81
+ }
82
+ await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, tenantId);
83
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
84
+ if (isSuperAdmin) return;
85
+ await assertActorOwnsOrganization(ctx, organizationId);
86
+ }
27
87
  async function mapEnforcementError(error) {
28
88
  if (error instanceof CrudHttpError) {
29
89
  return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
@@ -92,8 +152,10 @@ function isMfaEnforcementServiceError(error) {
92
152
  return error instanceof Error && error.name === "MfaEnforcementServiceError" && typeof error.statusCode === "number";
93
153
  }
94
154
  export {
155
+ assertActorOwnsEnforcementScope,
95
156
  attachPolicyScopeNames,
96
157
  mapEnforcementError,
158
+ resolveActorContext,
97
159
  resolveEnforcementContext,
98
160
  toPolicyResponse
99
161
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/enforcement/_shared.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { MfaEnforcementPolicy } from '../../data/entities'\nimport type { MfaEnforcementServiceError, MfaEnforcementService } from '../../services/MfaEnforcementService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type EnforcementRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n enforcementService: MfaEnforcementService\n}\n\nexport async function resolveEnforcementContext(req: Request): Promise<EnforcementRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n enforcementService: container.resolve<MfaEnforcementService>('mfaEnforcementService'),\n }\n}\n\nexport async function mapEnforcementError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaEnforcementServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n console.error('security.enforcement.route failure', error)\n return securityApiError(500, 'Failed to process enforcement request.')\n}\n\nexport function toPolicyResponse(policy: MfaEnforcementPolicy): {\n id: string\n scope: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: string | null\n enforcedBy: string\n createdAt: string\n updatedAt: string\n} {\n return {\n id: policy.id,\n scope: policy.scope,\n tenantId: policy.tenantId ?? null,\n tenantName: null,\n organizationId: policy.organizationId ?? null,\n organizationName: null,\n isEnforced: policy.isEnforced,\n allowedMethods: policy.allowedMethods ?? null,\n enforcementDeadline: policy.enforcementDeadline ? policy.enforcementDeadline.toISOString() : null,\n enforcedBy: policy.enforcedBy,\n createdAt: policy.createdAt.toISOString(),\n updatedAt: policy.updatedAt.toISOString(),\n }\n}\n\nexport async function attachPolicyScopeNames(\n container: RequestContainer,\n policies: MfaEnforcementPolicy[],\n): Promise<Array<ReturnType<typeof toPolicyResponse>>> {\n if (policies.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.tenantId ?? null)\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.organizationId ?? null)\n .filter((organizationId): organizationId is string => typeof organizationId === 'string' && organizationId.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length\n ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null })\n : Promise.resolve([]),\n organizationIds.length\n ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null })\n : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n acc[tenantId] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : tenantId\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, organization) => {\n const organizationId = organization?.id ? String(organization.id) : null\n if (!organizationId) return acc\n acc[organizationId] = typeof organization.name === 'string' && organization.name.length > 0\n ? organization.name\n : organizationId\n return acc\n }, {})\n\n return policies.map((policy) => {\n const response = toPolicyResponse(policy)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId\n ? organizationMap[response.organizationId] ?? response.organizationId\n : null,\n }\n })\n}\n\nfunction isMfaEnforcementServiceError(error: unknown): error is MfaEnforcementServiceError {\n return error instanceof Error\n && error.name === 'MfaEnforcementServiceError'\n && typeof (error as Partial<MfaEnforcementServiceError>).statusCode === 'number'\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { EnforcementScope, type MfaEnforcementPolicy } from '../../data/entities'\nimport type { MfaEnforcementServiceError, MfaEnforcementService } from '../../services/MfaEnforcementService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport type EnforcementRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n enforcementService: MfaEnforcementService\n}\n\nexport async function resolveEnforcementContext(req: Request): Promise<EnforcementRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n enforcementService: container.resolve<MfaEnforcementService>('mfaEnforcementService'),\n }\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function resolveActorContext(ctx: EnforcementRequestContext): Promise<EnforcementActorContext> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n return {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n isSuperAdmin,\n }\n}\n\nasync function assertActorOwnsOrganization(\n ctx: EnforcementRequestContext,\n organizationId: string,\n): Promise<void> {\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations === null || organizations.includes('__all__')) return\n if (!organizations.includes(organizationId)) {\n throw forbidden('Not authorized to target this organization.')\n }\n}\n\nexport async function assertActorOwnsEnforcementScope(\n ctx: EnforcementRequestContext,\n scope: EnforcementScope,\n scopeId: string | null | undefined,\n): Promise<void> {\n if (scope === EnforcementScope.PLATFORM) {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (!isSuperAdmin) {\n throw forbidden('Platform scope requires platform administrator privileges.')\n }\n return\n }\n\n if (scope === EnforcementScope.TENANT) {\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, scopeId)\n return\n }\n\n const normalizedScopeId = normalizeNullableString(scopeId)\n if (!normalizedScopeId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n const [tenantId, organizationId] = normalizedScopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, tenantId)\n\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n await assertActorOwnsOrganization(ctx, organizationId)\n}\n\nexport async function mapEnforcementError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaEnforcementServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n console.error('security.enforcement.route failure', error)\n return securityApiError(500, 'Failed to process enforcement request.')\n}\n\nexport function toPolicyResponse(policy: MfaEnforcementPolicy): {\n id: string\n scope: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: string | null\n enforcedBy: string\n createdAt: string\n updatedAt: string\n} {\n return {\n id: policy.id,\n scope: policy.scope,\n tenantId: policy.tenantId ?? null,\n tenantName: null,\n organizationId: policy.organizationId ?? null,\n organizationName: null,\n isEnforced: policy.isEnforced,\n allowedMethods: policy.allowedMethods ?? null,\n enforcementDeadline: policy.enforcementDeadline ? policy.enforcementDeadline.toISOString() : null,\n enforcedBy: policy.enforcedBy,\n createdAt: policy.createdAt.toISOString(),\n updatedAt: policy.updatedAt.toISOString(),\n }\n}\n\nexport async function attachPolicyScopeNames(\n container: RequestContainer,\n policies: MfaEnforcementPolicy[],\n): Promise<Array<ReturnType<typeof toPolicyResponse>>> {\n if (policies.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.tenantId ?? null)\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.organizationId ?? null)\n .filter((organizationId): organizationId is string => typeof organizationId === 'string' && organizationId.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length\n ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null })\n : Promise.resolve([]),\n organizationIds.length\n ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null })\n : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n acc[tenantId] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : tenantId\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, organization) => {\n const organizationId = organization?.id ? String(organization.id) : null\n if (!organizationId) return acc\n acc[organizationId] = typeof organization.name === 'string' && organization.name.length > 0\n ? organization.name\n : organizationId\n return acc\n }, {})\n\n return policies.map((policy) => {\n const response = toPolicyResponse(policy)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId\n ? organizationMap[response.organizationId] ?? response.organizationId\n : null,\n }\n })\n}\n\nfunction isMfaEnforcementServiceError(error: unknown): error is MfaEnforcementServiceError {\n return error instanceof Error\n && error.name === 'MfaEnforcementServiceError'\n && typeof (error as Partial<MfaEnforcementServiceError>).statusCode === 'number'\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["isSuperAdmin"]
7
7
  }
@@ -3,7 +3,12 @@ import { z } from "zod";
3
3
  import { EnforcementScope } from "../../../data/entities.js";
4
4
  import { buildSecurityOpenApi, securityErrorSchema } from "../../openapi.js";
5
5
  import { securityApiError } from "../../i18n.js";
6
- import { mapEnforcementError, resolveEnforcementContext } from "../_shared.js";
6
+ import {
7
+ assertActorOwnsEnforcementScope,
8
+ mapEnforcementError,
9
+ resolveActorContext,
10
+ resolveEnforcementContext
11
+ } from "../_shared.js";
7
12
  const complianceQuerySchema = z.object({
8
13
  scope: z.nativeEnum(EnforcementScope).default(EnforcementScope.PLATFORM),
9
14
  scopeId: z.string().optional()
@@ -31,9 +36,12 @@ async function GET(req) {
31
36
  return securityApiError(400, "Invalid query parameters", { issues: parsedQuery.error.issues });
32
37
  }
33
38
  try {
39
+ await assertActorOwnsEnforcementScope(context, parsedQuery.data.scope, parsedQuery.data.scopeId);
40
+ const actor = await resolveActorContext(context);
34
41
  const report = await context.enforcementService.getComplianceReport(
35
42
  parsedQuery.data.scope,
36
- parsedQuery.data.scopeId
43
+ parsedQuery.data.scopeId,
44
+ actor
37
45
  );
38
46
  return NextResponse.json({
39
47
  scope: parsedQuery.data.scope,
@@ -55,7 +63,8 @@ const openApi = buildSecurityOpenApi({
55
63
  ],
56
64
  errors: [
57
65
  { status: 400, description: "Invalid query parameters", schema: securityErrorSchema },
58
- { status: 401, description: "Unauthorized", schema: securityErrorSchema }
66
+ { status: 401, description: "Unauthorized", schema: securityErrorSchema },
67
+ { status: 403, description: "Not authorized for the requested scope", schema: securityErrorSchema }
59
68
  ]
60
69
  }
61
70
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/enforcement/compliance/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { EnforcementScope } from '../../../data/entities'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { mapEnforcementError, resolveEnforcementContext } from '../_shared'\n\nconst complianceQuerySchema = z.object({\n scope: z.nativeEnum(EnforcementScope).default(EnforcementScope.PLATFORM),\n scopeId: z.string().optional(),\n})\n\nconst complianceResponseSchema = z.object({\n scope: z.nativeEnum(EnforcementScope),\n scopeId: z.string().nullable(),\n total: z.number().int().nonnegative(),\n enrolled: z.number().int().nonnegative(),\n pending: z.number().int().nonnegative(),\n overdue: z.number().int().nonnegative(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n const url = new URL(req.url)\n const parsedQuery = complianceQuerySchema.safeParse({\n scope: url.searchParams.get('scope') ?? undefined,\n scopeId: url.searchParams.get('scopeId') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n try {\n const report = await context.enforcementService.getComplianceReport(\n parsedQuery.data.scope,\n parsedQuery.data.scopeId,\n )\n return NextResponse.json({\n scope: parsedQuery.data.scope,\n scopeId: parsedQuery.data.scopeId ?? null,\n ...report,\n })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Enforcement compliance routes',\n methods: {\n GET: {\n summary: 'Get compliance summary for enforcement scope',\n query: complianceQuerySchema,\n responses: [\n { status: 200, description: 'Compliance summary', schema: complianceResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,wBAAwB;AACjC,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,qBAAqB,iCAAiC;AAE/D,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,OAAO,EAAE,WAAW,gBAAgB,EAAE,QAAQ,iBAAiB,QAAQ;AAAA,EACvE,SAAS,EAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,WAAW,gBAAgB;AAAA,EACpC,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACtC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACxC,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,UAAU,MAAM,0BAA0B,GAAG;AACnD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,sBAAsB,UAAU;AAAA,IAClD,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,SAAS,IAAI,aAAa,IAAI,SAAS,KAAK;AAAA,EAC9C,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,iBAAiB,KAAK,4BAA4B,EAAE,QAAQ,YAAY,MAAM,OAAO,CAAC;AAAA,EAC/F;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,mBAAmB;AAAA,MAC9C,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,IACnB;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,YAAY,KAAK;AAAA,MACxB,SAAS,YAAY,KAAK,WAAW;AAAA,MACrC,GAAG;AAAA,IACL,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,oBAAoB,KAAK;AAAA,EACxC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,yBAAyB;AAAA,MACrF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,oBAAoB;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { EnforcementScope } from '../../../data/entities'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport {\n assertActorOwnsEnforcementScope,\n mapEnforcementError,\n resolveActorContext,\n resolveEnforcementContext,\n} from '../_shared'\n\nconst complianceQuerySchema = z.object({\n scope: z.nativeEnum(EnforcementScope).default(EnforcementScope.PLATFORM),\n scopeId: z.string().optional(),\n})\n\nconst complianceResponseSchema = z.object({\n scope: z.nativeEnum(EnforcementScope),\n scopeId: z.string().nullable(),\n total: z.number().int().nonnegative(),\n enrolled: z.number().int().nonnegative(),\n pending: z.number().int().nonnegative(),\n overdue: z.number().int().nonnegative(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n const url = new URL(req.url)\n const parsedQuery = complianceQuerySchema.safeParse({\n scope: url.searchParams.get('scope') ?? undefined,\n scopeId: url.searchParams.get('scopeId') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n try {\n await assertActorOwnsEnforcementScope(context, parsedQuery.data.scope, parsedQuery.data.scopeId)\n const actor = await resolveActorContext(context)\n const report = await context.enforcementService.getComplianceReport(\n parsedQuery.data.scope,\n parsedQuery.data.scopeId,\n actor,\n )\n return NextResponse.json({\n scope: parsedQuery.data.scope,\n scopeId: parsedQuery.data.scopeId ?? null,\n ...report,\n })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Enforcement compliance routes',\n methods: {\n GET: {\n summary: 'Get compliance summary for enforcement scope',\n query: complianceQuerySchema,\n responses: [\n { status: 200, description: 'Compliance summary', schema: complianceResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,wBAAwB;AACjC,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,OAAO,EAAE,WAAW,gBAAgB,EAAE,QAAQ,iBAAiB,QAAQ;AAAA,EACvE,SAAS,EAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,WAAW,gBAAgB;AAAA,EACpC,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACtC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACxC,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,UAAU,MAAM,0BAA0B,GAAG;AACnD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,sBAAsB,UAAU;AAAA,IAClD,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,SAAS,IAAI,aAAa,IAAI,SAAS,KAAK;AAAA,EAC9C,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,iBAAiB,KAAK,4BAA4B,EAAE,QAAQ,YAAY,MAAM,OAAO,CAAC;AAAA,EAC/F;AAEA,MAAI;AACF,UAAM,gCAAgC,SAAS,YAAY,KAAK,OAAO,YAAY,KAAK,OAAO;AAC/F,UAAM,QAAQ,MAAM,oBAAoB,OAAO;AAC/C,UAAM,SAAS,MAAM,QAAQ,mBAAmB;AAAA,MAC9C,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB;AAAA,IACF;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,YAAY,KAAK;AAAA,MACxB,SAAS,YAAY,KAAK,WAAW;AAAA,MACrC,GAAG;AAAA,IACL,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,oBAAoB,KAAK;AAAA,EACxC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,yBAAyB;AAAA,MACrF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,oBAAoB;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,0CAA0C,QAAQ,oBAAoB;AAAA,MACpG;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,13 @@ import { enforcementPolicySchema } from "../../data/validators.js";
4
4
  import { EnforcementScope } from "../../data/entities.js";
5
5
  import { buildSecurityOpenApi, securityErrorSchema } from "../openapi.js";
6
6
  import { securityApiError } from "../i18n.js";
7
- import { attachPolicyScopeNames, mapEnforcementError, resolveEnforcementContext } from "./_shared.js";
7
+ import {
8
+ assertActorOwnsEnforcementScope,
9
+ attachPolicyScopeNames,
10
+ mapEnforcementError,
11
+ resolveActorContext,
12
+ resolveEnforcementContext
13
+ } from "./_shared.js";
8
14
  const enforcementPolicyResponseSchema = z.object({
9
15
  id: z.string().uuid(),
10
16
  scope: z.nativeEnum(EnforcementScope),
@@ -43,7 +49,8 @@ async function GET(req) {
43
49
  if (!parsedQuery.success) {
44
50
  return securityApiError(400, "Invalid query parameters", { issues: parsedQuery.error.issues });
45
51
  }
46
- const policies = await context.enforcementService.listPolicies(parsedQuery.data);
52
+ const actor = await resolveActorContext(context);
53
+ const policies = await context.enforcementService.listPolicies(parsedQuery.data, actor);
47
54
  return NextResponse.json({
48
55
  items: await attachPolicyScopeNames(context.container, policies)
49
56
  });
@@ -65,6 +72,11 @@ async function POST(req) {
65
72
  return securityApiError(400, "Invalid payload", { issues: parsedBody.error.issues });
66
73
  }
67
74
  try {
75
+ await assertActorOwnsEnforcementScope(
76
+ context,
77
+ parsedBody.data.scope,
78
+ scopeIdFromPolicyInput(parsedBody.data)
79
+ );
68
80
  const commandBus = context.container.resolve("commandBus");
69
81
  const { result } = await commandBus.execute("security.enforcement.create", {
70
82
  input: parsedBody.data,
@@ -75,6 +87,16 @@ async function POST(req) {
75
87
  return await mapEnforcementError(error);
76
88
  }
77
89
  }
90
+ function scopeIdFromPolicyInput(data) {
91
+ if (data.scope === EnforcementScope.TENANT) {
92
+ return data.tenantId ?? null;
93
+ }
94
+ if (data.scope === EnforcementScope.ORGANISATION) {
95
+ if (!data.tenantId || !data.organizationId) return null;
96
+ return `${data.tenantId}:${data.organizationId}`;
97
+ }
98
+ return null;
99
+ }
78
100
  const openApi = buildSecurityOpenApi({
79
101
  summary: "Enforcement policy routes",
80
102
  methods: {
@@ -101,6 +123,7 @@ const openApi = buildSecurityOpenApi({
101
123
  errors: [
102
124
  { status: 400, description: "Invalid payload", schema: securityErrorSchema },
103
125
  { status: 401, description: "Unauthorized", schema: securityErrorSchema },
126
+ { status: 403, description: "Not authorized for the requested scope", schema: securityErrorSchema },
104
127
  { status: 409, description: "Conflict", schema: securityErrorSchema }
105
128
  ]
106
129
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/enforcement/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforcementPolicySchema } from '../../data/validators'\nimport { EnforcementScope } from '../../data/entities'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../openapi'\nimport { securityApiError } from '../i18n'\nimport { attachPolicyScopeNames, mapEnforcementError, resolveEnforcementContext } from './_shared'\n\nconst enforcementPolicyResponseSchema = z.object({\n id: z.string().uuid(),\n scope: z.nativeEnum(EnforcementScope),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n isEnforced: z.boolean(),\n allowedMethods: z.array(z.string()).nullable(),\n enforcementDeadline: z.string().nullable(),\n enforcedBy: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst enforcementPolicyListResponseSchema = z.object({\n items: z.array(enforcementPolicyResponseSchema),\n})\n\nconst createEnforcementPolicyResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst listQuerySchema = z.object({\n scope: z.nativeEnum(EnforcementScope).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n POST: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n try {\n const url = new URL(req.url)\n const parsedQuery = listQuerySchema.safeParse({\n scope: url.searchParams.get('scope') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n const policies = await context.enforcementService.listPolicies(parsedQuery.data)\n return NextResponse.json({\n items: await attachPolicyScopeNames(context.container, policies),\n })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nexport async function POST(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n rawBody = {}\n }\n\n const parsedBody = enforcementPolicySchema.safeParse(rawBody)\n if (!parsedBody.success) {\n return securityApiError(400, 'Invalid payload', { issues: parsedBody.error.issues })\n }\n\n try {\n const commandBus = context.container.resolve<CommandBus>('commandBus')\n const { result } = await commandBus.execute('security.enforcement.create', {\n input: parsedBody.data,\n ctx: context.commandContext,\n })\n return NextResponse.json(result, { status: 201 })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Enforcement policy routes',\n methods: {\n GET: {\n summary: 'List enforcement policies',\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Enforcement policies', schema: enforcementPolicyListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n POST: {\n summary: 'Create enforcement policy',\n requestBody: {\n contentType: 'application/json',\n schema: enforcementPolicySchema,\n },\n responses: [\n { status: 201, description: 'Policy created', schema: createEnforcementPolicyResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 409, description: 'Conflict', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforcementPolicySchema } from '../../data/validators'\nimport { EnforcementScope } from '../../data/entities'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../openapi'\nimport { securityApiError } from '../i18n'\nimport {\n assertActorOwnsEnforcementScope,\n attachPolicyScopeNames,\n mapEnforcementError,\n resolveActorContext,\n resolveEnforcementContext,\n} from './_shared'\n\nconst enforcementPolicyResponseSchema = z.object({\n id: z.string().uuid(),\n scope: z.nativeEnum(EnforcementScope),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n isEnforced: z.boolean(),\n allowedMethods: z.array(z.string()).nullable(),\n enforcementDeadline: z.string().nullable(),\n enforcedBy: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst enforcementPolicyListResponseSchema = z.object({\n items: z.array(enforcementPolicyResponseSchema),\n})\n\nconst createEnforcementPolicyResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst listQuerySchema = z.object({\n scope: z.nativeEnum(EnforcementScope).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n POST: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n try {\n const url = new URL(req.url)\n const parsedQuery = listQuerySchema.safeParse({\n scope: url.searchParams.get('scope') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n const actor = await resolveActorContext(context)\n const policies = await context.enforcementService.listPolicies(parsedQuery.data, actor)\n return NextResponse.json({\n items: await attachPolicyScopeNames(context.container, policies),\n })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nexport async function POST(req: Request) {\n const context = await resolveEnforcementContext(req)\n if (context instanceof NextResponse) return context\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n rawBody = {}\n }\n\n const parsedBody = enforcementPolicySchema.safeParse(rawBody)\n if (!parsedBody.success) {\n return securityApiError(400, 'Invalid payload', { issues: parsedBody.error.issues })\n }\n\n try {\n await assertActorOwnsEnforcementScope(\n context,\n parsedBody.data.scope,\n scopeIdFromPolicyInput(parsedBody.data),\n )\n const commandBus = context.container.resolve<CommandBus>('commandBus')\n const { result } = await commandBus.execute('security.enforcement.create', {\n input: parsedBody.data,\n ctx: context.commandContext,\n })\n return NextResponse.json(result, { status: 201 })\n } catch (error) {\n return await mapEnforcementError(error)\n }\n}\n\nfunction scopeIdFromPolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n}): string | null {\n if (data.scope === EnforcementScope.TENANT) {\n return data.tenantId ?? null\n }\n if (data.scope === EnforcementScope.ORGANISATION) {\n if (!data.tenantId || !data.organizationId) return null\n return `${data.tenantId}:${data.organizationId}`\n }\n return null\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Enforcement policy routes',\n methods: {\n GET: {\n summary: 'List enforcement policies',\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Enforcement policies', schema: enforcementPolicyListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n POST: {\n summary: 'Create enforcement policy',\n requestBody: {\n contentType: 'application/json',\n schema: enforcementPolicySchema,\n },\n responses: [\n { status: 201, description: 'Policy created', schema: createEnforcementPolicyResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },\n { status: 409, description: 'Conflict', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,7 @@ const responseSchema = z.object({
12
12
  clientData: z.record(z.string(), z.unknown()).optional()
13
13
  });
14
14
  const metadata = {
15
- POST: { requireAuth: true }
15
+ POST: { requireAuth: true, rateLimit: { points: 20, duration: 60, keyPrefix: "security_mfa_prepare" } }
16
16
  };
17
17
  async function POST(req) {
18
18
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/prepare/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { mapMfaError, readJsonRecord, readString, resolveMfaRequestContext } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n clientData: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const prepared = await context.mfaVerificationService.prepareChallenge(challengeId, methodType, { request: req })\n return NextResponse.json({ ok: true, ...(prepared.clientData ? { clientData: prepared.clientData } : {}) })\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge prepare routes',\n methods: {\n POST: {\n summary: 'Prepare MFA challenge payload for selected method',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge prepared', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,aAAa,gBAAgB,YAAY,gCAAgC;AAElF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACzD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,iBAAiB,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAChH,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,GAAI,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAG,CAAC;AAAA,EAC5G,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { mapMfaError, readJsonRecord, readString, resolveMfaRequestContext } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n clientData: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 20, duration: 60, keyPrefix: 'security_mfa_prepare' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const prepared = await context.mfaVerificationService.prepareChallenge(challengeId, methodType, { request: req })\n return NextResponse.json({ ok: true, ...(prepared.clientData ? { clientData: prepared.clientData } : {}) })\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge prepare routes',\n methods: {\n POST: {\n summary: 'Prepare MFA challenge payload for selected method',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge prepared', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,aAAa,gBAAgB,YAAY,gCAAgC;AAElF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACzD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,uBAAuB,EAAE;AACxG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,iBAAiB,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAChH,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,GAAI,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAG,CAAC;AAAA,EAC5G,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,7 @@ const responseSchema = z.object({
12
12
  redirect: z.string()
13
13
  });
14
14
  const metadata = {
15
- POST: { requireAuth: true }
15
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: "security_mfa_recovery" } }
16
16
  };
17
17
  async function POST(req) {
18
18
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/recovery/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n code: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const code = readString(body.code)\n if (!code) {\n return securityApiError(400, 'code is required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyRecoveryCode(context.auth.sub, code)\n if (!verified) {\n return securityApiError(401, 'Invalid recovery code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA recovery routes',\n methods: {\n POST: {\n summary: 'Verify MFA recovery code during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'Recovery challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid recovery code', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,OAAO,WAAW,KAAK,IAAI;AACjC,MAAI,CAAC,MAAM;AACT,WAAO,iBAAiB,KAAK,mBAAmB;AAAA,EAClD;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,mBAAmB,QAAQ,KAAK,KAAK,IAAI;AAC/F,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,wBAAwB;AAAA,IACvD;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,+BAA+B,QAAQ,eAAe,CAAC;AAAA,MAC/F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,oBAAoB;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n code: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_recovery' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const code = readString(body.code)\n if (!code) {\n return securityApiError(400, 'code is required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyRecoveryCode(context.auth.sub, code)\n if (!verified) {\n return securityApiError(401, 'Invalid recovery code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA recovery routes',\n methods: {\n POST: {\n summary: 'Verify MFA recovery code during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'Recovery challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid recovery code', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,wBAAwB,EAAE;AACzG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,OAAO,WAAW,KAAK,IAAI;AACjC,MAAI,CAAC,MAAM;AACT,WAAO,iBAAiB,KAAK,mBAAmB;AAAA,EAClD;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,mBAAmB,QAAQ,KAAK,KAAK,IAAI;AAC/F,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,wBAAwB;AAAA,IACvD;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,+BAA+B,QAAQ,eAAe,CAAC;AAAA,MAC/F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,oBAAoB;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,7 @@ const responseSchema = z.object({
14
14
  redirect: z.string()
15
15
  });
16
16
  const metadata = {
17
- POST: { requireAuth: true }
17
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: "security_mfa_verify" } }
18
18
  };
19
19
  async function POST(req) {
20
20
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/verify/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).default({}),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n const payload = body.payload && typeof body.payload === 'object' ? body.payload : {}\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyChallenge(challengeId, methodType, payload, { request: req })\n if (!verified) {\n return securityApiError(401, 'Invalid MFA verification code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge verify routes',\n methods: {\n POST: {\n summary: 'Verify MFA challenge during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid challenge response', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,QAAM,UAAU,KAAK,WAAW,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,CAAC;AACnF,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,gBAAgB,aAAa,YAAY,SAAS,EAAE,SAAS,IAAI,CAAC;AACxH,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,gCAAgC;AAAA,IAC/D;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,oBAAoB;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).default({}),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_verify' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n const payload = body.payload && typeof body.payload === 'object' ? body.payload : {}\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyChallenge(challengeId, methodType, payload, { request: req })\n if (!verified) {\n return securityApiError(401, 'Invalid MFA verification code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge verify routes',\n methods: {\n POST: {\n summary: 'Verify MFA challenge during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid challenge response', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,sBAAsB,EAAE;AACvG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,QAAM,UAAU,KAAK,WAAW,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,CAAC;AACnF,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,gBAAgB,aAAa,YAAY,SAAS,EAAE,SAAS,IAAI,CAAC;AACxH,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,gCAAgC;AAAA,IAC/D;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,oBAAoB;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,11 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { buildSecurityOpenApi, securityErrorSchema } from "../../../../openapi.js";
4
4
  import { securityApiError } from "../../../../i18n.js";
5
- import { mapSecurityUsersError, resolveSecurityUsersContext } from "../../../_shared.js";
5
+ import {
6
+ assertActorCanAccessSecurityUserTarget,
7
+ mapSecurityUsersError,
8
+ resolveSecurityUsersContext
9
+ } from "../../../_shared.js";
6
10
  import { requireSudo } from "../../../../../lib/sudo-middleware.js";
7
11
  const paramsSchema = z.object({
8
12
  id: z.string().uuid()
@@ -34,6 +38,7 @@ async function POST(req, routeContext) {
34
38
  return securityApiError(400, "Invalid payload", { issues: parsedBody.error.issues });
35
39
  }
36
40
  try {
41
+ await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id);
37
42
  await requireSudo(req, "security.admin.mfa.reset");
38
43
  const commandBus = context.container.resolve("commandBus");
39
44
  const { result } = await commandBus.execute("security.admin.mfa.reset", {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/security/api/users/%5Bid%5D/mfa/reset/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'\nimport { securityApiError } from '../../../../i18n'\nimport { mapSecurityUsersError, resolveSecurityUsersContext } from '../../../_shared'\nimport { requireSudo } from '../../../../../lib/sudo-middleware'\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst bodySchema = z.object({\n reason: z.string().min(1),\n})\n\nconst okResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function POST(req: Request, routeContext: { params: Promise<{ id: string }> }) {\n const context = await resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const parsedParams = paramsSchema.safeParse(await routeContext.params)\n if (!parsedParams.success) {\n return securityApiError(400, 'Invalid user id.', { issues: parsedParams.error.issues })\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n rawBody = {}\n }\n\n const parsedBody = bodySchema.safeParse(rawBody)\n if (!parsedBody.success) {\n return securityApiError(400, 'Invalid payload', { issues: parsedBody.error.issues })\n }\n\n try {\n await requireSudo(req, 'security.admin.mfa.reset')\n const commandBus = context.container.resolve<CommandBus>('commandBus')\n const { result } = await commandBus.execute('security.admin.mfa.reset', {\n input: {\n userId: parsedParams.data.id,\n reason: parsedBody.data.reason,\n },\n ctx: context.commandContext,\n })\n return NextResponse.json(result)\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin user MFA reset routes',\n methods: {\n POST: {\n summary: 'Reset MFA for user',\n pathParams: paramsSchema,\n requestBody: {\n contentType: 'application/json',\n schema: bodySchema,\n },\n responses: [\n { status: 200, description: 'MFA reset completed', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid input', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Sudo required', schema: securityErrorSchema },\n { status: 404, description: 'User not found', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,mCAAmC;AACnE,SAAS,mBAAmB;AAE5B,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAC1B,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACxE;AAEA,eAAsB,KAAK,KAAc,cAAmD;AAC1F,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,eAAe,aAAa,UAAU,MAAM,aAAa,MAAM;AACrE,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,iBAAiB,KAAK,oBAAoB,EAAE,QAAQ,aAAa,MAAM,OAAO,CAAC;AAAA,EACxF;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,cAAU,CAAC;AAAA,EACb;AAEA,QAAM,aAAa,WAAW,UAAU,OAAO;AAC/C,MAAI,CAAC,WAAW,SAAS;AACvB,WAAO,iBAAiB,KAAK,mBAAmB,EAAE,QAAQ,WAAW,MAAM,OAAO,CAAC;AAAA,EACrF;AAEA,MAAI;AACF,UAAM,YAAY,KAAK,0BAA0B;AACjD,UAAM,aAAa,QAAQ,UAAU,QAAoB,YAAY;AACrE,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAAQ,4BAA4B;AAAA,MACtE,OAAO;AAAA,QACL,QAAQ,aAAa,KAAK;AAAA,QAC1B,QAAQ,WAAW,KAAK;AAAA,MAC1B;AAAA,MACA,KAAK,QAAQ;AAAA,IACf,CAAC;AACD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAO;AACd,WAAO,MAAM,sBAAsB,KAAK;AAAA,EAC1C;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB,QAAQ,iBAAiB;AAAA,MAC9E;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,oBAAoB;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,oBAAoB;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'\nimport { securityApiError } from '../../../../i18n'\nimport {\n assertActorCanAccessSecurityUserTarget,\n mapSecurityUsersError,\n resolveSecurityUsersContext,\n} from '../../../_shared'\nimport { requireSudo } from '../../../../../lib/sudo-middleware'\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst bodySchema = z.object({\n reason: z.string().min(1),\n})\n\nconst okResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function POST(req: Request, routeContext: { params: Promise<{ id: string }> }) {\n const context = await resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const parsedParams = paramsSchema.safeParse(await routeContext.params)\n if (!parsedParams.success) {\n return securityApiError(400, 'Invalid user id.', { issues: parsedParams.error.issues })\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n rawBody = {}\n }\n\n const parsedBody = bodySchema.safeParse(rawBody)\n if (!parsedBody.success) {\n return securityApiError(400, 'Invalid payload', { issues: parsedBody.error.issues })\n }\n\n try {\n await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id)\n await requireSudo(req, 'security.admin.mfa.reset')\n const commandBus = context.container.resolve<CommandBus>('commandBus')\n const { result } = await commandBus.execute('security.admin.mfa.reset', {\n input: {\n userId: parsedParams.data.id,\n reason: parsedBody.data.reason,\n },\n ctx: context.commandContext,\n })\n return NextResponse.json(result)\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin user MFA reset routes',\n methods: {\n POST: {\n summary: 'Reset MFA for user',\n pathParams: paramsSchema,\n requestBody: {\n contentType: 'application/json',\n schema: bodySchema,\n },\n responses: [\n { status: 200, description: 'MFA reset completed', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid input', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Sudo required', schema: securityErrorSchema },\n { status: 404, description: 'User not found', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAmB;AAE5B,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAC1B,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACxE;AAEA,eAAsB,KAAK,KAAc,cAAmD;AAC1F,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,eAAe,aAAa,UAAU,MAAM,aAAa,MAAM;AACrE,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,iBAAiB,KAAK,oBAAoB,EAAE,QAAQ,aAAa,MAAM,OAAO,CAAC;AAAA,EACxF;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,cAAU,CAAC;AAAA,EACb;AAEA,QAAM,aAAa,WAAW,UAAU,OAAO;AAC/C,MAAI,CAAC,WAAW,SAAS;AACvB,WAAO,iBAAiB,KAAK,mBAAmB,EAAE,QAAQ,WAAW,MAAM,OAAO,CAAC;AAAA,EACrF;AAEA,MAAI;AACF,UAAM,uCAAuC,SAAS,aAAa,KAAK,EAAE;AAC1E,UAAM,YAAY,KAAK,0BAA0B;AACjD,UAAM,aAAa,QAAQ,UAAU,QAAoB,YAAY;AACrE,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAAQ,4BAA4B;AAAA,MACtE,OAAO;AAAA,QACL,QAAQ,aAAa,KAAK;AAAA,QAC1B,QAAQ,WAAW,KAAK;AAAA,MAC1B;AAAA,MACA,KAAK,QAAQ;AAAA,IACf,CAAC;AACD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAO;AACd,WAAO,MAAM,sBAAsB,KAAK;AAAA,EAC1C;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB,QAAQ,iBAAiB;AAAA,MAC9E;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,oBAAoB;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,oBAAoB;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,12 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { buildSecurityOpenApi, securityErrorSchema } from "../../../../openapi.js";
4
4
  import { securityApiError } from "../../../../i18n.js";
5
- import { mapSecurityUsersError, resolveSecurityUsersContext } from "../../../_shared.js";
5
+ import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
6
+ import {
7
+ assertActorCanAccessSecurityUserTarget,
8
+ mapSecurityUsersError,
9
+ resolveSecurityUsersContext
10
+ } from "../../../_shared.js";
6
11
  const paramsSchema = z.object({
7
12
  id: z.string().uuid()
8
13
  });
@@ -28,7 +33,12 @@ async function GET(req, routeContext) {
28
33
  return securityApiError(400, "Invalid user id.", { issues: parsedParams.error.issues });
29
34
  }
30
35
  try {
31
- const status = await context.mfaAdminService.getUserMfaStatus(parsedParams.data.id);
36
+ await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id);
37
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container });
38
+ const status = await context.mfaAdminService.getUserMfaStatus(parsedParams.data.id, {
39
+ tenantId: context.auth.tenantId ?? null,
40
+ isSuperAdmin
41
+ });
32
42
  return NextResponse.json({
33
43
  ...status,
34
44
  methods: status.methods.map((method) => ({
@@ -52,6 +62,7 @@ const openApi = buildSecurityOpenApi({
52
62
  errors: [
53
63
  { status: 400, description: "Invalid user id", schema: securityErrorSchema },
54
64
  { status: 401, description: "Unauthorized", schema: securityErrorSchema },
65
+ { status: 403, description: "Not authorized to access this user", schema: securityErrorSchema },
55
66
  { status: 404, description: "User not found", schema: securityErrorSchema }
56
67
  ]
57
68
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/security/api/users/%5Bid%5D/mfa/status/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'\nimport { securityApiError } from '../../../../i18n'\nimport { mapSecurityUsersError, resolveSecurityUsersContext } from '../../../_shared'\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst methodSchema = z.object({\n type: z.string(),\n label: z.string().optional(),\n lastUsed: z.string().datetime().optional(),\n})\n\nconst statusResponseSchema = z.object({\n enrolled: z.boolean(),\n methods: z.array(methodSchema),\n recoveryCodesRemaining: z.number().int().nonnegative(),\n compliant: z.boolean(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request, routeContext: { params: Promise<{ id: string }> }) {\n const context = await resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const parsedParams = paramsSchema.safeParse(await routeContext.params)\n if (!parsedParams.success) {\n return securityApiError(400, 'Invalid user id.', { issues: parsedParams.error.issues })\n }\n\n try {\n const status = await context.mfaAdminService.getUserMfaStatus(parsedParams.data.id)\n return NextResponse.json({\n ...status,\n methods: status.methods.map((method) => ({\n ...method,\n ...(method.lastUsed ? { lastUsed: method.lastUsed.toISOString() } : {}),\n })),\n })\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin user MFA status routes',\n methods: {\n GET: {\n summary: 'Get MFA status for user',\n pathParams: paramsSchema,\n responses: [\n { status: 200, description: 'MFA status', schema: statusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid user id', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 404, description: 'User not found', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,mCAAmC;AAEnE,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC3C,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,QAAQ;AAAA,EACpB,SAAS,EAAE,MAAM,YAAY;AAAA,EAC7B,wBAAwB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACrD,WAAW,EAAE,QAAQ;AACvB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc,cAAmD;AACzF,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,eAAe,aAAa,UAAU,MAAM,aAAa,MAAM;AACrE,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,iBAAiB,KAAK,oBAAoB,EAAE,QAAQ,aAAa,MAAM,OAAO,CAAC;AAAA,EACxF;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,gBAAgB,iBAAiB,aAAa,KAAK,EAAE;AAClF,WAAO,aAAa,KAAK;AAAA,MACvB,GAAG;AAAA,MACH,SAAS,OAAO,QAAQ,IAAI,CAAC,YAAY;AAAA,QACvC,GAAG;AAAA,QACH,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,YAAY,EAAE,IAAI,CAAC;AAAA,MACvE,EAAE;AAAA,IACJ,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,sBAAsB,KAAK;AAAA,EAC1C;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,cAAc,QAAQ,qBAAqB;AAAA,MACzE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'\nimport { securityApiError } from '../../../../i18n'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport {\n assertActorCanAccessSecurityUserTarget,\n mapSecurityUsersError,\n resolveSecurityUsersContext,\n} from '../../../_shared'\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst methodSchema = z.object({\n type: z.string(),\n label: z.string().optional(),\n lastUsed: z.string().datetime().optional(),\n})\n\nconst statusResponseSchema = z.object({\n enrolled: z.boolean(),\n methods: z.array(methodSchema),\n recoveryCodesRemaining: z.number().int().nonnegative(),\n compliant: z.boolean(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['security.admin.manage'] },\n}\n\nexport async function GET(req: Request, routeContext: { params: Promise<{ id: string }> }) {\n const context = await resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const parsedParams = paramsSchema.safeParse(await routeContext.params)\n if (!parsedParams.success) {\n return securityApiError(400, 'Invalid user id.', { issues: parsedParams.error.issues })\n }\n\n try {\n await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id)\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container })\n const status = await context.mfaAdminService.getUserMfaStatus(parsedParams.data.id, {\n tenantId: context.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return NextResponse.json({\n ...status,\n methods: status.methods.map((method) => ({\n ...method,\n ...(method.lastUsed ? { lastUsed: method.lastUsed.toISOString() } : {}),\n })),\n })\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin user MFA status routes',\n methods: {\n GET: {\n summary: 'Get MFA status for user',\n pathParams: paramsSchema,\n responses: [\n { status: 200, description: 'MFA status', schema: statusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid user id', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Not authorized to access this user', schema: securityErrorSchema },\n { status: 404, description: 'User not found', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC3C,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,QAAQ;AAAA,EACpB,SAAS,EAAE,MAAM,YAAY;AAAA,EAC7B,wBAAwB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACrD,WAAW,EAAE,QAAQ;AACvB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc,cAAmD;AACzF,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,eAAe,aAAa,UAAU,MAAM,aAAa,MAAM;AACrE,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,iBAAiB,KAAK,oBAAoB,EAAE,QAAQ,aAAa,MAAM,OAAO,CAAC;AAAA,EACxF;AAEA,MAAI;AACF,UAAM,uCAAuC,SAAS,aAAa,KAAK,EAAE;AAC1E,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,QAAQ,MAAM,WAAW,QAAQ,UAAU,CAAC;AACnG,UAAM,SAAS,MAAM,QAAQ,gBAAgB,iBAAiB,aAAa,KAAK,IAAI;AAAA,MAClF,UAAU,QAAQ,KAAK,YAAY;AAAA,MACnC;AAAA,IACF,CAAC;AACD,WAAO,aAAa,KAAK;AAAA,MACvB,GAAG;AAAA,MACH,SAAS,OAAO,QAAQ,IAAI,CAAC,YAAY;AAAA,QACvC,GAAG;AAAA,QACH,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,YAAY,EAAE,IAAI,CAAC;AAAA,MACvE,EAAE;AAAA,IACJ,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,sBAAsB,KAAK;AAAA,EAC1C;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,cAAc,QAAQ,qBAAqB;AAAA,MACzE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,oBAAoB;AAAA,QAC9F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }