@open-mercato/enterprise 0.6.4-develop.4382.1.6b4f656b77 → 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,7 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
- import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ import { CrudHttpError, forbidden } from "@open-mercato/shared/lib/crud/errors";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
7
+ import { User } from "@open-mercato/core/modules/auth/data/entities";
5
8
  import { isSudoRequiredError } from "../../lib/sudo-middleware.js";
6
9
  import { localizeSecurityApiBody, securityApiError } from "../i18n.js";
7
10
  async function resolveSecurityUsersContext(req) {
@@ -24,6 +27,56 @@ async function resolveSecurityUsersContext(req) {
24
27
  mfaAdminService: container.resolve("mfaAdminService")
25
28
  };
26
29
  }
30
+ function normalizeNullableString(value) {
31
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
32
+ }
33
+ function normalizeOrganizationList(values) {
34
+ if (values === null || values === void 0) return null;
35
+ if (!Array.isArray(values)) return null;
36
+ const result = [];
37
+ for (const value of values) {
38
+ if (typeof value !== "string") continue;
39
+ const trimmed = value.trim();
40
+ if (trimmed) result.push(trimmed);
41
+ }
42
+ return result;
43
+ }
44
+ async function assertActorCanAccessSecurityUserTarget(ctx, targetUserId) {
45
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
46
+ if (isSuperAdmin) return;
47
+ const em = ctx.container.resolve("em");
48
+ const target = await findOneWithDecryption(
49
+ em,
50
+ User,
51
+ { id: targetUserId, deletedAt: null },
52
+ {},
53
+ { tenantId: null, organizationId: null }
54
+ );
55
+ if (!target) {
56
+ throw new CrudHttpError(404, { error: "User not found" });
57
+ }
58
+ const actorTenantId = normalizeNullableString(ctx.auth.tenantId);
59
+ const targetTenantId = normalizeNullableString(target.tenantId);
60
+ if (!targetTenantId || targetTenantId !== actorTenantId) {
61
+ throw new CrudHttpError(404, { error: "User not found" });
62
+ }
63
+ const rbacService = ctx.container.resolve("rbacService");
64
+ const acl = await rbacService.loadAcl(ctx.auth.sub, {
65
+ tenantId: actorTenantId,
66
+ organizationId: normalizeNullableString(ctx.auth.orgId)
67
+ });
68
+ const organizations = normalizeOrganizationList(acl?.organizations);
69
+ if (organizations !== null && !organizations.includes("__all__")) {
70
+ const targetOrganizationId = normalizeNullableString(target.organizationId);
71
+ if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {
72
+ throw forbidden("Not authorized to access this user.");
73
+ }
74
+ }
75
+ }
76
+ async function assertActorOwnsTenantScope(ctx, requestedTenantId) {
77
+ const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId);
78
+ return resolved ?? ctx.auth.tenantId ?? null;
79
+ }
27
80
  async function mapSecurityUsersError(error) {
28
81
  if (error instanceof CrudHttpError) {
29
82
  return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
@@ -41,6 +94,8 @@ function isMfaAdminServiceError(error) {
41
94
  return error instanceof Error && error.name === "MfaAdminServiceError" && typeof error.statusCode === "number";
42
95
  }
43
96
  export {
97
+ assertActorCanAccessSecurityUserTarget,
98
+ assertActorOwnsTenantScope,
44
99
  mapSecurityUsersError,
45
100
  resolveSecurityUsersContext
46
101
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/users/_shared.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | 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 mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\n }\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n console.error('security.users.route failure', error)\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAE9B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAEpC,SAAS,yBAAyB,wBAAwB;AAY1D,eAAsB,4BACpB,KACqD;AACrD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,iBAAiB,KAAK,cAAc;AAAA,EAC7C;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,MACd;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,wBAAwB,KAAK,SAAS;AAAA,MACtC,iBAAiB,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,MAC7C,SAAS;AAAA,IACX;AAAA,IACA,iBAAiB,UAAU,QAAyB,iBAAiB;AAAA,EACvE;AACF;AAEA,eAAsB,sBAAsB,OAAuC;AACjF,MAAI,iBAAiB,eAAe;AAClC,WAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC9F;AACA,MAAI,oBAAoB,KAAK,GAAG;AAC9B,WAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,WAAW,CAAC;AAAA,EAClG;AACA,MAAI,uBAAuB,KAAK,GAAG;AACjC,WAAO,iBAAiB,MAAM,YAAY,MAAM,OAAO;AAAA,EACzD;AAEA,UAAQ,MAAM,gCAAgC,KAAK;AACnD,SAAO,iBAAiB,KAAK,0CAA0C;AACzE;AAEA,SAAS,uBAAuB,OAA+C;AAC7E,SAAO,iBAAiB,SACnB,MAAM,SAAS,0BACf,OAAQ,MAAwC,eAAe;AACtE;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | 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 mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\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 assertActorCanAccessSecurityUserTarget(\n ctx: SecurityUsersRequestContext,\n targetUserId: string,\n): Promise<void> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n\n const em = ctx.container.resolve<EntityManager>('em')\n const target = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const actorTenantId = normalizeNullableString(ctx.auth.tenantId)\n const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)\n if (!targetTenantId || targetTenantId !== actorTenantId) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: actorTenantId,\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations !== null && !organizations.includes('__all__')) {\n const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)\n if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {\n throw forbidden('Not authorized to access this user.')\n }\n }\n}\n\nexport async function assertActorOwnsTenantScope(\n ctx: SecurityUsersRequestContext,\n requestedTenantId: string | null | undefined,\n): Promise<string | null> {\n const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)\n return resolved ?? ctx.auth.tenantId ?? null\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n console.error('security.users.route failure', error)\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,eAAe,iBAAiB;AAEzC,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,wBAAwB,2BAA2B;AAC5D,SAAS,YAAY;AAErB,SAAS,2BAA2B;AAEpC,SAAS,yBAAyB,wBAAwB;AAY1D,eAAsB,4BACpB,KACqD;AACrD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,iBAAiB,KAAK,cAAc;AAAA,EAC7C;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,MACd;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,wBAAwB,KAAK,SAAS;AAAA,MACtC,iBAAiB,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,MAC7C,SAAS;AAAA,IACX;AAAA,IACA,iBAAiB,UAAU,QAAyB,iBAAiB;AAAA,EACvE;AACF;AAEA,SAAS,wBAAwB,OAA+B;AAC9D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;AAEA,SAAS,0BAA0B,QAAkC;AACnE,MAAI,WAAW,QAAQ,WAAW,OAAW,QAAO;AACpD,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO;AACnC,QAAM,SAAmB,CAAC;AAC1B,aAAW,SAAS,QAAQ;AAC1B,QAAI,OAAO,UAAU,SAAU;AAC/B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,QAAS,QAAO,KAAK,OAAO;AAAA,EAClC;AACA,SAAO;AACT;AAEA,eAAsB,uCACpB,KACA,cACe;AACf,QAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAC3F,MAAI,aAAc;AAElB,QAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,cAAc,WAAW,KAAK;AAAA,IACpC,CAAC;AAAA,IACD,EAAE,UAAU,MAAM,gBAAgB,KAAK;AAAA,EACzC;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAAA,EAC1D;AAEA,QAAM,gBAAgB,wBAAwB,IAAI,KAAK,QAAQ;AAC/D,QAAM,iBAAiB,wBAAyB,OAAwC,QAAQ;AAChG,MAAI,CAAC,kBAAkB,mBAAmB,eAAe;AACvD,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAAA,EAC1D;AAEA,QAAM,cAAc,IAAI,UAAU,QAAqB,aAAa;AACpE,QAAM,MAAM,MAAM,YAAY,QAAQ,IAAI,KAAK,KAAK;AAAA,IAClD,UAAU;AAAA,IACV,gBAAgB,wBAAwB,IAAI,KAAK,KAAK;AAAA,EACxD,CAAC;AACD,QAAM,gBAAgB,0BAA0B,KAAK,aAAa;AAClE,MAAI,kBAAkB,QAAQ,CAAC,cAAc,SAAS,SAAS,GAAG;AAChE,UAAM,uBAAuB,wBAAyB,OAA8C,cAAc;AAClH,QAAI,CAAC,wBAAwB,CAAC,cAAc,SAAS,oBAAoB,GAAG;AAC1E,YAAM,UAAU,qCAAqC;AAAA,IACvD;AAAA,EACF;AACF;AAEA,eAAsB,2BACpB,KACA,mBACwB;AACxB,QAAM,WAAW,MAAM,uBAAuB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,GAAG,iBAAiB;AAC7G,SAAO,YAAY,IAAI,KAAK,YAAY;AAC1C;AAEA,eAAsB,sBAAsB,OAAuC;AACjF,MAAI,iBAAiB,eAAe;AAClC,WAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC9F;AACA,MAAI,oBAAoB,KAAK,GAAG;AAC9B,WAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,WAAW,CAAC;AAAA,EAClG;AACA,MAAI,uBAAuB,KAAK,GAAG;AACjC,WAAO,iBAAiB,MAAM,YAAY,MAAM,OAAO;AAAA,EACzD;AAEA,UAAQ,MAAM,gCAAgC,KAAK;AACnD,SAAO,iBAAiB,KAAK,0CAA0C;AACzE;AAEA,SAAS,uBAAuB,OAA+C;AAC7E,SAAO,iBAAiB,SACnB,MAAM,SAAS,0BACf,OAAQ,MAAwC,eAAe;AACtE;",
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
+ assertActorOwnsTenantScope,
8
+ mapSecurityUsersError,
9
+ resolveSecurityUsersContext
10
+ } from "../../_shared.js";
6
11
  const querySchema = z.object({
7
12
  tenantId: z.string().uuid().optional()
8
13
  });
@@ -30,12 +35,16 @@ async function GET(req) {
30
35
  if (!parsedQuery.success) {
31
36
  return securityApiError(400, "Invalid query parameters", { issues: parsedQuery.error.issues });
32
37
  }
33
- const tenantId = parsedQuery.data.tenantId ?? context.auth.tenantId ?? null;
34
- if (!tenantId) {
35
- return securityApiError(400, "Tenant context is required.");
36
- }
37
38
  try {
38
- const items = await context.mfaAdminService.bulkComplianceCheck(tenantId);
39
+ const tenantId = await assertActorOwnsTenantScope(context, parsedQuery.data.tenantId);
40
+ if (!tenantId) {
41
+ return securityApiError(400, "Tenant context is required.");
42
+ }
43
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container });
44
+ const items = await context.mfaAdminService.bulkComplianceCheck(tenantId, {
45
+ tenantId: context.auth.tenantId ?? null,
46
+ isSuperAdmin
47
+ });
39
48
  return NextResponse.json({ items });
40
49
  } catch (error) {
41
50
  return await mapSecurityUsersError(error);
@@ -52,7 +61,8 @@ const openApi = buildSecurityOpenApi({
52
61
  ],
53
62
  errors: [
54
63
  { status: 400, description: "Invalid query or missing tenant context", schema: securityErrorSchema },
55
- { status: 401, description: "Unauthorized", schema: securityErrorSchema }
64
+ { status: 401, description: "Unauthorized", schema: securityErrorSchema },
65
+ { status: 403, description: "Not authorized for the requested tenant scope", schema: securityErrorSchema }
56
66
  ]
57
67
  }
58
68
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/security/api/users/mfa/compliance/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 querySchema = z.object({\n tenantId: z.string().uuid().optional(),\n})\n\nconst complianceItemSchema = z.object({\n userId: z.string().uuid(),\n email: z.string().email(),\n enrolled: z.boolean(),\n methodCount: z.number().int().nonnegative(),\n compliant: z.boolean(),\n lastLoginAt: z.string().datetime().optional(),\n})\n\nconst complianceListResponseSchema = z.object({\n items: z.array(complianceItemSchema),\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 resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const url = new URL(req.url)\n const parsedQuery = querySchema.safeParse({\n tenantId: url.searchParams.get('tenantId') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n const tenantId = parsedQuery.data.tenantId ?? context.auth.tenantId ?? null\n if (!tenantId) {\n return securityApiError(400, 'Tenant context is required.')\n }\n\n try {\n const items = await context.mfaAdminService.bulkComplianceCheck(tenantId)\n return NextResponse.json({ items })\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin users MFA compliance routes',\n methods: {\n GET: {\n summary: 'List MFA compliance for tenant users',\n query: querySchema,\n responses: [\n { status: 200, description: 'MFA compliance list', schema: complianceListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query or missing tenant context', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', 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,cAAc,EAAE,OAAO;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,QAAQ;AAAA,EACpB,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC1C,WAAW,EAAE,QAAQ;AAAA,EACrB,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC9C,CAAC;AAED,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,OAAO,EAAE,MAAM,oBAAoB;AACrC,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,YAAY,UAAU;AAAA,IACxC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,iBAAiB,KAAK,4BAA4B,EAAE,QAAQ,YAAY,MAAM,OAAO,CAAC;AAAA,EAC/F;AAEA,QAAM,WAAW,YAAY,KAAK,YAAY,QAAQ,KAAK,YAAY;AACvE,MAAI,CAAC,UAAU;AACb,WAAO,iBAAiB,KAAK,6BAA6B;AAAA,EAC5D;AAEA,MAAI;AACF,UAAM,QAAQ,MAAM,QAAQ,gBAAgB,oBAAoB,QAAQ;AACxE,WAAO,aAAa,KAAK,EAAE,MAAM,CAAC;AAAA,EACpC,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,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB,QAAQ,6BAA6B;AAAA,MAC1F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,2CAA2C,QAAQ,oBAAoB;AAAA,QACnG,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 { buildSecurityOpenApi, securityErrorSchema } from '../../../openapi'\nimport { securityApiError } from '../../../i18n'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport {\n assertActorOwnsTenantScope,\n mapSecurityUsersError,\n resolveSecurityUsersContext,\n} from '../../_shared'\n\nconst querySchema = z.object({\n tenantId: z.string().uuid().optional(),\n})\n\nconst complianceItemSchema = z.object({\n userId: z.string().uuid(),\n email: z.string().email(),\n enrolled: z.boolean(),\n methodCount: z.number().int().nonnegative(),\n compliant: z.boolean(),\n lastLoginAt: z.string().datetime().optional(),\n})\n\nconst complianceListResponseSchema = z.object({\n items: z.array(complianceItemSchema),\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 resolveSecurityUsersContext(req)\n if (context instanceof NextResponse) return context\n\n const url = new URL(req.url)\n const parsedQuery = querySchema.safeParse({\n tenantId: url.searchParams.get('tenantId') ?? undefined,\n })\n if (!parsedQuery.success) {\n return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })\n }\n\n try {\n const tenantId = await assertActorOwnsTenantScope(context, parsedQuery.data.tenantId)\n if (!tenantId) {\n return securityApiError(400, 'Tenant context is required.')\n }\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container })\n const items = await context.mfaAdminService.bulkComplianceCheck(tenantId, {\n tenantId: context.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return NextResponse.json({ items })\n } catch (error) {\n return await mapSecurityUsersError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Admin users MFA compliance routes',\n methods: {\n GET: {\n summary: 'List MFA compliance for tenant users',\n query: querySchema,\n responses: [\n { status: 200, description: 'MFA compliance list', schema: complianceListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query or missing tenant context', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n { status: 403, description: 'Not authorized for the requested tenant scope', 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,cAAc,EAAE,OAAO;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,QAAQ;AAAA,EACpB,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC1C,WAAW,EAAE,QAAQ;AAAA,EACrB,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC9C,CAAC;AAED,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,OAAO,EAAE,MAAM,oBAAoB;AACrC,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,UAAU,MAAM,4BAA4B,GAAG;AACrD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,YAAY,UAAU;AAAA,IACxC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,iBAAiB,KAAK,4BAA4B,EAAE,QAAQ,YAAY,MAAM,OAAO,CAAC;AAAA,EAC/F;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,2BAA2B,SAAS,YAAY,KAAK,QAAQ;AACpF,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,6BAA6B;AAAA,IAC5D;AACA,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,QAAQ,MAAM,WAAW,QAAQ,UAAU,CAAC;AACnG,UAAM,QAAQ,MAAM,QAAQ,gBAAgB,oBAAoB,UAAU;AAAA,MACxE,UAAU,QAAQ,KAAK,YAAY;AAAA,MACnC;AAAA,IACF,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,MAAM,CAAC;AAAA,EACpC,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,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB,QAAQ,6BAA6B;AAAA,MAC1F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,2CAA2C,QAAQ,oBAAoB;AAAA,QACnG,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,iDAAiD,QAAQ,oBAAoB;AAAA,MAC3G;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
2
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
3
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
+ import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
4
5
  import { enforcementPolicySchema } from "../data/validators.js";
5
6
  const commandId = "security.enforcement.create";
6
7
  const commandSchema = enforcementPolicySchema;
@@ -20,8 +21,12 @@ registerCommand({
20
21
  throw new CrudHttpError(400, { error: "Invalid payload", issues: parsed.error.issues });
21
22
  }
22
23
  const enforcementService = ctx.container.resolve("mfaEnforcementService");
24
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
23
25
  try {
24
- const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub);
26
+ const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub, {
27
+ tenantId: ctx.auth.tenantId ?? null,
28
+ isSuperAdmin
29
+ });
25
30
  return { id: policy.id };
26
31
  } catch (error) {
27
32
  if (isEnforcementServiceError(error)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/commands/createEnforcementPolicy.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforcementPolicySchema } from '../data/validators'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.create'\n\nconst commandSchema = enforcementPolicySchema\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n try {\n const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub)\n return { id: policy.id }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, result, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n const commandResult = result as { id: string }\n return {\n actionLabel: translate('security.audit.enforcement.create', 'Create enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: commandResult.id,\n tenantId: payload.tenantId ?? null,\n organizationId: payload.organizationId ?? null,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
- "mappings": "AACA,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,+BAA+B;AAGjC,MAAM,YAAY;AAEzB,MAAM,gBAAgB;AAQtB,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,aAAa,OAAO,MAAM,IAAI,KAAK,GAAG;AAC9E,aAAO,EAAE,IAAI,OAAO,GAAG;AAAA,IACzB,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,QAAQ,IAAI,GAAG;AACrC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,UAAM,gBAAgB;AACtB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,cAAc;AAAA,MAC1B,UAAU,QAAQ,YAAY;AAAA,MAC9B,gBAAgB,QAAQ,kBAAkB;AAAA,MAC1C,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { enforcementPolicySchema } from '../data/validators'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.create'\n\nconst commandSchema = enforcementPolicySchema\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n try {\n const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub, {\n tenantId: ctx.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return { id: policy.id }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, result, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n const commandResult = result as { id: string }\n return {\n actionLabel: translate('security.audit.enforcement.create', 'Create enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: commandResult.id,\n tenantId: payload.tenantId ?? null,\n organizationId: payload.organizationId ?? null,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
+ "mappings": "AACA,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,+BAA+B;AAGjC,MAAM,YAAY;AAEzB,MAAM,gBAAgB;AAQtB,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAC3F,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,aAAa,OAAO,MAAM,IAAI,KAAK,KAAK;AAAA,QAC9E,UAAU,IAAI,KAAK,YAAY;AAAA,QAC/B;AAAA,MACF,CAAC;AACD,aAAO,EAAE,IAAI,OAAO,GAAG;AAAA,IACzB,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,QAAQ,IAAI,GAAG;AACrC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,UAAM,gBAAgB;AACtB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,cAAc;AAAA,MAC1B,UAAU,QAAQ,YAAY;AAAA,MAC9B,gBAAgB,QAAQ,kBAAkB;AAAA,MAC1C,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { registerCommand } from "@open-mercato/shared/lib/commands";
3
3
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
5
6
  const commandId = "security.enforcement.delete";
6
7
  const commandSchema = z.object({
7
8
  id: z.string().uuid()
@@ -22,8 +23,12 @@ registerCommand({
22
23
  throw new CrudHttpError(400, { error: "Invalid payload", issues: parsed.error.issues });
23
24
  }
24
25
  const enforcementService = ctx.container.resolve("mfaEnforcementService");
26
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
25
27
  try {
26
- await enforcementService.deletePolicy(parsed.data.id);
28
+ await enforcementService.deletePolicy(parsed.data.id, {
29
+ tenantId: ctx.auth.tenantId ?? null,
30
+ isSuperAdmin
31
+ });
27
32
  return { ok: true };
28
33
  } catch (error) {
29
34
  if (isEnforcementServiceError(error)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/commands/deleteEnforcementPolicy.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.delete'\n\nconst commandSchema = z.object({\n id: z.string().uuid(),\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n try {\n await enforcementService.deletePolicy(parsed.data.id)\n return { ok: true as const }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.enforcement.delete', 'Delete enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: payload.id,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AAG7B,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAQD,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,QAAI;AACF,YAAM,mBAAmB,aAAa,OAAO,KAAK,EAAE;AACpD,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.delete'\n\nconst commandSchema = z.object({\n id: z.string().uuid(),\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n try {\n await enforcementService.deletePolicy(parsed.data.id, {\n tenantId: ctx.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return { ok: true as const }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.enforcement.delete', 'Delete enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: payload.id,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAG7B,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAQD,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAC3F,QAAI;AACF,YAAM,mBAAmB,aAAa,OAAO,KAAK,IAAI;AAAA,QACpD,UAAU,IAAI,KAAK,YAAY;AAAA,QAC/B;AAAA,MACF,CAAC;AACD,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { registerCommand } from "@open-mercato/shared/lib/commands";
3
3
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
5
6
  const commandId = "security.admin.mfa.reset";
6
7
  const commandSchema = z.object({
7
8
  userId: z.string().uuid(),
@@ -23,8 +24,12 @@ registerCommand({
23
24
  throw new CrudHttpError(400, { error: "Invalid payload", issues: parsed.error.issues });
24
25
  }
25
26
  const mfaAdminService = ctx.container.resolve("mfaAdminService");
27
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
26
28
  try {
27
- await mfaAdminService.resetUserMfa(ctx.auth.sub, parsed.data.userId, parsed.data.reason);
29
+ await mfaAdminService.resetUserMfa(ctx.auth.sub, parsed.data.userId, parsed.data.reason, {
30
+ tenantId: ctx.auth.tenantId ?? null,
31
+ isSuperAdmin
32
+ });
28
33
  return { ok: true };
29
34
  } catch (error) {
30
35
  if (isMfaAdminServiceError(error)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/commands/resetUserMfa.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { MfaAdminService } from '../services/MfaAdminService'\n\nexport const commandId = 'security.admin.mfa.reset'\n\nconst commandSchema = z.object({\n userId: z.string().uuid(),\n reason: z.string().min(1),\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype MfaAdminServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<MfaAdminServiceErrorLike>\n return error.name === 'MfaAdminServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const mfaAdminService = ctx.container.resolve<MfaAdminService>('mfaAdminService')\n try {\n await mfaAdminService.resetUserMfa(ctx.auth.sub, parsed.data.userId, parsed.data.reason)\n return { ok: true as const }\n } catch (error) {\n if (isMfaAdminServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.mfa.reset', 'Reset user MFA'),\n resourceKind: 'security.user_mfa',\n resourceId: payload.userId,\n actorUserId: ctx.auth?.sub ?? null,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n payload,\n context: {\n source: 'security.admin.users',\n },\n }\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AAG7B,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAC1B,CAAC;AAQD,SAAS,uBAAuB,OAAmD;AACjF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,0BAA0B,OAAO,MAAM,eAAe;AAC9E;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,kBAAkB,IAAI,UAAU,QAAyB,iBAAiB;AAChF,QAAI;AACF,YAAM,gBAAgB,aAAa,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,MAAM;AACvF,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,uBAAuB,KAAK,GAAG;AACjC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,4BAA4B,gBAAgB;AAAA,MACnE,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,MAAM,SAAS;AAAA,MACnC;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { MfaAdminService } from '../services/MfaAdminService'\n\nexport const commandId = 'security.admin.mfa.reset'\n\nconst commandSchema = z.object({\n userId: z.string().uuid(),\n reason: z.string().min(1),\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype MfaAdminServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<MfaAdminServiceErrorLike>\n return error.name === 'MfaAdminServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const mfaAdminService = ctx.container.resolve<MfaAdminService>('mfaAdminService')\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n try {\n await mfaAdminService.resetUserMfa(ctx.auth.sub, parsed.data.userId, parsed.data.reason, {\n tenantId: ctx.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return { ok: true as const }\n } catch (error) {\n if (isMfaAdminServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.mfa.reset', 'Reset user MFA'),\n resourceKind: 'security.user_mfa',\n resourceId: payload.userId,\n actorUserId: ctx.auth?.sub ?? null,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n payload,\n context: {\n source: 'security.admin.users',\n },\n }\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAG7B,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAC1B,CAAC;AAQD,SAAS,uBAAuB,OAAmD;AACjF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,0BAA0B,OAAO,MAAM,eAAe;AAC9E;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,kBAAkB,IAAI,UAAU,QAAyB,iBAAiB;AAChF,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAC3F,QAAI;AACF,YAAM,gBAAgB,aAAa,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,QAAQ;AAAA,QACvF,UAAU,IAAI,KAAK,YAAY;AAAA,QAC/B;AAAA,MACF,CAAC;AACD,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,uBAAuB,KAAK,GAAG;AACjC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,4BAA4B,gBAAgB;AAAA,MACnE,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,MAAM,SAAS;AAAA,MACnC;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { registerCommand } from "@open-mercato/shared/lib/commands";
3
3
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
5
6
  import { updateEnforcementPolicySchema } from "../data/validators.js";
6
7
  const commandId = "security.enforcement.update";
7
8
  const commandSchema = z.object({
@@ -24,8 +25,12 @@ registerCommand({
24
25
  throw new CrudHttpError(400, { error: "Invalid payload", issues: parsed.error.issues });
25
26
  }
26
27
  const enforcementService = ctx.container.resolve("mfaEnforcementService");
28
+ const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container });
27
29
  try {
28
- await enforcementService.updatePolicy(parsed.data.id, parsed.data.data, ctx.auth.sub);
30
+ await enforcementService.updatePolicy(parsed.data.id, parsed.data.data, ctx.auth.sub, {
31
+ tenantId: ctx.auth.tenantId ?? null,
32
+ isSuperAdmin
33
+ });
29
34
  return { ok: true };
30
35
  } catch (error) {
31
36
  if (isEnforcementServiceError(error)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/commands/updateEnforcementPolicy.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { updateEnforcementPolicySchema } from '../data/validators'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.update'\n\nconst commandSchema = z.object({\n id: z.string().uuid(),\n data: updateEnforcementPolicySchema,\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n try {\n await enforcementService.updatePolicy(parsed.data.id, parsed.data.data, ctx.auth.sub)\n return { ok: true as const }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.enforcement.update', 'Update enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: payload.id,\n tenantId: payload.data.tenantId ?? null,\n organizationId: payload.data.organizationId ?? null,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,qCAAqC;AAGvC,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM;AACR,CAAC;AAQD,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,QAAI;AACF,YAAM,mBAAmB,aAAa,OAAO,KAAK,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;AACpF,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,UAAU,QAAQ,KAAK,YAAY;AAAA,MACnC,gBAAgB,QAAQ,KAAK,kBAAkB;AAAA,MAC/C,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { updateEnforcementPolicySchema } from '../data/validators'\nimport type { MfaEnforcementService } from '../services/MfaEnforcementService'\n\nexport const commandId = 'security.enforcement.update'\n\nconst commandSchema = z.object({\n id: z.string().uuid(),\n data: updateEnforcementPolicySchema,\n})\n\ntype CommandInput = z.infer<typeof commandSchema>\n\ntype EnforcementServiceErrorLike = Error & {\n statusCode: number\n}\n\nfunction isEnforcementServiceError(error: unknown): error is EnforcementServiceErrorLike {\n if (!(error instanceof Error)) return false\n const maybe = error as Partial<EnforcementServiceErrorLike>\n return error.name === 'MfaEnforcementServiceError' && typeof maybe.statusCode === 'number'\n}\n\nregisterCommand({\n id: commandId,\n async execute(rawInput, ctx) {\n if (!ctx.auth?.sub) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const parsed = commandSchema.safeParse(rawInput)\n if (!parsed.success) {\n throw new CrudHttpError(400, { error: 'Invalid payload', issues: parsed.error.issues })\n }\n\n const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n try {\n await enforcementService.updatePolicy(parsed.data.id, parsed.data.data, ctx.auth.sub, {\n tenantId: ctx.auth.tenantId ?? null,\n isSuperAdmin,\n })\n return { ok: true as const }\n } catch (error) {\n if (isEnforcementServiceError(error)) {\n throw new CrudHttpError(error.statusCode, { error: error.message })\n }\n throw error\n }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n const payload = input as CommandInput\n return {\n actionLabel: translate('security.audit.enforcement.update', 'Update enforcement policy'),\n resourceKind: 'security.enforcement_policy',\n resourceId: payload.id,\n tenantId: payload.data.tenantId ?? null,\n organizationId: payload.data.organizationId ?? null,\n actorUserId: ctx.auth?.sub ?? null,\n payload,\n context: {\n source: 'security.enforcement',\n },\n }\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,qCAAqC;AAGvC,MAAM,YAAY;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM;AACR,CAAC;AAQD,SAAS,0BAA0B,OAAsD;AACvF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ;AACd,SAAO,MAAM,SAAS,gCAAgC,OAAO,MAAM,eAAe;AACpF;AAEA,gBAAgB;AAAA,EACd,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,QAAI,CAAC,IAAI,MAAM,KAAK;AAClB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,IACxD;AAEA,UAAM,SAAS,cAAc,UAAU,QAAQ;AAC/C,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,mBAAmB,QAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACxF;AAEA,UAAM,qBAAqB,IAAI,UAAU,QAA+B,uBAAuB;AAC/F,UAAM,eAAe,MAAM,oBAAoB,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAC3F,QAAI;AACF,YAAM,mBAAmB,aAAa,OAAO,KAAK,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,KAAK;AAAA,QACpF,UAAU,IAAI,KAAK,YAAY;AAAA,QAC/B;AAAA,MACF,CAAC;AACD,aAAO,EAAE,IAAI,KAAc;AAAA,IAC7B,SAAS,OAAO;AACd,UAAI,0BAA0B,KAAK,GAAG;AACpC,cAAM,IAAI,cAAc,MAAM,YAAY,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,MACpE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,EAAE,OAAO,IAAI,GAAG;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU;AAChB,WAAO;AAAA,MACL,aAAa,UAAU,qCAAqC,2BAA2B;AAAA,MACvF,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,UAAU,QAAQ,KAAK,YAAY;AAAA,MACnC,gBAAgB,QAAQ,KAAK,kBAAkB;AAAA,MAC/C,aAAa,IAAI,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { User } from "@open-mercato/core/modules/auth/data/entities";
2
- import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
3
  import { MfaRecoveryCode, UserMfaMethod } from "../data/entities.js";
4
4
  import { emitSecurityEvent } from "../events.js";
5
5
  class MfaAdminServiceError extends Error {
@@ -14,7 +14,7 @@ class MfaAdminService {
14
14
  this.em = em;
15
15
  this.mfaEnforcementService = mfaEnforcementService;
16
16
  }
17
- async resetUserMfa(adminId, userId, reason) {
17
+ async resetUserMfa(adminId, userId, reason, actor) {
18
18
  if (!adminId.trim()) {
19
19
  throw new MfaAdminServiceError("Admin ID is required", 400);
20
20
  }
@@ -29,6 +29,7 @@ class MfaAdminService {
29
29
  if (!user) {
30
30
  throw new MfaAdminServiceError("User not found", 404);
31
31
  }
32
+ this.assertActorOwnsUser(user, actor);
32
33
  const activeMethods = await this.em.find(UserMfaMethod, {
33
34
  userId,
34
35
  isActive: true,
@@ -60,7 +61,7 @@ class MfaAdminService {
60
61
  resetAt: (/* @__PURE__ */ new Date()).toISOString()
61
62
  });
62
63
  }
63
- async getUserMfaStatus(userId) {
64
+ async getUserMfaStatus(userId, actor) {
64
65
  if (!userId.trim()) {
65
66
  throw new MfaAdminServiceError("User ID is required", 400);
66
67
  }
@@ -68,6 +69,7 @@ class MfaAdminService {
68
69
  if (!user) {
69
70
  throw new MfaAdminServiceError("User not found", 404);
70
71
  }
72
+ this.assertActorOwnsUser(user, actor);
71
73
  const methods = await this.em.find(
72
74
  UserMfaMethod,
73
75
  {
@@ -95,10 +97,13 @@ class MfaAdminService {
95
97
  compliant: compliance.compliant
96
98
  };
97
99
  }
98
- async bulkComplianceCheck(tenantId) {
100
+ async bulkComplianceCheck(tenantId, actor) {
99
101
  if (!tenantId.trim()) {
100
102
  throw new MfaAdminServiceError("Tenant ID is required", 400);
101
103
  }
104
+ if (actor && !actor.isSuperAdmin && tenantId !== actor.tenantId) {
105
+ throw new MfaAdminServiceError("Not authorized for the requested tenant scope.", 403);
106
+ }
102
107
  const users = await findWithDecryption(
103
108
  this.em,
104
109
  User,
@@ -138,8 +143,20 @@ class MfaAdminService {
138
143
  };
139
144
  });
140
145
  }
146
+ assertActorOwnsUser(user, actor) {
147
+ if (!actor || actor.isSuperAdmin) return;
148
+ if (!user.tenantId || user.tenantId !== actor.tenantId) {
149
+ throw new MfaAdminServiceError("User not found", 404);
150
+ }
151
+ }
141
152
  async findUserById(userId) {
142
- return this.em.findOne(User, { id: userId, deletedAt: null });
153
+ return findOneWithDecryption(
154
+ this.em,
155
+ User,
156
+ { id: userId, deletedAt: null },
157
+ {},
158
+ { tenantId: null, organizationId: null }
159
+ );
143
160
  }
144
161
  }
145
162
  var MfaAdminService_default = MfaAdminService;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/MfaAdminService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n async resetUserMfa(adminId: string, userId: string, reason: string): Promise<void> {\n if (!adminId.trim()) {\n throw new MfaAdminServiceError('Admin ID is required', 400)\n }\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const normalizedReason = reason.trim()\n if (!normalizedReason) {\n throw new MfaAdminServiceError('Reset reason is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n\n const activeMethods = await this.em.find(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n })\n const activeRecoveryCodes = await this.em.find(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const now = new Date()\n for (const method of activeMethods) {\n method.isActive = false\n method.deletedAt = now\n method.updatedAt = now\n }\n for (const recoveryCode of activeRecoveryCodes) {\n recoveryCode.isUsed = true\n recoveryCode.usedAt = now\n }\n await this.em.flush()\n\n await emitSecurityEvent('security.mfa.reset', {\n adminId,\n targetUserId: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n reason: normalizedReason,\n methodCount: activeMethods.length,\n recoveryCodesInvalidated: activeRecoveryCodes.length,\n resetAt: new Date().toISOString(),\n })\n }\n\n async getUserMfaStatus(userId: string): Promise<UserMfaStatus> {\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'desc' },\n },\n )\n\n const recoveryCodesRemaining = await this.em.count(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const compliance = await this.mfaEnforcementService.checkUserCompliance(userId)\n\n return {\n enrolled: methods.length > 0,\n methods: methods.map((method) => ({\n type: method.type,\n ...(method.label ? { label: method.label } : {}),\n ...(method.lastUsedAt ? { lastUsed: method.lastUsedAt } : {}),\n })),\n recoveryCodesRemaining,\n compliant: compliance.compliant,\n }\n }\n\n async bulkComplianceCheck(tenantId: string): Promise<BulkComplianceStatus[]> {\n if (!tenantId.trim()) {\n throw new MfaAdminServiceError('Tenant ID is required', 400)\n }\n\n const users = await findWithDecryption(\n this.em,\n User,\n {\n tenantId,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n { tenantId, organizationId: null },\n )\n\n const userIds = users.map((user) => user.id)\n const activeMethods = userIds.length\n ? await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n })\n : []\n const methodCountByUserId = new Map<string, number>()\n for (const method of activeMethods) {\n const currentCount = methodCountByUserId.get(method.userId) ?? 0\n methodCountByUserId.set(method.userId, currentCount + 1)\n }\n\n const complianceResults = await Promise.all(\n users.map((user) => this.mfaEnforcementService.checkUserCompliance(user.id)),\n )\n\n return users.map((user, index) => {\n const methodCount = methodCountByUserId.get(user.id) ?? 0\n const compliance = complianceResults[index]\n return {\n userId: user.id,\n email: user.email,\n enrolled: methodCount > 0,\n methodCount,\n compliant: compliance.compliant,\n ...(user.lastLoginAt ? { lastLoginAt: user.lastLoginAt } : {}),\n }\n })\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return this.em.findOne(User, { id: userId, deletedAt: null })\n }\n}\n\nexport default MfaAdminService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\ntype ActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n async resetUserMfa(adminId: string, userId: string, reason: string, actor?: ActorContext): Promise<void> {\n if (!adminId.trim()) {\n throw new MfaAdminServiceError('Admin ID is required', 400)\n }\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const normalizedReason = reason.trim()\n if (!normalizedReason) {\n throw new MfaAdminServiceError('Reset reason is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const activeMethods = await this.em.find(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n })\n const activeRecoveryCodes = await this.em.find(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const now = new Date()\n for (const method of activeMethods) {\n method.isActive = false\n method.deletedAt = now\n method.updatedAt = now\n }\n for (const recoveryCode of activeRecoveryCodes) {\n recoveryCode.isUsed = true\n recoveryCode.usedAt = now\n }\n await this.em.flush()\n\n await emitSecurityEvent('security.mfa.reset', {\n adminId,\n targetUserId: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n reason: normalizedReason,\n methodCount: activeMethods.length,\n recoveryCodesInvalidated: activeRecoveryCodes.length,\n resetAt: new Date().toISOString(),\n })\n }\n\n async getUserMfaStatus(userId: string, actor?: ActorContext): Promise<UserMfaStatus> {\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'desc' },\n },\n )\n\n const recoveryCodesRemaining = await this.em.count(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const compliance = await this.mfaEnforcementService.checkUserCompliance(userId)\n\n return {\n enrolled: methods.length > 0,\n methods: methods.map((method) => ({\n type: method.type,\n ...(method.label ? { label: method.label } : {}),\n ...(method.lastUsedAt ? { lastUsed: method.lastUsedAt } : {}),\n })),\n recoveryCodesRemaining,\n compliant: compliance.compliant,\n }\n }\n\n async bulkComplianceCheck(tenantId: string, actor?: ActorContext): Promise<BulkComplianceStatus[]> {\n if (!tenantId.trim()) {\n throw new MfaAdminServiceError('Tenant ID is required', 400)\n }\n if (actor && !actor.isSuperAdmin && tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('Not authorized for the requested tenant scope.', 403)\n }\n\n const users = await findWithDecryption(\n this.em,\n User,\n {\n tenantId,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n { tenantId, organizationId: null },\n )\n\n const userIds = users.map((user) => user.id)\n const activeMethods = userIds.length\n ? await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n })\n : []\n const methodCountByUserId = new Map<string, number>()\n for (const method of activeMethods) {\n const currentCount = methodCountByUserId.get(method.userId) ?? 0\n methodCountByUserId.set(method.userId, currentCount + 1)\n }\n\n const complianceResults = await Promise.all(\n users.map((user) => this.mfaEnforcementService.checkUserCompliance(user.id)),\n )\n\n return users.map((user, index) => {\n const methodCount = methodCountByUserId.get(user.id) ?? 0\n const compliance = complianceResults[index]\n return {\n userId: user.id,\n email: user.email,\n enrolled: methodCount > 0,\n methodCount,\n compliant: compliance.compliant,\n ...(user.lastLoginAt ? { lastLoginAt: user.lastLoginAt } : {}),\n }\n })\n }\n\n private assertActorOwnsUser(user: User, actor?: ActorContext): void {\n if (!actor || actor.isSuperAdmin) return\n if (!user.tenantId || user.tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n }\n}\n\nexport default MfaAdminService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -30,20 +30,26 @@ class MfaEnforcementService {
30
30
  }
31
31
  return { enforced: true, policy };
32
32
  }
33
- async listPolicies(filters) {
33
+ async getPolicyById(id) {
34
+ return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null });
35
+ }
36
+ async listPolicies(filters, actor) {
37
+ const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {};
34
38
  return this.em.find(
35
39
  MfaEnforcementPolicy,
36
40
  {
37
41
  deletedAt: null,
38
- ...filters?.scope ? { scope: filters.scope } : {}
42
+ ...filters?.scope ? { scope: filters.scope } : {},
43
+ ...tenantConstraint
39
44
  },
40
45
  {
41
46
  orderBy: { updatedAt: "desc" }
42
47
  }
43
48
  );
44
49
  }
45
- async getComplianceReport(scope, scopeId) {
50
+ async getComplianceReport(scope, scopeId, actor) {
46
51
  const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId);
52
+ this.assertActorOwnsScopeFilters(actor, scope, tenantId);
47
53
  const users = await this.em.find(User, {
48
54
  deletedAt: null,
49
55
  ...tenantId ? { tenantId } : {},
@@ -75,8 +81,9 @@ class MfaEnforcementService {
75
81
  overdue
76
82
  };
77
83
  }
78
- async createPolicy(data, adminId) {
84
+ async createPolicy(data, adminId, actor) {
79
85
  const normalized = this.normalizePolicyInput(data);
86
+ this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId);
80
87
  const existing = await this.findPolicyByScope(
81
88
  normalized.scope,
82
89
  normalized.tenantId ?? void 0,
@@ -119,7 +126,7 @@ class MfaEnforcementService {
119
126
  await this.emitDeadlineReminderRequest(policy.id);
120
127
  return policy;
121
128
  }
122
- async updatePolicy(id, data, adminId) {
129
+ async updatePolicy(id, data, adminId, actor) {
123
130
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
124
131
  id,
125
132
  deletedAt: null
@@ -127,6 +134,7 @@ class MfaEnforcementService {
127
134
  if (!policy) {
128
135
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
129
136
  }
137
+ this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
130
138
  const mergedInput = this.normalizePolicyInput({
131
139
  scope: data.scope ?? policy.scope,
132
140
  tenantId: data.tenantId ?? policy.tenantId ?? void 0,
@@ -135,6 +143,7 @@ class MfaEnforcementService {
135
143
  allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,
136
144
  enforcementDeadline: data.enforcementDeadline === void 0 ? policy.enforcementDeadline ?? null : data.enforcementDeadline
137
145
  });
146
+ this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId);
138
147
  if (mergedInput.scope !== policy.scope || mergedInput.tenantId !== (policy.tenantId ?? null) || mergedInput.organizationId !== (policy.organizationId ?? null)) {
139
148
  const conflict = await this.findPolicyByScope(
140
149
  mergedInput.scope,
@@ -162,7 +171,7 @@ class MfaEnforcementService {
162
171
  await this.emitDeadlineReminderRequest(policy.id);
163
172
  return policy;
164
173
  }
165
- async deletePolicy(id) {
174
+ async deletePolicy(id, actor) {
166
175
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
167
176
  id,
168
177
  deletedAt: null
@@ -170,6 +179,7 @@ class MfaEnforcementService {
170
179
  if (!policy) {
171
180
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
172
181
  }
182
+ this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
173
183
  const now = /* @__PURE__ */ new Date();
174
184
  policy.deletedAt = now;
175
185
  policy.updatedAt = now;
@@ -227,6 +237,18 @@ class MfaEnforcementService {
227
237
  }
228
238
  );
229
239
  }
240
+ assertActorOwnsScopeFilters(actor, scope, tenantId) {
241
+ if (!actor || actor.isSuperAdmin) return;
242
+ if (scope === EnforcementScope.PLATFORM) {
243
+ throw new MfaEnforcementServiceError(
244
+ "Platform scope requires platform administrator privileges.",
245
+ 403
246
+ );
247
+ }
248
+ if (!tenantId || tenantId !== actor.tenantId) {
249
+ throw new MfaEnforcementServiceError("Not authorized for the requested scope.", 403);
250
+ }
251
+ }
230
252
  resolveScopeFilters(scope, scopeId) {
231
253
  if (scope === EnforcementScope.PLATFORM) {
232
254
  return {};