@open-mercato/enterprise 0.6.5-develop.4882.1.901c3aa813 → 0.6.5-develop.5033.1.c970204a3f

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 (58) hide show
  1. package/dist/modules/security/api/enforcement/[id]/route.js +35 -1
  2. package/dist/modules/security/api/enforcement/[id]/route.js.map +2 -2
  3. package/dist/modules/security/api/enforcement/_shared.js +63 -1
  4. package/dist/modules/security/api/enforcement/_shared.js.map +3 -3
  5. package/dist/modules/security/api/enforcement/compliance/route.js +12 -3
  6. package/dist/modules/security/api/enforcement/compliance/route.js.map +2 -2
  7. package/dist/modules/security/api/enforcement/route.js +25 -2
  8. package/dist/modules/security/api/enforcement/route.js.map +2 -2
  9. package/dist/modules/security/api/mfa/prepare/route.js +1 -1
  10. package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
  11. package/dist/modules/security/api/mfa/recovery/route.js +1 -1
  12. package/dist/modules/security/api/mfa/recovery/route.js.map +2 -2
  13. package/dist/modules/security/api/mfa/verify/route.js +1 -1
  14. package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
  15. package/dist/modules/security/api/users/[id]/mfa/reset/route.js +6 -1
  16. package/dist/modules/security/api/users/[id]/mfa/reset/route.js.map +2 -2
  17. package/dist/modules/security/api/users/[id]/mfa/status/route.js +13 -2
  18. package/dist/modules/security/api/users/[id]/mfa/status/route.js.map +2 -2
  19. package/dist/modules/security/api/users/_shared.js +56 -1
  20. package/dist/modules/security/api/users/_shared.js.map +2 -2
  21. package/dist/modules/security/api/users/mfa/compliance/route.js +17 -7
  22. package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
  23. package/dist/modules/security/commands/createEnforcementPolicy.js +6 -1
  24. package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
  25. package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -1
  26. package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
  27. package/dist/modules/security/commands/resetUserMfa.js +6 -1
  28. package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
  29. package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -1
  30. package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
  31. package/dist/modules/security/services/MfaAdminService.js +22 -5
  32. package/dist/modules/security/services/MfaAdminService.js.map +2 -2
  33. package/dist/modules/security/services/MfaEnforcementService.js +28 -6
  34. package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
  35. package/dist/modules/security/services/MfaVerificationService.js +30 -10
  36. package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
  37. package/dist/modules/security/services/SudoChallengeService.js +14 -3
  38. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  39. package/package.json +5 -5
  40. package/src/modules/security/api/enforcement/[id]/route.ts +50 -1
  41. package/src/modules/security/api/enforcement/_shared.ts +83 -2
  42. package/src/modules/security/api/enforcement/compliance/route.ts +10 -1
  43. package/src/modules/security/api/enforcement/route.ts +30 -2
  44. package/src/modules/security/api/mfa/prepare/route.ts +1 -1
  45. package/src/modules/security/api/mfa/recovery/route.ts +1 -1
  46. package/src/modules/security/api/mfa/verify/route.ts +1 -1
  47. package/src/modules/security/api/users/[id]/mfa/reset/route.ts +6 -1
  48. package/src/modules/security/api/users/[id]/mfa/status/route.ts +13 -2
  49. package/src/modules/security/api/users/_shared.ts +69 -1
  50. package/src/modules/security/api/users/mfa/compliance/route.ts +16 -7
  51. package/src/modules/security/commands/createEnforcementPolicy.ts +6 -1
  52. package/src/modules/security/commands/deleteEnforcementPolicy.ts +6 -1
  53. package/src/modules/security/commands/resetUserMfa.ts +6 -1
  54. package/src/modules/security/commands/updateEnforcementPolicy.ts +6 -1
  55. package/src/modules/security/services/MfaAdminService.ts +29 -6
  56. package/src/modules/security/services/MfaEnforcementService.ts +42 -2
  57. package/src/modules/security/services/MfaVerificationService.ts +33 -10
  58. package/src/modules/security/services/SudoChallengeService.ts +16 -11
@@ -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
  }
@@ -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": "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",
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
  }