@open-mercato/core 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6784.1.f80b9afce5

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 (162) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +3 -3
  3. package/agentic/standalone-guide.md +13 -0
  4. package/dist/generated/entities/organization/index.js +2 -0
  5. package/dist/generated/entities/organization/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +1 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/cli.js +1 -1
  9. package/dist/modules/auth/cli.js.map +2 -2
  10. package/dist/modules/auth/lib/backendChrome.js +4 -6
  11. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  12. package/dist/modules/auth/lib/setup-app.js +1 -0
  13. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  14. package/dist/modules/auth/services/rbacService.js +29 -44
  15. package/dist/modules/auth/services/rbacService.js.map +2 -2
  16. package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
  17. package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
  18. package/dist/modules/communication_channels/lib/access-control.js +2 -2
  19. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  20. package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
  21. package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
  22. package/dist/modules/customer_accounts/api/login.js +4 -2
  23. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  24. package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
  25. package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
  27. package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
  29. package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
  31. package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
  32. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
  33. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
  34. package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
  35. package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
  36. package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
  37. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  38. package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
  39. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  40. package/dist/modules/customer_accounts/setup.js +6 -0
  41. package/dist/modules/customer_accounts/setup.js.map +2 -2
  42. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
  43. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
  44. package/dist/modules/customers/lib/visibilityFilter.js +6 -4
  45. package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
  46. package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
  47. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  48. package/dist/modules/dashboards/api/layout/route.js +22 -9
  49. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  50. package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
  51. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  52. package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
  53. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  54. package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
  55. package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
  56. package/dist/modules/dashboards/lib/access.js +5 -3
  57. package/dist/modules/dashboards/lib/access.js.map +2 -2
  58. package/dist/modules/directory/api/organization-branding/route.js +10 -3
  59. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  60. package/dist/modules/directory/api/organizations/route.js +7 -0
  61. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  62. package/dist/modules/directory/backend/directory/branding/page.js +31 -8
  63. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  64. package/dist/modules/directory/commands/organizations.js +8 -1
  65. package/dist/modules/directory/commands/organizations.js.map +2 -2
  66. package/dist/modules/directory/data/entities.js +4 -0
  67. package/dist/modules/directory/data/entities.js.map +2 -2
  68. package/dist/modules/directory/data/validators.js +2 -0
  69. package/dist/modules/directory/data/validators.js.map +2 -2
  70. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
  71. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
  72. package/dist/modules/entities/lib/entityAcl.js +47 -15
  73. package/dist/modules/entities/lib/entityAcl.js.map +3 -3
  74. package/dist/modules/inbox_ops/ai-tools.js +5 -2
  75. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  76. package/dist/modules/messages/lib/routeHelpers.js +1 -3
  77. package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
  78. package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
  79. package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
  80. package/dist/modules/query_index/di.js +3 -0
  81. package/dist/modules/query_index/di.js.map +2 -2
  82. package/dist/modules/query_index/lib/coverage.js +2 -2
  83. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  84. package/dist/modules/query_index/lib/indexer.js +83 -17
  85. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  86. package/dist/modules/query_index/lib/search-tokens.js +13 -7
  87. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  88. package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
  89. package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
  90. package/dist/modules/query_index/subscribers/delete_one.js +53 -1
  91. package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
  92. package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
  93. package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
  94. package/dist/modules/sales/commands/documents.js +17 -0
  95. package/dist/modules/sales/commands/documents.js.map +2 -2
  96. package/dist/modules/staff/api/interceptors.js +5 -3
  97. package/dist/modules/staff/api/interceptors.js.map +2 -2
  98. package/dist/modules/sync_excel/api/upload/route.js +15 -2
  99. package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
  100. package/dist/modules/workflows/lib/activity-executor.js +7 -7
  101. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  102. package/generated/entities/organization/index.ts +1 -0
  103. package/generated/entity-fields-registry.ts +1 -0
  104. package/package.json +7 -7
  105. package/src/modules/auth/AGENTS.md +5 -3
  106. package/src/modules/auth/cli.ts +1 -1
  107. package/src/modules/auth/lib/backendChrome.tsx +5 -11
  108. package/src/modules/auth/lib/setup-app.ts +1 -0
  109. package/src/modules/auth/services/rbacService.ts +41 -46
  110. package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
  111. package/src/modules/communication_channels/lib/access-control.ts +3 -3
  112. package/src/modules/customer_accounts/AGENTS.md +3 -3
  113. package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
  114. package/src/modules/customer_accounts/api/login.ts +4 -2
  115. package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
  116. package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
  117. package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
  118. package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
  119. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
  120. package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
  121. package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
  122. package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
  123. package/src/modules/customer_accounts/setup.ts +7 -0
  124. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
  125. package/src/modules/customers/lib/visibilityFilter.ts +7 -5
  126. package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
  127. package/src/modules/dashboards/api/layout/route.ts +22 -9
  128. package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
  129. package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
  130. package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
  131. package/src/modules/dashboards/lib/access.ts +5 -3
  132. package/src/modules/directory/api/organization-branding/route.ts +12 -1
  133. package/src/modules/directory/api/organizations/route.ts +7 -0
  134. package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
  135. package/src/modules/directory/commands/organizations.ts +9 -1
  136. package/src/modules/directory/data/entities.ts +3 -0
  137. package/src/modules/directory/data/validators.ts +2 -0
  138. package/src/modules/directory/i18n/de.json +3 -1
  139. package/src/modules/directory/i18n/en.json +3 -1
  140. package/src/modules/directory/i18n/es.json +3 -1
  141. package/src/modules/directory/i18n/pl.json +3 -1
  142. package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
  143. package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
  144. package/src/modules/entities/lib/entityAcl.ts +47 -18
  145. package/src/modules/inbox_ops/ai-tools.ts +5 -3
  146. package/src/modules/messages/lib/routeHelpers.ts +4 -6
  147. package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
  148. package/src/modules/query_index/di.ts +6 -0
  149. package/src/modules/query_index/lib/coverage.ts +9 -6
  150. package/src/modules/query_index/lib/indexer.ts +105 -19
  151. package/src/modules/query_index/lib/search-tokens.ts +19 -8
  152. package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
  153. package/src/modules/query_index/subscribers/delete_one.ts +65 -1
  154. package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
  155. package/src/modules/sales/commands/documents.ts +27 -0
  156. package/src/modules/sales/i18n/de.json +1 -0
  157. package/src/modules/sales/i18n/en.json +1 -0
  158. package/src/modules/sales/i18n/es.json +1 -0
  159. package/src/modules/sales/i18n/pl.json +1 -0
  160. package/src/modules/staff/api/interceptors.ts +6 -4
  161. package/src/modules/sync_excel/api/upload/route.ts +15 -2
  162. package/src/modules/workflows/lib/activity-executor.ts +12 -10
@@ -3,7 +3,10 @@ import {
3
3
  CustomerRoleAcl,
4
4
  CustomerUserRole
5
5
  } from "@open-mercato/core/modules/customer_accounts/data/entities";
6
- import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
6
+ import {
7
+ authorizeFeatures,
8
+ resolveEffectiveFeatures
9
+ } from "@open-mercato/shared/security/featurePolicy";
7
10
  function isCustomerAclData(value) {
8
11
  if (typeof value !== "object" || value === null) return false;
9
12
  const record = value;
@@ -92,8 +95,14 @@ class CustomerRbacService {
92
95
  async userHasAllFeatures(userId, required, scope) {
93
96
  if (!required.length) return true;
94
97
  const acl = await this.loadAcl(userId, scope);
95
- if (acl.isPortalAdmin) return true;
96
- return hasAllFeatures(required, acl.features);
98
+ return authorizeFeatures(required, {
99
+ grantedFeatures: acl.features,
100
+ unrestricted: acl.isPortalAdmin
101
+ });
102
+ }
103
+ async getEffectiveFeatures(userId, scope) {
104
+ const acl = await this.loadAcl(userId, scope);
105
+ return resolveEffectiveFeatures(acl.isPortalAdmin ? ["portal.*"] : acl.features);
97
106
  }
98
107
  async invalidateUserCache(userId) {
99
108
  if (!this.cache) return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/customerRbacService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport {\n CustomerUserAcl,\n CustomerRoleAcl,\n CustomerUserRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\n\ninterface CustomerAclData {\n isPortalAdmin: boolean\n features: string[]\n}\n\nfunction isCustomerAclData(value: unknown): value is CustomerAclData {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<CustomerAclData>\n if (typeof record.isPortalAdmin !== 'boolean') return false\n if (!Array.isArray(record.features) || record.features.some((f) => typeof f !== 'string')) return false\n return true\n}\n\nexport class CustomerRbacService {\n private cacheTtlMs: number = 5 * 60 * 1000\n private cache: CacheStrategy | null = null\n\n constructor(private em: EntityManager, cache?: CacheStrategy) {\n this.cache = cache || null\n }\n\n private getCacheKey(userId: string, scope: { tenantId: string; organizationId: string }): string {\n // Keep the authorization query versioned in the key so cache entries created\n // before scope hardening cannot preserve grants that are no longer valid.\n return `customer_rbac:v2:${userId}:${scope.tenantId}:${scope.organizationId}`\n }\n\n private getUserTag(userId: string): string {\n return `customer_rbac:user:${userId}`\n }\n\n private getTenantTag(tenantId: string): string {\n return `customer_rbac:tenant:${tenantId}`\n }\n\n private async getFromCache(cacheKey: string): Promise<CustomerAclData | null> {\n if (!this.cache) return null\n const cached = await this.cache.get(cacheKey)\n if (!cached) return null\n return isCustomerAclData(cached) ? cached : null\n }\n\n private async setCache(\n cacheKey: string,\n data: CustomerAclData,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<void> {\n if (!this.cache) return\n const tags = [\n this.getUserTag(userId),\n this.getTenantTag(scope.tenantId),\n 'customer_rbac:all',\n ]\n await this.cache.set(cacheKey, data, { ttl: this.cacheTtlMs, tags })\n }\n\n async loadAcl(\n userId: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerAclData> {\n const cacheKey = this.getCacheKey(userId, scope)\n const cached = await this.getFromCache(cacheKey)\n if (cached) return cached\n\n const em = this.em.fork()\n\n // Per-user ACL first\n const userAcl = await em.findOne(CustomerUserAcl, {\n user: userId as any,\n tenantId: scope.tenantId,\n })\n if (userAcl) {\n const result: CustomerAclData = {\n isPortalAdmin: !!userAcl.isPortalAdmin,\n features: Array.isArray(userAcl.featuresJson) ? userAcl.featuresJson : [],\n }\n await this.setCache(cacheKey, result, userId, scope)\n return result\n }\n\n // Aggregate role ACLs\n const links = await em.find(CustomerUserRole, {\n user: userId as any,\n role: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n deletedAt: null,\n } as any, { populate: ['role'] })\n const roleIds = links.map((l) => (l.role as any)?.id).filter(Boolean)\n\n let isPortalAdmin = false\n const features: string[] = []\n if (roleIds.length) {\n const roleAcls = await em.find(CustomerRoleAcl, {\n tenantId: scope.tenantId,\n role: { $in: roleIds as any },\n } as any)\n for (const acl of roleAcls) {\n isPortalAdmin = isPortalAdmin || !!acl.isPortalAdmin\n if (Array.isArray(acl.featuresJson)) {\n for (const f of acl.featuresJson) {\n if (!features.includes(f)) features.push(f)\n }\n }\n }\n }\n\n const result: CustomerAclData = { isPortalAdmin, features }\n await this.setCache(cacheKey, result, userId, scope)\n return result\n }\n\n async userHasAllFeatures(\n userId: string,\n required: string[],\n scope: { tenantId: string; organizationId: string },\n ): Promise<boolean> {\n if (!required.length) return true\n const acl = await this.loadAcl(userId, scope)\n if (acl.isPortalAdmin) return true\n return hasAllFeatures(required, acl.features)\n }\n\n async invalidateUserCache(userId: string): Promise<void> {\n if (!this.cache) return\n await this.cache.deleteByTags([this.getUserTag(userId)])\n }\n\n async invalidateRoleCache(roleId: string): Promise<void> {\n if (!this.cache) return\n // When a role changes, invalidate all customer RBAC caches since we don't track role\u2192user mappings\n await this.cache.deleteByTags(['customer_rbac:all'])\n }\n\n async invalidateTenantCache(tenantId: string): Promise<void> {\n if (!this.cache) return\n await this.cache.deleteByTags([this.getTenantTag(tenantId)])\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport {\n CustomerUserAcl,\n CustomerRoleAcl,\n CustomerUserRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport {\n authorizeFeatures,\n resolveEffectiveFeatures,\n} from '@open-mercato/shared/security/featurePolicy'\n\ninterface CustomerAclData {\n isPortalAdmin: boolean\n features: string[]\n}\n\nfunction isCustomerAclData(value: unknown): value is CustomerAclData {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<CustomerAclData>\n if (typeof record.isPortalAdmin !== 'boolean') return false\n if (!Array.isArray(record.features) || record.features.some((f) => typeof f !== 'string')) return false\n return true\n}\n\nexport class CustomerRbacService {\n private cacheTtlMs: number = 5 * 60 * 1000\n private cache: CacheStrategy | null = null\n\n constructor(private em: EntityManager, cache?: CacheStrategy) {\n this.cache = cache || null\n }\n\n private getCacheKey(userId: string, scope: { tenantId: string; organizationId: string }): string {\n // Keep the authorization query versioned in the key so cache entries created\n // before scope hardening cannot preserve grants that are no longer valid.\n return `customer_rbac:v2:${userId}:${scope.tenantId}:${scope.organizationId}`\n }\n\n private getUserTag(userId: string): string {\n return `customer_rbac:user:${userId}`\n }\n\n private getTenantTag(tenantId: string): string {\n return `customer_rbac:tenant:${tenantId}`\n }\n\n private async getFromCache(cacheKey: string): Promise<CustomerAclData | null> {\n if (!this.cache) return null\n const cached = await this.cache.get(cacheKey)\n if (!cached) return null\n return isCustomerAclData(cached) ? cached : null\n }\n\n private async setCache(\n cacheKey: string,\n data: CustomerAclData,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<void> {\n if (!this.cache) return\n const tags = [\n this.getUserTag(userId),\n this.getTenantTag(scope.tenantId),\n 'customer_rbac:all',\n ]\n await this.cache.set(cacheKey, data, { ttl: this.cacheTtlMs, tags })\n }\n\n async loadAcl(\n userId: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerAclData> {\n const cacheKey = this.getCacheKey(userId, scope)\n const cached = await this.getFromCache(cacheKey)\n if (cached) return cached\n\n const em = this.em.fork()\n\n // Per-user ACL first\n const userAcl = await em.findOne(CustomerUserAcl, {\n user: userId as any,\n tenantId: scope.tenantId,\n })\n if (userAcl) {\n const result: CustomerAclData = {\n isPortalAdmin: !!userAcl.isPortalAdmin,\n features: Array.isArray(userAcl.featuresJson) ? userAcl.featuresJson : [],\n }\n await this.setCache(cacheKey, result, userId, scope)\n return result\n }\n\n // Aggregate role ACLs\n const links = await em.find(CustomerUserRole, {\n user: userId as any,\n role: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n deletedAt: null,\n } as any, { populate: ['role'] })\n const roleIds = links.map((l) => (l.role as any)?.id).filter(Boolean)\n\n let isPortalAdmin = false\n const features: string[] = []\n if (roleIds.length) {\n const roleAcls = await em.find(CustomerRoleAcl, {\n tenantId: scope.tenantId,\n role: { $in: roleIds as any },\n } as any)\n for (const acl of roleAcls) {\n isPortalAdmin = isPortalAdmin || !!acl.isPortalAdmin\n if (Array.isArray(acl.featuresJson)) {\n for (const f of acl.featuresJson) {\n if (!features.includes(f)) features.push(f)\n }\n }\n }\n }\n\n const result: CustomerAclData = { isPortalAdmin, features }\n await this.setCache(cacheKey, result, userId, scope)\n return result\n }\n\n async userHasAllFeatures(\n userId: string,\n required: string[],\n scope: { tenantId: string; organizationId: string },\n ): Promise<boolean> {\n if (!required.length) return true\n const acl = await this.loadAcl(userId, scope)\n return authorizeFeatures(required, {\n grantedFeatures: acl.features,\n unrestricted: acl.isPortalAdmin,\n })\n }\n\n async getEffectiveFeatures(\n userId: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<string[]> {\n const acl = await this.loadAcl(userId, scope)\n return resolveEffectiveFeatures(acl.isPortalAdmin ? ['portal.*'] : acl.features)\n }\n\n async invalidateUserCache(userId: string): Promise<void> {\n if (!this.cache) return\n await this.cache.deleteByTags([this.getUserTag(userId)])\n }\n\n async invalidateRoleCache(roleId: string): Promise<void> {\n if (!this.cache) return\n // When a role changes, invalidate all customer RBAC caches since we don't track role\u2192user mappings\n await this.cache.deleteByTags(['customer_rbac:all'])\n }\n\n async invalidateTenantCache(tenantId: string): Promise<void> {\n if (!this.cache) return\n await this.cache.deleteByTags([this.getTenantTag(tenantId)])\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["result"]
7
7
  }
@@ -98,6 +98,9 @@ const DEFAULT_ROLES = [
98
98
  }
99
99
  }
100
100
  ];
101
+ const DEFAULT_CUSTOMER_ROLE_FEATURES = Object.fromEntries(
102
+ DEFAULT_ROLES.map((role) => [role.slug, [...role.acl.features]])
103
+ );
101
104
  async function ensureDefaultCustomerRoleAcls(em, tenantId, modules) {
102
105
  const featuresByRole = {};
103
106
  for (const mod of modules) {
@@ -162,6 +165,9 @@ const setup = {
162
165
  superadmin: ["customer_accounts.*"],
163
166
  admin: ["customer_accounts.*"]
164
167
  },
168
+ // Keep the portal feature catalog declarative so the shared feature policy
169
+ // can project portal.* into concrete capabilities without importing core.
170
+ defaultCustomerRoleFeatures: DEFAULT_CUSTOMER_ROLE_FEATURES,
165
171
  async onTenantCreated({ em, tenantId, organizationId }) {
166
172
  await seedDefaultRoles(em, { tenantId, organizationId });
167
173
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customer_accounts/setup.ts"],
4
- "sourcesContent": ["import type { ModuleSetupConfig, DefaultCustomerRoleFeatures } from '@open-mercato/shared/modules/setup'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Module } from '@open-mercato/shared/modules/registry'\nimport { hash } from 'bcryptjs'\nimport { hashForLookup } from '@open-mercato/shared/lib/encryption/aes'\nimport {\n CustomerRole,\n CustomerRoleAcl,\n CustomerUser,\n CustomerUserRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\n\ninterface SeedScope {\n tenantId: string\n organizationId: string\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'system'\n scheduleType: 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload?: Record<string, unknown>\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\n// Stable, deterministic UUIDs for the two domain-routing system schedules so\n// re-running setup re-upserts the same rows instead of creating duplicates.\nconst DOMAIN_VERIFICATION_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1001'\nconst DOMAIN_TLS_RETRY_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1002'\n\nfunction intervalSecondsFromEnv(envName: string, fallbackSeconds: number): string {\n const parsed = Number.parseInt(process.env[envName] ?? '', 10)\n const seconds = Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackSeconds\n return `${seconds}s`\n}\n\nasync function registerDomainSchedules(\n container: { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },\n): Promise<void> {\n if (typeof container.hasRegistration !== 'function' || !container.hasRegistration('schedulerService')) {\n return\n }\n const schedulerService = container.resolve('schedulerService') as SchedulerServiceLike\n\n await schedulerService.register({\n id: DOMAIN_VERIFICATION_SCHEDULE_ID,\n name: 'Custom domain \u2014 DNS auto-verification',\n description:\n 'Re-runs DNS verification for custom-domain mappings stuck in pending or dns_failed, and triggers TLS health checks on success.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: intervalSecondsFromEnv('DOMAIN_AUTO_VERIFY_INTERVAL_SECONDS', 300),\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'domain-verification',\n sourceType: 'module',\n sourceModule: 'customer_accounts',\n isEnabled: true,\n })\n\n await schedulerService.register({\n id: DOMAIN_TLS_RETRY_SCHEDULE_ID,\n name: 'Custom domain \u2014 TLS retry',\n description:\n 'Retries TLS certificate provisioning for verified or tls_failed custom-domain mappings, with batch cap and adaptive backoff on Let\\'s Encrypt rate limits.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: intervalSecondsFromEnv('DOMAIN_TLS_RETRY_INTERVAL_SECONDS', 1800),\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'domain-tls-retry',\n sourceType: 'module',\n sourceModule: 'customer_accounts',\n isEnabled: true,\n })\n}\n\nconst DEFAULT_ROLES = [\n {\n name: 'Portal Admin',\n slug: 'portal_admin',\n description: 'Full portal administration access',\n isSystem: true,\n customerAssignable: false,\n isDefault: false,\n acl: {\n isPortalAdmin: true,\n features: ['portal.*'],\n },\n },\n {\n name: 'Buyer',\n slug: 'buyer',\n description: 'Standard buyer access with ordering capabilities',\n isSystem: true,\n customerAssignable: true,\n isDefault: true,\n acl: {\n isPortalAdmin: false,\n features: [\n 'portal.account.manage',\n 'portal.orders.view',\n 'portal.orders.create',\n 'portal.quotes.view',\n 'portal.quotes.request',\n 'portal.invoices.view',\n 'portal.catalog.view',\n ],\n },\n },\n {\n name: 'Viewer',\n slug: 'viewer',\n description: 'Read-only portal access',\n isSystem: true,\n customerAssignable: true,\n isDefault: false,\n acl: {\n isPortalAdmin: false,\n features: [\n 'portal.account.manage',\n 'portal.orders.view',\n 'portal.invoices.view',\n 'portal.catalog.view',\n ],\n },\n },\n]\n\n/**\n * Collect defaultCustomerRoleFeatures from all enabled modules and merge\n * them into the corresponding CustomerRoleAcl records.\n */\nasync function ensureDefaultCustomerRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n): Promise<void> {\n const featuresByRole: Record<string, string[]> = {}\n\n for (const mod of modules) {\n const customerRoleFeatures = mod.setup?.defaultCustomerRoleFeatures\n if (!customerRoleFeatures) continue\n for (const [roleSlug, features] of Object.entries(customerRoleFeatures)) {\n if (!features || !features.length) continue\n if (!featuresByRole[roleSlug]) featuresByRole[roleSlug] = []\n featuresByRole[roleSlug].push(...features)\n }\n }\n\n const roleSlugs = Object.keys(featuresByRole)\n if (!roleSlugs.length) return\n\n for (const roleSlug of roleSlugs) {\n const role = await em.findOne(CustomerRole, { tenantId, slug: roleSlug, deletedAt: null })\n if (!role) continue\n\n const acl = await em.findOne(CustomerRoleAcl, { role: role.id as any, tenantId })\n if (!acl) continue\n\n const currentFeatures = Array.isArray(acl.featuresJson) ? acl.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...featuresByRole[roleSlug]]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) {\n acl.featuresJson = merged\n em.persist(acl)\n }\n }\n await em.flush()\n}\n\nasync function seedDefaultRoles(em: EntityManager, scope: SeedScope): Promise<void> {\n for (const roleDef of DEFAULT_ROLES) {\n const existing = await em.findOne(CustomerRole, {\n tenantId: scope.tenantId,\n slug: roleDef.slug,\n deletedAt: null,\n })\n if (existing) continue\n\n const role = em.create(CustomerRole, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n name: roleDef.name,\n slug: roleDef.slug,\n description: roleDef.description,\n isSystem: roleDef.isSystem,\n customerAssignable: roleDef.customerAssignable,\n isDefault: roleDef.isDefault,\n createdAt: new Date(),\n } as any)\n em.persist(role)\n\n const acl = em.create(CustomerRoleAcl, {\n role,\n tenantId: scope.tenantId,\n featuresJson: roleDef.acl.features,\n isPortalAdmin: roleDef.acl.isPortalAdmin,\n createdAt: new Date(),\n } as any)\n em.persist(acl)\n }\n await em.flush()\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['customer_accounts.*'],\n admin: ['customer_accounts.*'],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n await seedDefaultRoles(em, { tenantId, organizationId })\n },\n\n async seedDefaults({ em, tenantId, organizationId, container }) {\n await seedDefaultRoles(em, { tenantId, organizationId })\n // Merge defaultCustomerRoleFeatures from all enabled modules\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const allModules = getModules()\n await ensureDefaultCustomerRoleAcls(em, tenantId, allModules)\n } catch {\n // Modules may not be registered yet during initial setup\n }\n\n // System-scoped schedules: register is idempotent, so re-running setup\n // (or running it for additional tenants) just upserts the same two rows.\n try {\n await registerDomainSchedules(\n container as { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },\n )\n } catch {\n // Scheduler may not be installed in this deployment; ignore.\n }\n },\n\n async seedExamples({ em, tenantId, organizationId }) {\n const BCRYPT_COST = 10\n const exampleUsers = [\n { email: 'alice.johnson@example.com', displayName: 'Alice Johnson', password: 'Password123!', roleSlug: 'portal_admin' },\n { email: 'bob.smith@example.com', displayName: 'Bob Smith', password: 'Password123!', roleSlug: 'buyer' },\n { email: 'carol.white@example.com', displayName: 'Carol White', password: 'Password123!', roleSlug: 'viewer' },\n ]\n\n for (const entry of exampleUsers) {\n const emailHash = hashForLookup(entry.email)\n const existing = await em.findOne(CustomerUser, { emailHash, tenantId, deletedAt: null })\n if (existing) continue\n\n const passwordHash = await hash(entry.password, BCRYPT_COST)\n const user = em.create(CustomerUser, {\n email: entry.email,\n emailHash,\n passwordHash,\n displayName: entry.displayName,\n tenantId,\n organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n emailVerifiedAt: new Date(),\n createdAt: new Date(),\n } as any)\n em.persist(user)\n\n const role = await em.findOne(CustomerRole, { tenantId, slug: entry.roleSlug, deletedAt: null })\n if (role) {\n const userRole = em.create(CustomerUserRole, {\n user,\n role,\n createdAt: new Date(),\n } as any)\n em.persist(userRole)\n }\n }\n await em.flush()\n },\n}\n\nexport default setup\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ModuleSetupConfig, DefaultCustomerRoleFeatures } from '@open-mercato/shared/modules/setup'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Module } from '@open-mercato/shared/modules/registry'\nimport { hash } from 'bcryptjs'\nimport { hashForLookup } from '@open-mercato/shared/lib/encryption/aes'\nimport {\n CustomerRole,\n CustomerRoleAcl,\n CustomerUser,\n CustomerUserRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\n\ninterface SeedScope {\n tenantId: string\n organizationId: string\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'system'\n scheduleType: 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload?: Record<string, unknown>\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\n// Stable, deterministic UUIDs for the two domain-routing system schedules so\n// re-running setup re-upserts the same rows instead of creating duplicates.\nconst DOMAIN_VERIFICATION_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1001'\nconst DOMAIN_TLS_RETRY_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1002'\n\nfunction intervalSecondsFromEnv(envName: string, fallbackSeconds: number): string {\n const parsed = Number.parseInt(process.env[envName] ?? '', 10)\n const seconds = Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackSeconds\n return `${seconds}s`\n}\n\nasync function registerDomainSchedules(\n container: { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },\n): Promise<void> {\n if (typeof container.hasRegistration !== 'function' || !container.hasRegistration('schedulerService')) {\n return\n }\n const schedulerService = container.resolve('schedulerService') as SchedulerServiceLike\n\n await schedulerService.register({\n id: DOMAIN_VERIFICATION_SCHEDULE_ID,\n name: 'Custom domain \u2014 DNS auto-verification',\n description:\n 'Re-runs DNS verification for custom-domain mappings stuck in pending or dns_failed, and triggers TLS health checks on success.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: intervalSecondsFromEnv('DOMAIN_AUTO_VERIFY_INTERVAL_SECONDS', 300),\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'domain-verification',\n sourceType: 'module',\n sourceModule: 'customer_accounts',\n isEnabled: true,\n })\n\n await schedulerService.register({\n id: DOMAIN_TLS_RETRY_SCHEDULE_ID,\n name: 'Custom domain \u2014 TLS retry',\n description:\n 'Retries TLS certificate provisioning for verified or tls_failed custom-domain mappings, with batch cap and adaptive backoff on Let\\'s Encrypt rate limits.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: intervalSecondsFromEnv('DOMAIN_TLS_RETRY_INTERVAL_SECONDS', 1800),\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'domain-tls-retry',\n sourceType: 'module',\n sourceModule: 'customer_accounts',\n isEnabled: true,\n })\n}\n\nconst DEFAULT_ROLES = [\n {\n name: 'Portal Admin',\n slug: 'portal_admin',\n description: 'Full portal administration access',\n isSystem: true,\n customerAssignable: false,\n isDefault: false,\n acl: {\n isPortalAdmin: true,\n features: ['portal.*'],\n },\n },\n {\n name: 'Buyer',\n slug: 'buyer',\n description: 'Standard buyer access with ordering capabilities',\n isSystem: true,\n customerAssignable: true,\n isDefault: true,\n acl: {\n isPortalAdmin: false,\n features: [\n 'portal.account.manage',\n 'portal.orders.view',\n 'portal.orders.create',\n 'portal.quotes.view',\n 'portal.quotes.request',\n 'portal.invoices.view',\n 'portal.catalog.view',\n ],\n },\n },\n {\n name: 'Viewer',\n slug: 'viewer',\n description: 'Read-only portal access',\n isSystem: true,\n customerAssignable: true,\n isDefault: false,\n acl: {\n isPortalAdmin: false,\n features: [\n 'portal.account.manage',\n 'portal.orders.view',\n 'portal.invoices.view',\n 'portal.catalog.view',\n ],\n },\n },\n]\n\nconst DEFAULT_CUSTOMER_ROLE_FEATURES = Object.fromEntries(\n DEFAULT_ROLES.map((role) => [role.slug, [...role.acl.features]]),\n)\n\n/**\n * Collect defaultCustomerRoleFeatures from all enabled modules and merge\n * them into the corresponding CustomerRoleAcl records.\n */\nasync function ensureDefaultCustomerRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n): Promise<void> {\n const featuresByRole: Record<string, string[]> = {}\n\n for (const mod of modules) {\n const customerRoleFeatures = mod.setup?.defaultCustomerRoleFeatures\n if (!customerRoleFeatures) continue\n for (const [roleSlug, features] of Object.entries(customerRoleFeatures)) {\n if (!features || !features.length) continue\n if (!featuresByRole[roleSlug]) featuresByRole[roleSlug] = []\n featuresByRole[roleSlug].push(...features)\n }\n }\n\n const roleSlugs = Object.keys(featuresByRole)\n if (!roleSlugs.length) return\n\n for (const roleSlug of roleSlugs) {\n const role = await em.findOne(CustomerRole, { tenantId, slug: roleSlug, deletedAt: null })\n if (!role) continue\n\n const acl = await em.findOne(CustomerRoleAcl, { role: role.id as any, tenantId })\n if (!acl) continue\n\n const currentFeatures = Array.isArray(acl.featuresJson) ? acl.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...featuresByRole[roleSlug]]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) {\n acl.featuresJson = merged\n em.persist(acl)\n }\n }\n await em.flush()\n}\n\nasync function seedDefaultRoles(em: EntityManager, scope: SeedScope): Promise<void> {\n for (const roleDef of DEFAULT_ROLES) {\n const existing = await em.findOne(CustomerRole, {\n tenantId: scope.tenantId,\n slug: roleDef.slug,\n deletedAt: null,\n })\n if (existing) continue\n\n const role = em.create(CustomerRole, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n name: roleDef.name,\n slug: roleDef.slug,\n description: roleDef.description,\n isSystem: roleDef.isSystem,\n customerAssignable: roleDef.customerAssignable,\n isDefault: roleDef.isDefault,\n createdAt: new Date(),\n } as any)\n em.persist(role)\n\n const acl = em.create(CustomerRoleAcl, {\n role,\n tenantId: scope.tenantId,\n featuresJson: roleDef.acl.features,\n isPortalAdmin: roleDef.acl.isPortalAdmin,\n createdAt: new Date(),\n } as any)\n em.persist(acl)\n }\n await em.flush()\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['customer_accounts.*'],\n admin: ['customer_accounts.*'],\n },\n // Keep the portal feature catalog declarative so the shared feature policy\n // can project portal.* into concrete capabilities without importing core.\n defaultCustomerRoleFeatures: DEFAULT_CUSTOMER_ROLE_FEATURES,\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n await seedDefaultRoles(em, { tenantId, organizationId })\n },\n\n async seedDefaults({ em, tenantId, organizationId, container }) {\n await seedDefaultRoles(em, { tenantId, organizationId })\n // Merge defaultCustomerRoleFeatures from all enabled modules\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const allModules = getModules()\n await ensureDefaultCustomerRoleAcls(em, tenantId, allModules)\n } catch {\n // Modules may not be registered yet during initial setup\n }\n\n // System-scoped schedules: register is idempotent, so re-running setup\n // (or running it for additional tenants) just upserts the same two rows.\n try {\n await registerDomainSchedules(\n container as { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },\n )\n } catch {\n // Scheduler may not be installed in this deployment; ignore.\n }\n },\n\n async seedExamples({ em, tenantId, organizationId }) {\n const BCRYPT_COST = 10\n const exampleUsers = [\n { email: 'alice.johnson@example.com', displayName: 'Alice Johnson', password: 'Password123!', roleSlug: 'portal_admin' },\n { email: 'bob.smith@example.com', displayName: 'Bob Smith', password: 'Password123!', roleSlug: 'buyer' },\n { email: 'carol.white@example.com', displayName: 'Carol White', password: 'Password123!', roleSlug: 'viewer' },\n ]\n\n for (const entry of exampleUsers) {\n const emailHash = hashForLookup(entry.email)\n const existing = await em.findOne(CustomerUser, { emailHash, tenantId, deletedAt: null })\n if (existing) continue\n\n const passwordHash = await hash(entry.password, BCRYPT_COST)\n const user = em.create(CustomerUser, {\n email: entry.email,\n emailHash,\n passwordHash,\n displayName: entry.displayName,\n tenantId,\n organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n emailVerifiedAt: new Date(),\n createdAt: new Date(),\n } as any)\n em.persist(user)\n\n const role = await em.findOne(CustomerRole, { tenantId, slug: entry.roleSlug, deletedAt: null })\n if (role) {\n const userRole = em.create(CustomerUserRole, {\n user,\n role,\n createdAt: new Date(),\n } as any)\n em.persist(userRole)\n }\n }\n await em.flush()\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -25,7 +25,7 @@ function PortalAiAssistantTriggerWidget({ context }) {
25
25
  const t = useT();
26
26
  const [open, setOpen] = React.useState(false);
27
27
  const resolvedFeatures = Array.isArray(context?.resolvedFeatures) ? context?.resolvedFeatures : [];
28
- const featureAllowed = context?.isPortalAdmin === true || hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE);
28
+ const featureAllowed = hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE);
29
29
  const pageContext = React.useMemo(() => ({
30
30
  view: "portal.profile",
31
31
  recordType: "customer",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\n/**\n * Step 4.10 \u2014 Portal AiChat injection widget (client).\n *\n * Renders an \"Ask AI\" trigger button + sheet inside the portal profile\n * page's `portal:profile:after` injection spot. Clicking the trigger\n * opens a right-side sheet embedding `<AiChat>` wired to\n * `customers.account_assistant`.\n *\n * Feature-gating:\n * - Declared in `widget.ts` metadata (`portal.account.manage`).\n * - The widget ALSO self-checks `context.resolvedFeatures` as a\n * defense-in-depth measure so the button never renders for a\n * customer who lacks the feature (portal pages are themselves\n * feature-gated, but the injection registry does not currently\n * enforce metadata.features at render time).\n *\n * `pageContext` shape:\n * { view: 'portal.profile', recordType: 'customer', recordId: <userId|null>, extra: {} }\n */\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { AiChat } from '@open-mercato/ui/ai/AiChat'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\nexport const PORTAL_AI_INJECT_AGENT_ID = 'customers.account_assistant'\nexport const PORTAL_AI_INJECT_REQUIRED_FEATURE = 'portal.account.manage'\n\nexport interface PortalAiInjectPageContext {\n view: 'portal.profile'\n recordType: 'customer'\n recordId: string | null\n extra: Record<string, never>\n}\n\ninterface PortalInjectionContext {\n orgSlug?: string\n user?: { id?: string | null } | null\n resolvedFeatures?: string[]\n isPortalAdmin?: boolean\n}\n\ninterface PortalAiAssistantTriggerProps {\n context?: PortalInjectionContext\n}\n\nfunction readUserId(user: PortalInjectionContext['user']): string | null {\n if (!user) return null\n const id = user.id\n return typeof id === 'string' && id.length > 0 ? id : null\n}\n\nexport default function PortalAiAssistantTriggerWidget({ context }: PortalAiAssistantTriggerProps) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n\n const resolvedFeatures = Array.isArray(context?.resolvedFeatures)\n ? (context?.resolvedFeatures as string[])\n : []\n const featureAllowed =\n context?.isPortalAdmin === true ||\n hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)\n\n const pageContext = React.useMemo<PortalAiInjectPageContext>(() => ({\n view: 'portal.profile',\n recordType: 'customer',\n recordId: readUserId(context?.user ?? null),\n extra: {},\n }), [context?.user])\n\n if (!featureAllowed) return null\n\n return (\n <div className=\"mt-6\" data-ai-portal-inject-wrapper=\"\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOpen(true)}\n data-ai-portal-inject-trigger=\"\"\n aria-label={t(\n 'customer_accounts.portal_ai_assistant.trigger.ariaLabel',\n 'Open portal AI assistant',\n )}\n >\n <Sparkles className=\"size-4\" aria-hidden />\n <span>{t('customer_accounts.portal_ai_assistant.trigger.label', 'Ask AI')}</span>\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent\n className={cn(\n 'sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl',\n 'flex flex-col gap-3 p-4',\n )}\n data-ai-portal-inject-sheet=\"\"\n >\n <DialogHeader>\n <DialogTitle>\n {t('customer_accounts.portal_ai_assistant.sheet.title', 'Portal AI assistant')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'customer_accounts.portal_ai_assistant.sheet.description',\n 'Read-only assistant for portal customers. Ask about your account and recent activity.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"min-h-0 flex-1\" data-ai-portal-inject-chat-container=\"\">\n <AiChat\n agent={PORTAL_AI_INJECT_AGENT_ID}\n pageContext={pageContext as unknown as Record<string, unknown>}\n className=\"h-full\"\n placeholder={t(\n 'customer_accounts.portal_ai_assistant.sheet.composerPlaceholder',\n 'Ask about your account...',\n )}\n />\n </div>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n"],
5
- "mappings": ";AAsFM,SAWE,KAXF;AAhEN,YAAY,WAAW;AACvB,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAEpB,MAAM,4BAA4B;AAClC,MAAM,oCAAoC;AAoBjD,SAAS,WAAW,MAAqD;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,KAAK,KAAK;AAChB,SAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK;AACxD;AAEe,SAAR,+BAAgD,EAAE,QAAQ,GAAkC;AACjG,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAE5C,QAAM,mBAAmB,MAAM,QAAQ,SAAS,gBAAgB,IAC3D,SAAS,mBACV,CAAC;AACL,QAAM,iBACJ,SAAS,kBAAkB,QAC3B,WAAW,kBAAkB,iCAAiC;AAEhE,QAAM,cAAc,MAAM,QAAmC,OAAO;AAAA,IAClE,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU,WAAW,SAAS,QAAQ,IAAI;AAAA,IAC1C,OAAO,CAAC;AAAA,EACV,IAAI,CAAC,SAAS,IAAI,CAAC;AAEnB,MAAI,CAAC,eAAgB,QAAO;AAE5B,SACE,qBAAC,SAAI,WAAU,QAAO,iCAA8B,IAClD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC3B,iCAA8B;AAAA,QAC9B,cAAY;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,YAAS,WAAU,UAAS,eAAW,MAAC;AAAA,UACzC,oBAAC,UAAM,YAAE,uDAAuD,QAAQ,GAAE;AAAA;AAAA;AAAA,IAC5E;AAAA,IACA,oBAAC,UAAO,MAAY,cAAc,SAChC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,+BAA4B;AAAA,QAE5B;AAAA,+BAAC,gBACC;AAAA,gCAAC,eACE,YAAE,qDAAqD,qBAAqB,GAC/E;AAAA,YACA,oBAAC,qBACE;AAAA,cACC;AAAA,cACA;AAAA,YACF,GACF;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,kBAAiB,wCAAqC,IACnE;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA,WAAU;AAAA,cACV,aAAa;AAAA,gBACX;AAAA,gBACA;AAAA,cACF;AAAA;AAAA,UACF,GACF;AAAA;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\n/**\n * Step 4.10 \u2014 Portal AiChat injection widget (client).\n *\n * Renders an \"Ask AI\" trigger button + sheet inside the portal profile\n * page's `portal:profile:after` injection spot. Clicking the trigger\n * opens a right-side sheet embedding `<AiChat>` wired to\n * `customers.account_assistant`.\n *\n * Feature-gating:\n * - Declared in `widget.ts` metadata (`portal.account.manage`).\n * - The widget ALSO self-checks `context.resolvedFeatures` as a\n * defense-in-depth measure so the button never renders for a\n * customer who lacks the feature (portal pages are themselves\n * feature-gated, but the injection registry does not currently\n * enforce metadata.features at render time).\n *\n * `pageContext` shape:\n * { view: 'portal.profile', recordType: 'customer', recordId: <userId|null>, extra: {} }\n */\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { AiChat } from '@open-mercato/ui/ai/AiChat'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\nexport const PORTAL_AI_INJECT_AGENT_ID = 'customers.account_assistant'\nexport const PORTAL_AI_INJECT_REQUIRED_FEATURE = 'portal.account.manage'\n\nexport interface PortalAiInjectPageContext {\n view: 'portal.profile'\n recordType: 'customer'\n recordId: string | null\n extra: Record<string, never>\n}\n\ninterface PortalInjectionContext {\n orgSlug?: string\n user?: { id?: string | null } | null\n resolvedFeatures?: string[]\n isPortalAdmin?: boolean\n}\n\ninterface PortalAiAssistantTriggerProps {\n context?: PortalInjectionContext\n}\n\nfunction readUserId(user: PortalInjectionContext['user']): string | null {\n if (!user) return null\n const id = user.id\n return typeof id === 'string' && id.length > 0 ? id : null\n}\n\nexport default function PortalAiAssistantTriggerWidget({ context }: PortalAiAssistantTriggerProps) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n\n const resolvedFeatures = Array.isArray(context?.resolvedFeatures)\n ? (context?.resolvedFeatures as string[])\n : []\n const featureAllowed = hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)\n\n const pageContext = React.useMemo<PortalAiInjectPageContext>(() => ({\n view: 'portal.profile',\n recordType: 'customer',\n recordId: readUserId(context?.user ?? null),\n extra: {},\n }), [context?.user])\n\n if (!featureAllowed) return null\n\n return (\n <div className=\"mt-6\" data-ai-portal-inject-wrapper=\"\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOpen(true)}\n data-ai-portal-inject-trigger=\"\"\n aria-label={t(\n 'customer_accounts.portal_ai_assistant.trigger.ariaLabel',\n 'Open portal AI assistant',\n )}\n >\n <Sparkles className=\"size-4\" aria-hidden />\n <span>{t('customer_accounts.portal_ai_assistant.trigger.label', 'Ask AI')}</span>\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent\n className={cn(\n 'sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl',\n 'flex flex-col gap-3 p-4',\n )}\n data-ai-portal-inject-sheet=\"\"\n >\n <DialogHeader>\n <DialogTitle>\n {t('customer_accounts.portal_ai_assistant.sheet.title', 'Portal AI assistant')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'customer_accounts.portal_ai_assistant.sheet.description',\n 'Read-only assistant for portal customers. Ask about your account and recent activity.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"min-h-0 flex-1\" data-ai-portal-inject-chat-container=\"\">\n <AiChat\n agent={PORTAL_AI_INJECT_AGENT_ID}\n pageContext={pageContext as unknown as Record<string, unknown>}\n className=\"h-full\"\n placeholder={t(\n 'customer_accounts.portal_ai_assistant.sheet.composerPlaceholder',\n 'Ask about your account...',\n )}\n />\n </div>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAoFM,SAWE,KAXF;AA9DN,YAAY,WAAW;AACvB,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAEpB,MAAM,4BAA4B;AAClC,MAAM,oCAAoC;AAoBjD,SAAS,WAAW,MAAqD;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,KAAK,KAAK;AAChB,SAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK;AACxD;AAEe,SAAR,+BAAgD,EAAE,QAAQ,GAAkC;AACjG,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAE5C,QAAM,mBAAmB,MAAM,QAAQ,SAAS,gBAAgB,IAC3D,SAAS,mBACV,CAAC;AACL,QAAM,iBAAiB,WAAW,kBAAkB,iCAAiC;AAErF,QAAM,cAAc,MAAM,QAAmC,OAAO;AAAA,IAClE,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU,WAAW,SAAS,QAAQ,IAAI;AAAA,IAC1C,OAAO,CAAC;AAAA,EACV,IAAI,CAAC,SAAS,IAAI,CAAC;AAEnB,MAAI,CAAC,eAAgB,QAAO;AAE5B,SACE,qBAAC,SAAI,WAAU,QAAO,iCAA8B,IAClD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC3B,iCAA8B;AAAA,QAC9B,cAAY;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,YAAS,WAAU,UAAS,eAAW,MAAC;AAAA,UACzC,oBAAC,UAAM,YAAE,uDAAuD,QAAQ,GAAE;AAAA;AAAA;AAAA,IAC5E;AAAA,IACA,oBAAC,UAAO,MAAY,cAAc,SAChC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,+BAA4B;AAAA,QAE5B;AAAA,+BAAC,gBACC;AAAA,gCAAC,eACE,YAAE,qDAAqD,qBAAqB,GAC/E;AAAA,YACA,oBAAC,qBACE;AAAA,cACC;AAAA,cACA;AAAA,YACF,GACF;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,kBAAiB,wCAAqC,IACnE;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA,WAAU;AAAA,cACV,aAAa;AAAA,gBACX;AAAA,gBACA;AAAA,cACF;AAAA;AAAA,UACF,GACF;AAAA;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,10 @@
1
- import { hasFeature } from "@open-mercato/shared/security/features";
1
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
2
2
  const EMAIL_VIEW_PRIVATE_FEATURE = "customers.email.view_private";
3
3
  function callerHasEmailViewPrivate(userFeatures) {
4
4
  if (!Array.isArray(userFeatures) || userFeatures.length === 0) return false;
5
- return hasFeature(userFeatures, EMAIL_VIEW_PRIVATE_FEATURE);
5
+ return authorizeFeatures([EMAIL_VIEW_PRIVATE_FEATURE], {
6
+ grantedFeatures: userFeatures
7
+ });
6
8
  }
7
9
  function canChangeEmailVisibility(opts) {
8
10
  if (opts.interactionType !== "email") return true;
@@ -24,8 +26,8 @@ async function resolveCallerEmailFeatures(container, userId, tenantId, organizat
24
26
  if (!userId) return void 0;
25
27
  try {
26
28
  const rbac = container.resolve("rbacService");
27
- if (!rbac?.getGrantedFeatures) return void 0;
28
- return await rbac.getGrantedFeatures(userId, { tenantId, organizationId });
29
+ if (!rbac?.getEffectiveFeatures) return void 0;
30
+ return await rbac.getEffectiveFeatures(userId, { tenantId, organizationId });
29
31
  } catch {
30
32
  return void 0;
31
33
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/visibilityFilter.ts"],
4
- "sourcesContent": ["import type { FilterQuery } from '@mikro-orm/postgresql'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { CustomerInteraction } from '../data/entities'\n\n/**\n * The ACL feature that grants admins the right to see private emails authored\n * by other users. Declared in `acl.ts` but granted to NO role in v1 (reserved\n * for the v2 oversight feature \u2014 see `callerHasEmailViewPrivate`).\n */\nexport const EMAIL_VIEW_PRIVATE_FEATURE = 'customers.email.view_private'\n\n/**\n * Returns true when the caller holds the admin override to see ALL private email\n * interactions. Honours wildcards (`customers.*`, `*`).\n *\n * RESERVED FOR v2 \u2014 NOT wired in v1. The v1 model is strict owner-only with no\n * admin bypass: the visibility filters and `canChangeEmailVisibility` ignore\n * caller features, and `customers.email.view_private` is granted to no role.\n * Kept (with {@link EMAIL_VIEW_PRIVATE_FEATURE}) so v2 oversight can opt back in\n * without re-introducing the helper. Do NOT wire this into a read path without\n * an explicit v2 spec.\n */\nexport function callerHasEmailViewPrivate(userFeatures: string[] | null | undefined): boolean {\n if (!Array.isArray(userFeatures) || userFeatures.length === 0) return false\n return hasFeature(userFeatures, EMAIL_VIEW_PRIVATE_FEATURE)\n}\n\n/**\n * Authorization predicate for CHANGING an email interaction's visibility.\n *\n * Personal mailbox privacy (v1: strict owner-only): ONLY the interaction's\n * author may flip their own email between private/shared \u2014 there is no admin\n * bypass. Non-email rows and no-op changes are always allowed. Mirrors the gate\n * in the dedicated `PATCH .../visibility` route so the generic interaction-update\n * path cannot bypass the privacy control. `userFeatures` is reserved for v2.\n */\nexport function canChangeEmailVisibility(opts: {\n interactionType: string\n currentVisibility: string | null | undefined\n nextVisibility: string | null | undefined\n authorUserId: string | null | undefined\n actorUserId: string | null | undefined\n userFeatures: string[] | null | undefined\n}): boolean {\n if (opts.interactionType !== 'email') return true\n if ((opts.nextVisibility ?? null) === (opts.currentVisibility ?? null)) return true\n return Boolean(opts.actorUserId) && opts.authorUserId === opts.actorUserId\n}\n\nexport interface ApplyEmailVisibilityFilterOptions {\n currentUserId: string | null\n userFeatures: string[] | null | undefined\n}\n\n/**\n * Adds a `WHERE` predicate to a kysely query so that:\n * - Non-email interactions (calls, meetings, tasks) pass through unchanged.\n * - Email interactions with `visibility = 'shared'` are visible to all.\n * - Email interactions with `visibility = 'private'` are visible ONLY to the\n * `authorUserId` (channel owner).\n *\n * Personal mailbox privacy (v1: strict owner-only) \u2014 there is NO admin bypass:\n * a private email is hidden from everyone except its author, including\n * admins/superadmins. `opts.userFeatures` is retained for signature stability\n * and reserved for the v2 admin-oversight feature.\n *\n * The function expects a kysely-style builder whose `.where()` accepts an\n * expression-builder callback. Returns the same builder for chaining.\n */\nexport function applyEmailVisibilityFilter<T extends { where: (...args: any[]) => T }>(\n query: T,\n opts: ApplyEmailVisibilityFilterOptions,\n): T {\n const currentUserId = opts.currentUserId\n // A row is hidden ONLY when it is an email explicitly marked `private` and the\n // caller is not its author. Everything else stays visible, including:\n // - non-email interactions (calls, meetings, tasks),\n // - emails marked `shared`,\n // - legacy/unset rows where `visibility IS NULL` (e.g. email-log entries\n // created before per-email visibility shipped) \u2014 these must remain\n // visible to avoid silently hiding pre-existing CRM history.\n return query.where((eb: any) =>\n eb.or([\n eb('interaction_type', '!=', 'email'),\n eb('visibility', 'is', null),\n eb('visibility', '!=', 'private'),\n currentUserId\n ? eb('author_user_id', '=', currentUserId)\n : eb.val(false),\n ]),\n )\n}\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[] | undefined>\n}\n\n/**\n * Resolve the caller's granted features (wildcard-aware downstream) so a v2\n * visibility filter could honour the `customers.email.view_private` admin\n * override. Returns `undefined` when there is no user or the RBAC service is\n * unavailable \u2014 callers MUST treat `undefined` as \"no bypass\" (fail closed).\n *\n * RESERVED FOR v2 \u2014 NOT called by any v1 read path. v1 is strict owner-only, so\n * the read routes pass `userFeatures: undefined` to the filters rather than\n * resolving features here (which would be a wasted RBAC round-trip). Re-wire\n * only under an explicit v2 oversight spec.\n */\nexport async function resolveCallerEmailFeatures(\n container: { resolve: (name: string) => unknown },\n userId: string | null,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n if (!userId) return undefined\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\n/**\n * MikroORM equivalent of {@link applyEmailVisibilityFilter}. Returns a\n * `FilterQuery` fragment to merge (implicit AND) into a `CustomerInteraction`\n * where-clause so private email rows are excluded for non-owner, non-admin\n * callers on MikroORM read paths (`findWithDecryption`/`em.find`/`em.count`).\n *\n * Mirrors the kysely predicate exactly, including the legacy `visibility IS NULL`\n * passthrough so pre-existing CRM history is never hidden. Personal mailbox\n * privacy (v1: strict owner-only): no admin bypass \u2014 a private email is hidden\n * from everyone except its author. `opts.userFeatures` is reserved for v2.\n */\nexport type EmailVisibilityMikroFilter = { $or?: FilterQuery<CustomerInteraction>[] }\n\nexport function buildEmailVisibilityMikroFilter(\n opts: ApplyEmailVisibilityFilterOptions,\n): EmailVisibilityMikroFilter {\n return {\n $or: [\n { interactionType: { $ne: 'email' } },\n { visibility: null },\n { visibility: { $ne: 'private' } },\n ...(opts.currentUserId ? [{ authorUserId: opts.currentUserId }] : []),\n ],\n }\n}\n"],
5
- "mappings": "AACA,SAAS,kBAAkB;AAQpB,MAAM,6BAA6B;AAanC,SAAS,0BAA0B,cAAoD;AAC5F,MAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,aAAa,WAAW,EAAG,QAAO;AACtE,SAAO,WAAW,cAAc,0BAA0B;AAC5D;AAWO,SAAS,yBAAyB,MAO7B;AACV,MAAI,KAAK,oBAAoB,QAAS,QAAO;AAC7C,OAAK,KAAK,kBAAkB,WAAW,KAAK,qBAAqB,MAAO,QAAO;AAC/E,SAAO,QAAQ,KAAK,WAAW,KAAK,KAAK,iBAAiB,KAAK;AACjE;AAsBO,SAAS,2BACd,OACA,MACG;AACH,QAAM,gBAAgB,KAAK;AAQ3B,SAAO,MAAM;AAAA,IAAM,CAAC,OAClB,GAAG,GAAG;AAAA,MACJ,GAAG,oBAAoB,MAAM,OAAO;AAAA,MACpC,GAAG,cAAc,MAAM,IAAI;AAAA,MAC3B,GAAG,cAAc,MAAM,SAAS;AAAA,MAChC,gBACI,GAAG,kBAAkB,KAAK,aAAa,IACvC,GAAG,IAAI,KAAK;AAAA,IAClB,CAAC;AAAA,EACH;AACF;AAoBA,eAAsB,2BACpB,WACA,QACA,UACA,gBAC+B;AAC/B,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,OAAO,UAAU,QAAQ,aAAa;AAC5C,QAAI,CAAC,MAAM,mBAAoB,QAAO;AACtC,WAAO,MAAM,KAAK,mBAAmB,QAAQ,EAAE,UAAU,eAAe,CAAC;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,gCACd,MAC4B;AAC5B,SAAO;AAAA,IACL,KAAK;AAAA,MACH,EAAE,iBAAiB,EAAE,KAAK,QAAQ,EAAE;AAAA,MACpC,EAAE,YAAY,KAAK;AAAA,MACnB,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE;AAAA,MACjC,GAAI,KAAK,gBAAgB,CAAC,EAAE,cAAc,KAAK,cAAc,CAAC,IAAI,CAAC;AAAA,IACrE;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { FilterQuery } from '@mikro-orm/postgresql'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { CustomerInteraction } from '../data/entities'\n\n/**\n * The ACL feature that grants admins the right to see private emails authored\n * by other users. Declared in `acl.ts` but granted to NO role in v1 (reserved\n * for the v2 oversight feature \u2014 see `callerHasEmailViewPrivate`).\n */\nexport const EMAIL_VIEW_PRIVATE_FEATURE = 'customers.email.view_private'\n\n/**\n * Returns true when the caller holds the admin override to see ALL private email\n * interactions. Honours wildcards (`customers.*`, `*`).\n *\n * RESERVED FOR v2 \u2014 NOT wired in v1. The v1 model is strict owner-only with no\n * admin bypass: the visibility filters and `canChangeEmailVisibility` ignore\n * caller features, and `customers.email.view_private` is granted to no role.\n * Kept (with {@link EMAIL_VIEW_PRIVATE_FEATURE}) so v2 oversight can opt back in\n * without re-introducing the helper. Do NOT wire this into a read path without\n * an explicit v2 spec.\n */\nexport function callerHasEmailViewPrivate(userFeatures: string[] | null | undefined): boolean {\n if (!Array.isArray(userFeatures) || userFeatures.length === 0) return false\n return authorizeFeatures([EMAIL_VIEW_PRIVATE_FEATURE], {\n grantedFeatures: userFeatures,\n })\n}\n\n/**\n * Authorization predicate for CHANGING an email interaction's visibility.\n *\n * Personal mailbox privacy (v1: strict owner-only): ONLY the interaction's\n * author may flip their own email between private/shared \u2014 there is no admin\n * bypass. Non-email rows and no-op changes are always allowed. Mirrors the gate\n * in the dedicated `PATCH .../visibility` route so the generic interaction-update\n * path cannot bypass the privacy control. `userFeatures` is reserved for v2.\n */\nexport function canChangeEmailVisibility(opts: {\n interactionType: string\n currentVisibility: string | null | undefined\n nextVisibility: string | null | undefined\n authorUserId: string | null | undefined\n actorUserId: string | null | undefined\n userFeatures: string[] | null | undefined\n}): boolean {\n if (opts.interactionType !== 'email') return true\n if ((opts.nextVisibility ?? null) === (opts.currentVisibility ?? null)) return true\n return Boolean(opts.actorUserId) && opts.authorUserId === opts.actorUserId\n}\n\nexport interface ApplyEmailVisibilityFilterOptions {\n currentUserId: string | null\n userFeatures: string[] | null | undefined\n}\n\n/**\n * Adds a `WHERE` predicate to a kysely query so that:\n * - Non-email interactions (calls, meetings, tasks) pass through unchanged.\n * - Email interactions with `visibility = 'shared'` are visible to all.\n * - Email interactions with `visibility = 'private'` are visible ONLY to the\n * `authorUserId` (channel owner).\n *\n * Personal mailbox privacy (v1: strict owner-only) \u2014 there is NO admin bypass:\n * a private email is hidden from everyone except its author, including\n * admins/superadmins. `opts.userFeatures` is retained for signature stability\n * and reserved for the v2 admin-oversight feature.\n *\n * The function expects a kysely-style builder whose `.where()` accepts an\n * expression-builder callback. Returns the same builder for chaining.\n */\nexport function applyEmailVisibilityFilter<T extends { where: (...args: any[]) => T }>(\n query: T,\n opts: ApplyEmailVisibilityFilterOptions,\n): T {\n const currentUserId = opts.currentUserId\n // A row is hidden ONLY when it is an email explicitly marked `private` and the\n // caller is not its author. Everything else stays visible, including:\n // - non-email interactions (calls, meetings, tasks),\n // - emails marked `shared`,\n // - legacy/unset rows where `visibility IS NULL` (e.g. email-log entries\n // created before per-email visibility shipped) \u2014 these must remain\n // visible to avoid silently hiding pre-existing CRM history.\n return query.where((eb: any) =>\n eb.or([\n eb('interaction_type', '!=', 'email'),\n eb('visibility', 'is', null),\n eb('visibility', '!=', 'private'),\n currentUserId\n ? eb('author_user_id', '=', currentUserId)\n : eb.val(false),\n ]),\n )\n}\n\ntype RbacServiceLike = {\n getEffectiveFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[] | undefined>\n}\n\n/**\n * Resolve the caller's granted features (wildcard-aware downstream) so a v2\n * visibility filter could honour the `customers.email.view_private` admin\n * override. Returns `undefined` when there is no user or the RBAC service is\n * unavailable \u2014 callers MUST treat `undefined` as \"no bypass\" (fail closed).\n *\n * RESERVED FOR v2 \u2014 NOT called by any v1 read path. v1 is strict owner-only, so\n * the read routes pass `userFeatures: undefined` to the filters rather than\n * resolving features here (which would be a wasted RBAC round-trip). Re-wire\n * only under an explicit v2 oversight spec.\n */\nexport async function resolveCallerEmailFeatures(\n container: { resolve: (name: string) => unknown },\n userId: string | null,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n if (!userId) return undefined\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getEffectiveFeatures) return undefined\n return await rbac.getEffectiveFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\n/**\n * MikroORM equivalent of {@link applyEmailVisibilityFilter}. Returns a\n * `FilterQuery` fragment to merge (implicit AND) into a `CustomerInteraction`\n * where-clause so private email rows are excluded for non-owner, non-admin\n * callers on MikroORM read paths (`findWithDecryption`/`em.find`/`em.count`).\n *\n * Mirrors the kysely predicate exactly, including the legacy `visibility IS NULL`\n * passthrough so pre-existing CRM history is never hidden. Personal mailbox\n * privacy (v1: strict owner-only): no admin bypass \u2014 a private email is hidden\n * from everyone except its author. `opts.userFeatures` is reserved for v2.\n */\nexport type EmailVisibilityMikroFilter = { $or?: FilterQuery<CustomerInteraction>[] }\n\nexport function buildEmailVisibilityMikroFilter(\n opts: ApplyEmailVisibilityFilterOptions,\n): EmailVisibilityMikroFilter {\n return {\n $or: [\n { interactionType: { $ne: 'email' } },\n { visibility: null },\n { visibility: { $ne: 'private' } },\n ...(opts.currentUserId ? [{ authorUserId: opts.currentUserId }] : []),\n ],\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,yBAAyB;AAQ3B,MAAM,6BAA6B;AAanC,SAAS,0BAA0B,cAAoD;AAC5F,MAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,aAAa,WAAW,EAAG,QAAO;AACtE,SAAO,kBAAkB,CAAC,0BAA0B,GAAG;AAAA,IACrD,iBAAiB;AAAA,EACnB,CAAC;AACH;AAWO,SAAS,yBAAyB,MAO7B;AACV,MAAI,KAAK,oBAAoB,QAAS,QAAO;AAC7C,OAAK,KAAK,kBAAkB,WAAW,KAAK,qBAAqB,MAAO,QAAO;AAC/E,SAAO,QAAQ,KAAK,WAAW,KAAK,KAAK,iBAAiB,KAAK;AACjE;AAsBO,SAAS,2BACd,OACA,MACG;AACH,QAAM,gBAAgB,KAAK;AAQ3B,SAAO,MAAM;AAAA,IAAM,CAAC,OAClB,GAAG,GAAG;AAAA,MACJ,GAAG,oBAAoB,MAAM,OAAO;AAAA,MACpC,GAAG,cAAc,MAAM,IAAI;AAAA,MAC3B,GAAG,cAAc,MAAM,SAAS;AAAA,MAChC,gBACI,GAAG,kBAAkB,KAAK,aAAa,IACvC,GAAG,IAAI,KAAK;AAAA,IAClB,CAAC;AAAA,EACH;AACF;AAoBA,eAAsB,2BACpB,WACA,QACA,UACA,gBAC+B;AAC/B,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,OAAO,UAAU,QAAQ,aAAa;AAC5C,QAAI,CAAC,MAAM,qBAAsB,QAAO;AACxC,WAAO,MAAM,KAAK,qBAAqB,QAAQ,EAAE,UAAU,eAAe,CAAC;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,gCACd,MAC4B;AAC5B,SAAO;AAAA,IACL,KAAK;AAAA,MACH,EAAE,iBAAiB,EAAE,KAAK,QAAQ,EAAE;AAAA,MACpC,EAAE,YAAY,KAAK;AAAA,MACnB,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE;AAAA,MACjC,GAAI,KAAK,gBAAgB,CAAC,EAAE,cAAc,KAAK,cAAc,CAAC,IAAI,CAAC;AAAA,IACrE;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,6 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { DashboardLayout } from "@open-mercato/core/modules/dashboards/data/entities";
6
6
  import { dashboardLayoutItemPatchSchema } from "@open-mercato/core/modules/dashboards/data/validators";
7
- import { hasFeature } from "@open-mercato/shared/security/features";
8
7
  import {
9
8
  runCrudMutationGuardAfterSuccess,
10
9
  validateCrudMutationGuard
@@ -47,8 +46,12 @@ async function PATCH(req, ctx) {
47
46
  tenantId: auth.tenantId ?? null,
48
47
  organizationId: auth.orgId ?? null
49
48
  };
50
- const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId });
51
- if (!acl.isSuperAdmin && !hasFeature(acl.features, "dashboards.configure")) {
49
+ const canConfigure = await rbac.userHasAllFeatures(
50
+ scope.userId,
51
+ ["dashboards.configure"],
52
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
53
+ );
54
+ if (!canConfigure) {
52
55
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
53
56
  }
54
57
  const guardResult = await validateCrudMutationGuard(container, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/layout/%5BitemId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutItemUpdateSchema,\n} from '../../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { itemId?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const layoutItemId = ctx.params?.itemId\n if (!layoutItemId) return NextResponse.json({ error: 'Missing layout item id' }, { status: 400 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const parsed = dashboardLayoutItemPatchSchema.safeParse({ ...(body as Record<string, unknown>), id: layoutItemId })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n const scope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, 'dashboards.configure')) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const layout = await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n if (!layout) {\n return NextResponse.json({ error: 'Layout not found' }, { status: 404 })\n }\n\n const idx = layout.layoutJson.findIndex((item: { id?: string | null }) => item?.id === layoutItemId)\n if (idx === -1) {\n return NextResponse.json({ error: 'Layout item not found' }, { status: 404 })\n }\n\n const current = layout.layoutJson[idx]\n layout.layoutJson[idx] = {\n ...current,\n size: parsed.data.size ?? current.size ?? DEFAULT_SIZE,\n settings: parsed.data.settings ?? current.settings,\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutItemParamsSchema = z.object({\n itemId: z.string().uuid(),\n})\n\nconst layoutItemPatchDoc: OpenApiMethodDoc = {\n summary: 'Update a dashboard layout item',\n description: 'Adjusts the size or settings for a single widget within the dashboard layout.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutItemUpdateSchema,\n description: 'Payload containing the new size or settings for the widget.',\n },\n responses: [\n { status: 200, description: 'Layout item updated.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing item id', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n { status: 404, description: 'Item not found', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Update dashboard layout item',\n pathParams: layoutItemParamsSchema,\n methods: {\n PATCH: layoutItemPatchDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutItemUpdateSchema,\n} from '../../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { itemId?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const layoutItemId = ctx.params?.itemId\n if (!layoutItemId) return NextResponse.json({ error: 'Missing layout item id' }, { status: 400 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const parsed = dashboardLayoutItemPatchSchema.safeParse({ ...(body as Record<string, unknown>), id: layoutItemId })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n const scope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const canConfigure = await rbac.userHasAllFeatures(\n scope.userId,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const layout = await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n if (!layout) {\n return NextResponse.json({ error: 'Layout not found' }, { status: 404 })\n }\n\n const idx = layout.layoutJson.findIndex((item: { id?: string | null }) => item?.id === layoutItemId)\n if (idx === -1) {\n return NextResponse.json({ error: 'Layout item not found' }, { status: 404 })\n }\n\n const current = layout.layoutJson[idx]\n layout.layoutJson[idx] = {\n ...current,\n size: parsed.data.size ?? current.size ?? DEFAULT_SIZE,\n settings: parsed.data.settings ?? current.settings,\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutItemParamsSchema = z.object({\n itemId: z.string().uuid(),\n})\n\nconst layoutItemPatchDoc: OpenApiMethodDoc = {\n summary: 'Update a dashboard layout item',\n description: 'Adjusts the size or settings for a single widget within the dashboard layout.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutItemUpdateSchema,\n description: 'Payload containing the new size or settings for the widget.',\n },\n responses: [\n { status: 200, description: 'Layout item updated.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing item id', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n { status: 404, description: 'Item not found', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Update dashboard layout item',\n pathParams: layoutItemParamsSchema,\n methods: {\n PATCH: layoutItemPatchDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,7 @@ import { DashboardLayout } from "@open-mercato/core/modules/dashboards/data/enti
6
6
  import { dashboardLayoutSchema } from "@open-mercato/core/modules/dashboards/data/validators";
7
7
  import { loadAllWidgets } from "@open-mercato/core/modules/dashboards/lib/widgets";
8
8
  import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/lib/access";
9
- import { hasFeature } from "@open-mercato/shared/security/features";
9
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
10
10
  import { User } from "@open-mercato/core/modules/auth/data/entities";
11
11
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
12
12
  import {
@@ -67,7 +67,10 @@ async function GET(req) {
67
67
  tenantId: auth.tenantId ?? null,
68
68
  organizationId: auth.orgId ?? null
69
69
  };
70
- const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId });
70
+ const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
71
+ tenantId: scope.tenantId,
72
+ organizationId: scope.organizationId
73
+ });
71
74
  const widgets = await loadAllWidgets();
72
75
  const allowedIds = await resolveAllowedWidgetIds(
73
76
  em,
@@ -75,8 +78,8 @@ async function GET(req) {
75
78
  userId: scope.userId,
76
79
  tenantId: scope.tenantId,
77
80
  organizationId: scope.organizationId,
78
- features: acl.features ?? [],
79
- isSuperAdmin: !!acl.isSuperAdmin
81
+ features: effectiveFeatures,
82
+ isSuperAdmin: false
80
83
  },
81
84
  widgets
82
85
  );
@@ -119,7 +122,9 @@ async function GET(req) {
119
122
  if (hasChanged) {
120
123
  await em.flush();
121
124
  }
122
- const canConfigure = acl.isSuperAdmin || hasFeature(acl.features, "dashboards.configure");
125
+ const canConfigure = authorizeFeatures(["dashboards.configure"], {
126
+ grantedFeatures: effectiveFeatures
127
+ });
123
128
  let userEmail = null;
124
129
  let userName = null;
125
130
  let userLabel = null;
@@ -186,8 +191,12 @@ async function PUT(req) {
186
191
  tenantId: auth.tenantId ?? null,
187
192
  organizationId: auth.orgId ?? null
188
193
  };
189
- const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId });
190
- if (!acl.isSuperAdmin && !hasFeature(acl.features, "dashboards.configure")) {
194
+ const canConfigure = await rbac.userHasAllFeatures(
195
+ scope.userId,
196
+ ["dashboards.configure"],
197
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
198
+ );
199
+ if (!canConfigure) {
191
200
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
192
201
  }
193
202
  const guardResult = await validateCrudMutationGuard(container, {
@@ -205,14 +214,18 @@ async function PUT(req) {
205
214
  return NextResponse.json(guardResult.body, { status: guardResult.status });
206
215
  }
207
216
  const widgets = await loadAllWidgets();
217
+ const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
218
+ tenantId: scope.tenantId,
219
+ organizationId: scope.organizationId
220
+ });
208
221
  const allowedIds = await resolveAllowedWidgetIds(
209
222
  em,
210
223
  {
211
224
  userId: scope.userId,
212
225
  tenantId: scope.tenantId,
213
226
  organizationId: scope.organizationId,
214
- features: acl.features ?? [],
215
- isSuperAdmin: !!acl.isSuperAdmin
227
+ features: effectiveFeatures,
228
+ isSuperAdmin: false
216
229
  },
217
230
  widgets
218
231
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/dashboards/api/layout/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const url = new URL(req.url)\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n } else {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged) {\n await em.flush()\n }\n\n const canConfigure = acl.isSuperAdmin || hasFeature(acl.features, 'dashboards.configure')\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, 'dashboards.configure')) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: parsed.data.items },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const url = new URL(req.url)\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n } else {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged) {\n await em.flush()\n }\n\n const canConfigure = authorizeFeatures(['dashboards.configure'], {\n grantedFeatures: effectiveFeatures,\n })\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const canConfigure = await rbac.userHasAllFeatures(\n scope.userId,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: parsed.data.items },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const widgets = await loadAllWidgets()\n const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -10,7 +10,7 @@ import {
10
10
  resolveWidgetAssignmentReadScope,
11
11
  resolveWidgetAssignmentTargetAccess
12
12
  } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
13
- import { hasFeature } from "@open-mercato/shared/security/features";
13
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
14
14
  import {
15
15
  runCrudMutationGuardAfterSuccess,
16
16
  validateCrudMutationGuard
@@ -54,7 +54,10 @@ async function GET(req) {
54
54
  const em = container.resolve("em");
55
55
  const rbac = container.resolve("rbacService");
56
56
  const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
57
- if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {
57
+ if (!authorizeFeatures([FEATURE], {
58
+ grantedFeatures: acl.features ?? [],
59
+ unrestricted: !!acl.isSuperAdmin
60
+ })) {
58
61
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
59
62
  }
60
63
  const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({
@@ -105,7 +108,10 @@ async function PUT(req) {
105
108
  const em = resolve("em");
106
109
  const rbac = resolve("rbacService");
107
110
  const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
108
- if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {
111
+ if (!authorizeFeatures([FEATURE], {
112
+ grantedFeatures: acl.features ?? [],
113
+ unrestricted: !!acl.isSuperAdmin
114
+ })) {
109
115
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
110
116
  }
111
117
  const widgets = await loadAllWidgets();