@open-mercato/core 0.6.5-develop.4534.1.b459babe6d → 0.6.5-develop.4559.1.839e136509

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 (644) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +5 -0
  3. package/dist/generated/entities/role/index.js +3 -1
  4. package/dist/generated/entities/role/index.js.map +2 -2
  5. package/dist/generated/entities/user/index.js +3 -1
  6. package/dist/generated/entities/user/index.js.map +2 -2
  7. package/dist/generated/entity-fields-registry.js +2 -0
  8. package/dist/generated/entity-fields-registry.js.map +2 -2
  9. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  10. package/dist/helpers/integration/dbFixtures.js +2 -1
  11. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  12. package/dist/helpers/integration/optimisticLockUi.js +104 -0
  13. package/dist/helpers/integration/optimisticLockUi.js.map +7 -0
  14. package/dist/helpers/integration/salesFixtures.js +17 -0
  15. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  16. package/dist/modules/api_keys/backend/api-keys/page.js +9 -5
  17. package/dist/modules/api_keys/backend/api-keys/page.js.map +2 -2
  18. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +17 -9
  19. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  20. package/dist/modules/auth/api/roles/acl/route.js +32 -13
  21. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  22. package/dist/modules/auth/api/roles/route.js +3 -1
  23. package/dist/modules/auth/api/roles/route.js.map +2 -2
  24. package/dist/modules/auth/api/sidebar/preferences/route.js +71 -3
  25. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  26. package/dist/modules/auth/api/users/acl/route.js +42 -19
  27. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  28. package/dist/modules/auth/api/users/route.js +3 -1
  29. package/dist/modules/auth/api/users/route.js.map +2 -2
  30. package/dist/modules/auth/backend/roles/[id]/edit/page.js +24 -4
  31. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  32. package/dist/modules/auth/backend/roles/page.js +8 -4
  33. package/dist/modules/auth/backend/roles/page.js.map +2 -2
  34. package/dist/modules/auth/backend/users/[id]/edit/page.js +27 -5
  35. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  36. package/dist/modules/auth/backend/users/page.js +6 -2
  37. package/dist/modules/auth/backend/users/page.js.map +2 -2
  38. package/dist/modules/auth/components/AclEditor.js +3 -1
  39. package/dist/modules/auth/components/AclEditor.js.map +2 -2
  40. package/dist/modules/auth/data/entities.js +6 -0
  41. package/dist/modules/auth/data/entities.js.map +2 -2
  42. package/dist/modules/auth/services/sidebarPreferencesService.js +32 -4
  43. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  44. package/dist/modules/business_rules/api/rules/route.js +28 -0
  45. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  46. package/dist/modules/business_rules/api/sets/route.js +28 -0
  47. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  48. package/dist/modules/business_rules/backend/rules/[id]/page.js +11 -4
  49. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +3 -3
  50. package/dist/modules/business_rules/backend/rules/page.js +20 -11
  51. package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
  52. package/dist/modules/business_rules/backend/sets/[id]/page.js +11 -4
  53. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  54. package/dist/modules/business_rules/backend/sets/page.js +20 -11
  55. package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
  56. package/dist/modules/catalog/api/categories/route.js +2 -0
  57. package/dist/modules/catalog/api/categories/route.js.map +2 -2
  58. package/dist/modules/catalog/api/products/route.js +2 -1
  59. package/dist/modules/catalog/api/products/route.js.map +2 -2
  60. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +2 -0
  61. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  62. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +94 -40
  63. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  64. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +37 -8
  65. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  66. package/dist/modules/catalog/backend/catalog/products/optionSchemaClient.js.map +2 -2
  67. package/dist/modules/catalog/commands/variants.js +32 -31
  68. package/dist/modules/catalog/commands/variants.js.map +2 -2
  69. package/dist/modules/catalog/components/PriceKindSettings.js +12 -5
  70. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  71. package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +2 -2
  72. package/dist/modules/catalog/components/products/ProductMediaManager.js.map +2 -2
  73. package/dist/modules/catalog/components/products/ProductsDataTable.js +5 -3
  74. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  75. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  76. package/dist/modules/catalog/components/products/variantForm.js +2 -1
  77. package/dist/modules/catalog/components/products/variantForm.js.map +2 -2
  78. package/dist/modules/communication_channels/api/post/test-seed/route.js +23 -2
  79. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  80. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +5 -0
  81. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  82. package/dist/modules/communication_channels/commands/set-primary-channel.js +2 -1
  83. package/dist/modules/communication_channels/commands/set-primary-channel.js.map +2 -2
  84. package/dist/modules/currencies/backend/currencies/[id]/page.js +6 -3
  85. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  86. package/dist/modules/currencies/backend/currencies/page.js +18 -11
  87. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  88. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +1 -0
  89. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  90. package/dist/modules/currencies/backend/exchange-rates/page.js +10 -6
  91. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  92. package/dist/modules/currencies/commands/currencies.js +7 -5
  93. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  94. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +26 -19
  95. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  96. package/dist/modules/customer_accounts/api/admin/roles/[id].js +28 -5
  97. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  98. package/dist/modules/customer_accounts/api/admin/roles.js +4 -2
  99. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  100. package/dist/modules/customer_accounts/api/admin/users/[id].js +28 -5
  101. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  102. package/dist/modules/customer_accounts/api/admin/users.js +2 -0
  103. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  104. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +16 -8
  105. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  106. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +8 -4
  107. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
  108. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +8 -4
  109. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +2 -2
  110. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +29 -18
  111. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  112. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +18 -11
  113. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  114. package/dist/modules/customers/api/companies/route.js +13 -2
  115. package/dist/modules/customers/api/companies/route.js.map +2 -2
  116. package/dist/modules/customers/api/deals/route.js +2 -0
  117. package/dist/modules/customers/api/deals/route.js.map +2 -2
  118. package/dist/modules/customers/api/people/route.js +11 -2
  119. package/dist/modules/customers/api/people/route.js.map +2 -2
  120. package/dist/modules/customers/api/todos/route.js +1 -0
  121. package/dist/modules/customers/api/todos/route.js.map +2 -2
  122. package/dist/modules/customers/backend/config/customers/deals/page.js.map +2 -2
  123. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +34 -21
  124. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  125. package/dist/modules/customers/backend/customers/companies/[id]/page.js +45 -27
  126. package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
  127. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  128. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +22 -5
  129. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  130. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.js +30 -8
  131. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.js.map +2 -2
  132. package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -0
  133. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  134. package/dist/modules/customers/backend/customers/deals/page.js +16 -6
  135. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  136. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +62 -39
  137. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  138. package/dist/modules/customers/backend/customers/people/[id]/page.js +41 -26
  139. package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
  140. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  141. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +50 -23
  142. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  143. package/dist/modules/customers/commands/addresses.js +16 -14
  144. package/dist/modules/customers/commands/addresses.js.map +2 -2
  145. package/dist/modules/customers/commands/companies.js +1 -1
  146. package/dist/modules/customers/commands/companies.js.map +2 -2
  147. package/dist/modules/customers/commands/interactions.js +41 -4
  148. package/dist/modules/customers/commands/interactions.js.map +2 -2
  149. package/dist/modules/customers/commands/people.js +1 -1
  150. package/dist/modules/customers/commands/people.js.map +2 -2
  151. package/dist/modules/customers/commands/personCompanyLinks.js +8 -5
  152. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  153. package/dist/modules/customers/commands/pipeline-stages.js +13 -11
  154. package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
  155. package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
  156. package/dist/modules/customers/components/DictionarySettings.js +20 -13
  157. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  158. package/dist/modules/customers/components/DictionarySortSettings.js +4 -0
  159. package/dist/modules/customers/components/DictionarySortSettings.js.map +2 -2
  160. package/dist/modules/customers/components/PipelineSettings.js +38 -23
  161. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  162. package/dist/modules/customers/components/detail/ActivityTimeline.js +1 -1
  163. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  164. package/dist/modules/customers/components/detail/AddressesSection.js +4 -0
  165. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  166. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +28 -22
  167. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  168. package/dist/modules/customers/components/detail/DealsSection.js +36 -24
  169. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  170. package/dist/modules/customers/components/detail/EmailCardActions.js +5 -0
  171. package/dist/modules/customers/components/detail/EmailCardActions.js.map +2 -2
  172. package/dist/modules/customers/components/detail/EntityTagsDialog.js +7 -0
  173. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
  174. package/dist/modules/customers/components/detail/ManageTagsDialog.js +34 -22
  175. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
  176. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +41 -29
  177. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  178. package/dist/modules/customers/components/detail/RoleAssignmentRow.js +14 -8
  179. package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
  180. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +14 -6
  181. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  182. package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js +29 -13
  183. package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js.map +2 -2
  184. package/dist/modules/customers/components/detail/hooks/useInteractions.js +77 -35
  185. package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
  186. package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +25 -17
  187. package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
  188. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  189. package/dist/modules/customers/components/formConfig.js.map +2 -2
  190. package/dist/modules/customers/data/guards.js +66 -0
  191. package/dist/modules/customers/data/guards.js.map +7 -0
  192. package/dist/modules/customers/di.js +37 -0
  193. package/dist/modules/customers/di.js.map +2 -2
  194. package/dist/modules/customers/lib/todoCompatibility.js +11 -0
  195. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  196. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  197. package/dist/modules/data_sync/api/options.js +4 -4
  198. package/dist/modules/data_sync/api/options.js.map +2 -2
  199. package/dist/modules/data_sync/api/schedules/route.js +9 -1
  200. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  201. package/dist/modules/data_sync/backend/data-sync/page.js +17 -8
  202. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  203. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +43 -22
  204. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  205. package/dist/modules/data_sync/lib/sync-schedule-service.js +9 -0
  206. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  207. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +8 -1
  208. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  209. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +17 -1
  210. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  211. package/dist/modules/dictionaries/components/DictionariesManager.js +31 -10
  212. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  213. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +28 -15
  214. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  215. package/dist/modules/directory/api/organizations/route.js +3 -0
  216. package/dist/modules/directory/api/organizations/route.js.map +2 -2
  217. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +2 -0
  218. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  219. package/dist/modules/directory/backend/directory/organizations/page.js +9 -5
  220. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  221. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +7 -3
  222. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  223. package/dist/modules/directory/backend/directory/tenants/page.js +8 -4
  224. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  225. package/dist/modules/directory/commands/organizations.js +7 -2
  226. package/dist/modules/directory/commands/organizations.js.map +2 -2
  227. package/dist/modules/entities/api/records.js +66 -0
  228. package/dist/modules/entities/api/records.js.map +2 -2
  229. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +1 -0
  230. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  231. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +8 -4
  232. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  233. package/dist/modules/entities/lib/helpers.js +17 -0
  234. package/dist/modules/entities/lib/helpers.js.map +2 -2
  235. package/dist/modules/feature_toggles/api/global/[id]/override/route.js +2 -1
  236. package/dist/modules/feature_toggles/api/global/[id]/override/route.js.map +2 -2
  237. package/dist/modules/feature_toggles/api/overrides/route.js +15 -0
  238. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  239. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js +15 -14
  240. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  241. package/dist/modules/feature_toggles/components/FeatureToggleOverrideCard.js +20 -12
  242. package/dist/modules/feature_toggles/components/FeatureToggleOverrideCard.js.map +2 -2
  243. package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +6 -2
  244. package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
  245. package/dist/modules/feature_toggles/components/formConfig.js +2 -1
  246. package/dist/modules/feature_toggles/components/formConfig.js.map +2 -2
  247. package/dist/modules/feature_toggles/components/overrideFormConfig.js +5 -1
  248. package/dist/modules/feature_toggles/components/overrideFormConfig.js.map +2 -2
  249. package/dist/modules/feature_toggles/data/validators.js +7 -4
  250. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  251. package/dist/modules/inbox_ops/api/settings/route.js +17 -2
  252. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  253. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +13 -8
  254. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
  255. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +9 -4
  256. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  257. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +18 -11
  258. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  259. package/dist/modules/integrations/backend/integrations/page.js +12 -8
  260. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  261. package/dist/modules/messages/commands/messages.js +13 -10
  262. package/dist/modules/messages/commands/messages.js.map +2 -2
  263. package/dist/modules/perspectives/api/[tableId]/route.js +39 -30
  264. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  265. package/dist/modules/perspectives/services/perspectiveService.js +7 -0
  266. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  267. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +6 -14
  268. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +3 -3
  269. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +4 -2
  270. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  271. package/dist/modules/planner/components/AvailabilityRuleSetForm.js +2 -0
  272. package/dist/modules/planner/components/AvailabilityRuleSetForm.js.map +2 -2
  273. package/dist/modules/planner/components/AvailabilityRulesEditor.js +36 -11
  274. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  275. package/dist/modules/planner/components/AvailabilitySchedule.js +9 -5
  276. package/dist/modules/planner/components/AvailabilitySchedule.js.map +2 -2
  277. package/dist/modules/query_index/lib/engine.js +19 -0
  278. package/dist/modules/query_index/lib/engine.js.map +2 -2
  279. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -0
  280. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  281. package/dist/modules/resources/backend/resources/resource-types/page.js +4 -2
  282. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  283. package/dist/modules/resources/backend/resources/resources/[id]/page.js +14 -3
  284. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  285. package/dist/modules/resources/backend/resources/resources/page.js +8 -4
  286. package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
  287. package/dist/modules/resources/components/ResourceCrudForm.js +2 -0
  288. package/dist/modules/resources/components/ResourceCrudForm.js.map +2 -2
  289. package/dist/modules/resources/components/ResourceTypeCrudForm.js +1 -0
  290. package/dist/modules/resources/components/ResourceTypeCrudForm.js.map +2 -2
  291. package/dist/modules/sales/api/documents/factory.js +7 -2
  292. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  293. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +3 -1
  294. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  295. package/dist/modules/sales/backend/sales/channels/offers/page.js +13 -4
  296. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  297. package/dist/modules/sales/backend/sales/channels/page.js +16 -4
  298. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  299. package/dist/modules/sales/backend/sales/documents/[id]/page.js +68 -22
  300. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  301. package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
  302. package/dist/modules/sales/commands/documentAddresses.js +181 -2
  303. package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
  304. package/dist/modules/sales/commands/documents.js +29 -1
  305. package/dist/modules/sales/commands/documents.js.map +2 -2
  306. package/dist/modules/sales/commands/returns.js +12 -2
  307. package/dist/modules/sales/commands/returns.js.map +2 -2
  308. package/dist/modules/sales/commands/shared.js +15 -0
  309. package/dist/modules/sales/commands/shared.js.map +2 -2
  310. package/dist/modules/sales/commands/shipments.js +4 -1
  311. package/dist/modules/sales/commands/shipments.js.map +2 -2
  312. package/dist/modules/sales/components/AdjustmentKindSettings.js +19 -11
  313. package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
  314. package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
  315. package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
  316. package/dist/modules/sales/components/PaymentMethodsSettings.js +12 -4
  317. package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
  318. package/dist/modules/sales/components/ShippingMethodsSettings.js +12 -4
  319. package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
  320. package/dist/modules/sales/components/StatusSettings.js +18 -11
  321. package/dist/modules/sales/components/StatusSettings.js.map +2 -2
  322. package/dist/modules/sales/components/TaxRatesSettings.js +12 -4
  323. package/dist/modules/sales/components/TaxRatesSettings.js.map +2 -2
  324. package/dist/modules/sales/components/channels/ChannelOfferForm.js +47 -16
  325. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  326. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +8 -4
  327. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
  328. package/dist/modules/sales/components/documents/AddressesSection.js +44 -25
  329. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  330. package/dist/modules/sales/components/documents/AdjustmentsSection.js +43 -23
  331. package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
  332. package/dist/modules/sales/components/documents/ItemsSection.js +22 -13
  333. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  334. package/dist/modules/sales/components/documents/LineItemDialog.js +23 -10
  335. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  336. package/dist/modules/sales/components/documents/PaymentDialog.js +29 -14
  337. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  338. package/dist/modules/sales/components/documents/PaymentsSection.js +20 -10
  339. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  340. package/dist/modules/sales/components/documents/ReturnDialog.js +26 -17
  341. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  342. package/dist/modules/sales/components/documents/ReturnsSection.js +3 -1
  343. package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
  344. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -5
  345. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  346. package/dist/modules/sales/components/documents/ShipmentDialog.js +21 -7
  347. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  348. package/dist/modules/sales/components/documents/ShipmentsSection.js +19 -10
  349. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  350. package/dist/modules/sales/components/documents/optimisticLock.js +27 -0
  351. package/dist/modules/sales/components/documents/optimisticLock.js.map +7 -0
  352. package/dist/modules/sales/di.js +18 -0
  353. package/dist/modules/sales/di.js.map +2 -2
  354. package/dist/modules/staff/api/job-histories.js +11 -2
  355. package/dist/modules/staff/api/job-histories.js.map +2 -2
  356. package/dist/modules/staff/api/timesheets/time-entries/route.js +11 -4
  357. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
  358. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +13 -8
  359. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  360. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +2 -1
  361. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  362. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +7 -4
  363. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  364. package/dist/modules/staff/backend/staff/team-members/page.js +4 -2
  365. package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
  366. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +1 -0
  367. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  368. package/dist/modules/staff/backend/staff/team-roles/page.js +4 -2
  369. package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
  370. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +5 -2
  371. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  372. package/dist/modules/staff/backend/staff/teams/page.js +12 -3
  373. package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
  374. package/dist/modules/staff/backend/staff/timesheets/page.js +4 -1
  375. package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
  376. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +2 -2
  377. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +12 -3
  378. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
  379. package/dist/modules/staff/commands/job-histories.js +40 -3
  380. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  381. package/dist/modules/staff/components/LeaveRequestForm.js +1 -0
  382. package/dist/modules/staff/components/LeaveRequestForm.js.map +2 -2
  383. package/dist/modules/staff/components/TeamForm.js +1 -0
  384. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  385. package/dist/modules/staff/components/TeamMemberForm.js +1 -0
  386. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  387. package/dist/modules/staff/components/TeamRoleForm.js +1 -0
  388. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  389. package/dist/modules/staff/components/detail/JobHistorySection.js +20 -7
  390. package/dist/modules/staff/components/detail/JobHistorySection.js.map +2 -2
  391. package/dist/modules/staff/data/validators.js +7 -1
  392. package/dist/modules/staff/data/validators.js.map +2 -2
  393. package/dist/modules/staff/lib/leaveRequestHelpers.js +2 -1
  394. package/dist/modules/staff/lib/leaveRequestHelpers.js.map +2 -2
  395. package/dist/modules/translations/components/TranslationManager.js +12 -8
  396. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  397. package/dist/modules/workflows/api/definitions/[id]/route.js +106 -0
  398. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  399. package/dist/modules/workflows/backend/definitions/[id]/page.js +11 -3
  400. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  401. package/dist/modules/workflows/backend/definitions/page.js +19 -8
  402. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  403. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +29 -16
  404. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  405. package/dist/modules/workflows/components/formConfig.js +4 -1
  406. package/dist/modules/workflows/components/formConfig.js.map +2 -2
  407. package/dist/modules/workflows/di.js +12 -0
  408. package/dist/modules/workflows/di.js.map +2 -2
  409. package/generated/entities/role/index.ts +1 -0
  410. package/generated/entities/user/index.ts +1 -0
  411. package/generated/entity-fields-registry.ts +2 -0
  412. package/jest.setup.ts +17 -0
  413. package/package.json +8 -7
  414. package/src/helpers/integration/communicationChannelsFixtures.ts +6 -0
  415. package/src/helpers/integration/dbFixtures.ts +1 -1
  416. package/src/helpers/integration/optimisticLockUi.ts +172 -0
  417. package/src/helpers/integration/salesFixtures.ts +29 -0
  418. package/src/modules/api_keys/backend/api-keys/page.tsx +10 -5
  419. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +19 -9
  420. package/src/modules/auth/api/roles/acl/route.ts +37 -11
  421. package/src/modules/auth/api/roles/route.ts +2 -0
  422. package/src/modules/auth/api/sidebar/preferences/route.ts +73 -0
  423. package/src/modules/auth/api/users/acl/route.ts +46 -18
  424. package/src/modules/auth/api/users/route.ts +2 -0
  425. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +29 -4
  426. package/src/modules/auth/backend/roles/page.tsx +9 -4
  427. package/src/modules/auth/backend/users/[id]/edit/page.tsx +37 -4
  428. package/src/modules/auth/backend/users/page.tsx +7 -2
  429. package/src/modules/auth/components/AclEditor.tsx +10 -1
  430. package/src/modules/auth/data/entities.ts +7 -1
  431. package/src/modules/auth/services/sidebarPreferencesService.ts +38 -4
  432. package/src/modules/business_rules/api/rules/route.ts +30 -0
  433. package/src/modules/business_rules/api/sets/route.ts +30 -0
  434. package/src/modules/business_rules/backend/rules/[id]/page.tsx +16 -4
  435. package/src/modules/business_rules/backend/rules/page.tsx +20 -11
  436. package/src/modules/business_rules/backend/sets/[id]/page.tsx +16 -4
  437. package/src/modules/business_rules/backend/sets/page.tsx +20 -11
  438. package/src/modules/catalog/api/categories/route.ts +3 -0
  439. package/src/modules/catalog/api/products/route.ts +4 -0
  440. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +5 -0
  441. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +112 -35
  442. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +56 -7
  443. package/src/modules/catalog/backend/catalog/products/optionSchemaClient.ts +2 -0
  444. package/src/modules/catalog/commands/variants.ts +32 -32
  445. package/src/modules/catalog/components/PriceKindSettings.tsx +20 -7
  446. package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +1 -0
  447. package/src/modules/catalog/components/products/ProductMediaManager.tsx +2 -0
  448. package/src/modules/catalog/components/products/ProductsDataTable.tsx +8 -4
  449. package/src/modules/catalog/components/products/productForm.ts +3 -0
  450. package/src/modules/catalog/components/products/variantForm.ts +9 -0
  451. package/src/modules/communication_channels/api/post/test-seed/route.ts +28 -1
  452. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +5 -0
  453. package/src/modules/communication_channels/commands/set-primary-channel.ts +10 -7
  454. package/src/modules/currencies/backend/currencies/[id]/page.tsx +13 -6
  455. package/src/modules/currencies/backend/currencies/page.tsx +18 -11
  456. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +3 -0
  457. package/src/modules/currencies/backend/exchange-rates/page.tsx +10 -6
  458. package/src/modules/currencies/commands/currencies.ts +10 -5
  459. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +31 -21
  460. package/src/modules/customer_accounts/api/admin/roles/[id].ts +35 -5
  461. package/src/modules/customer_accounts/api/admin/roles.ts +2 -0
  462. package/src/modules/customer_accounts/api/admin/users/[id].ts +38 -5
  463. package/src/modules/customer_accounts/api/admin/users.ts +2 -0
  464. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +34 -20
  465. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +9 -4
  466. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +11 -4
  467. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +28 -17
  468. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +19 -11
  469. package/src/modules/customers/AGENTS.md +2 -2
  470. package/src/modules/customers/api/companies/route.ts +14 -1
  471. package/src/modules/customers/api/deals/route.ts +3 -0
  472. package/src/modules/customers/api/people/route.ts +12 -1
  473. package/src/modules/customers/api/todos/route.ts +1 -0
  474. package/src/modules/customers/backend/config/customers/deals/page.tsx +1 -0
  475. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +36 -21
  476. package/src/modules/customers/backend/customers/companies/[id]/page.tsx +52 -27
  477. package/src/modules/customers/backend/customers/companies/page.tsx +2 -0
  478. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +27 -5
  479. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.ts +39 -7
  480. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -0
  481. package/src/modules/customers/backend/customers/deals/page.tsx +18 -6
  482. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +64 -39
  483. package/src/modules/customers/backend/customers/people/[id]/page.tsx +46 -26
  484. package/src/modules/customers/backend/customers/people/page.tsx +2 -0
  485. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +84 -24
  486. package/src/modules/customers/commands/addresses.ts +16 -14
  487. package/src/modules/customers/commands/companies.ts +3 -1
  488. package/src/modules/customers/commands/interactions.ts +50 -4
  489. package/src/modules/customers/commands/people.ts +2 -1
  490. package/src/modules/customers/commands/personCompanyLinks.ts +8 -5
  491. package/src/modules/customers/commands/pipeline-stages.ts +16 -16
  492. package/src/modules/customers/components/AddressFormatSettings.tsx +1 -0
  493. package/src/modules/customers/components/DictionarySettings.tsx +18 -13
  494. package/src/modules/customers/components/DictionarySortSettings.tsx +4 -0
  495. package/src/modules/customers/components/PipelineSettings.tsx +42 -21
  496. package/src/modules/customers/components/detail/ActivityTimeline.tsx +3 -3
  497. package/src/modules/customers/components/detail/AddressesSection.tsx +4 -0
  498. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +2 -0
  499. package/src/modules/customers/components/detail/DealsSection.tsx +4 -0
  500. package/src/modules/customers/components/detail/EmailCardActions.tsx +5 -0
  501. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +7 -0
  502. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +4 -0
  503. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +4 -0
  504. package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +2 -0
  505. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +23 -7
  506. package/src/modules/customers/components/detail/hooks/useInteractionMutations.ts +25 -15
  507. package/src/modules/customers/components/detail/hooks/useInteractions.ts +76 -35
  508. package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +30 -17
  509. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +2 -0
  510. package/src/modules/customers/components/detail/types.ts +1 -0
  511. package/src/modules/customers/components/formConfig.tsx +2 -0
  512. package/src/modules/customers/data/guards.ts +67 -0
  513. package/src/modules/customers/di.ts +66 -0
  514. package/src/modules/customers/i18n/de.json +2 -0
  515. package/src/modules/customers/i18n/en.json +2 -0
  516. package/src/modules/customers/i18n/es.json +2 -0
  517. package/src/modules/customers/i18n/pl.json +2 -0
  518. package/src/modules/customers/lib/todoCompatibility.ts +14 -0
  519. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +2 -0
  520. package/src/modules/data_sync/api/options.ts +7 -4
  521. package/src/modules/data_sync/api/schedules/route.ts +9 -1
  522. package/src/modules/data_sync/backend/data-sync/page.tsx +18 -5
  523. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +46 -19
  524. package/src/modules/data_sync/lib/sync-schedule-service.ts +11 -0
  525. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +8 -1
  526. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +23 -0
  527. package/src/modules/dictionaries/components/DictionariesManager.tsx +32 -9
  528. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +30 -14
  529. package/src/modules/dictionaries/i18n/de.json +1 -0
  530. package/src/modules/dictionaries/i18n/en.json +1 -0
  531. package/src/modules/dictionaries/i18n/es.json +1 -0
  532. package/src/modules/dictionaries/i18n/pl.json +1 -0
  533. package/src/modules/directory/api/organizations/route.ts +3 -0
  534. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +8 -0
  535. package/src/modules/directory/backend/directory/organizations/page.tsx +10 -5
  536. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +16 -5
  537. package/src/modules/directory/backend/directory/tenants/page.tsx +8 -4
  538. package/src/modules/directory/commands/organizations.ts +7 -4
  539. package/src/modules/entities/api/records.ts +99 -0
  540. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +7 -0
  541. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +8 -4
  542. package/src/modules/entities/lib/helpers.ts +17 -0
  543. package/src/modules/feature_toggles/api/global/[id]/override/route.ts +1 -0
  544. package/src/modules/feature_toggles/api/overrides/route.ts +19 -0
  545. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +19 -13
  546. package/src/modules/feature_toggles/components/FeatureToggleOverrideCard.tsx +22 -12
  547. package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +7 -2
  548. package/src/modules/feature_toggles/components/formConfig.tsx +2 -1
  549. package/src/modules/feature_toggles/components/overrideFormConfig.tsx +10 -1
  550. package/src/modules/feature_toggles/data/validators.ts +11 -3
  551. package/src/modules/inbox_ops/api/settings/route.ts +18 -0
  552. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +15 -10
  553. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +9 -4
  554. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +20 -11
  555. package/src/modules/integrations/backend/integrations/page.tsx +13 -8
  556. package/src/modules/messages/commands/messages.ts +27 -15
  557. package/src/modules/perspectives/api/[tableId]/route.ts +11 -2
  558. package/src/modules/perspectives/services/perspectiveService.ts +13 -1
  559. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +16 -14
  560. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +6 -3
  561. package/src/modules/planner/components/AvailabilityRuleSetForm.tsx +3 -0
  562. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +58 -15
  563. package/src/modules/planner/components/AvailabilitySchedule.tsx +22 -7
  564. package/src/modules/query_index/lib/engine.ts +34 -0
  565. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +7 -1
  566. package/src/modules/resources/backend/resources/resource-types/page.tsx +6 -3
  567. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +23 -3
  568. package/src/modules/resources/backend/resources/resources/page.tsx +15 -4
  569. package/src/modules/resources/components/ResourceCrudForm.tsx +3 -0
  570. package/src/modules/resources/components/ResourceTypeCrudForm.tsx +2 -0
  571. package/src/modules/sales/api/documents/factory.ts +13 -1
  572. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +6 -0
  573. package/src/modules/sales/backend/sales/channels/offers/page.tsx +10 -4
  574. package/src/modules/sales/backend/sales/channels/page.tsx +19 -4
  575. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +73 -20
  576. package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -0
  577. package/src/modules/sales/commands/documentAddresses.ts +226 -4
  578. package/src/modules/sales/commands/documents.ts +28 -0
  579. package/src/modules/sales/commands/returns.ts +12 -3
  580. package/src/modules/sales/commands/shared.ts +36 -0
  581. package/src/modules/sales/commands/shipments.ts +17 -1
  582. package/src/modules/sales/components/AdjustmentKindSettings.tsx +20 -11
  583. package/src/modules/sales/components/DocumentNumberSettings.tsx +1 -0
  584. package/src/modules/sales/components/OrderEditingSettings.tsx +1 -0
  585. package/src/modules/sales/components/PaymentMethodsSettings.tsx +12 -4
  586. package/src/modules/sales/components/ShippingMethodsSettings.tsx +12 -4
  587. package/src/modules/sales/components/StatusSettings.tsx +20 -11
  588. package/src/modules/sales/components/TaxRatesSettings.tsx +12 -5
  589. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +67 -14
  590. package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +7 -4
  591. package/src/modules/sales/components/documents/AddressesSection.tsx +35 -25
  592. package/src/modules/sales/components/documents/AdjustmentsSection.tsx +50 -25
  593. package/src/modules/sales/components/documents/ItemsSection.tsx +24 -13
  594. package/src/modules/sales/components/documents/LineItemDialog.tsx +26 -9
  595. package/src/modules/sales/components/documents/PaymentDialog.tsx +33 -14
  596. package/src/modules/sales/components/documents/PaymentsSection.tsx +22 -10
  597. package/src/modules/sales/components/documents/ReturnDialog.tsx +28 -17
  598. package/src/modules/sales/components/documents/ReturnsSection.tsx +4 -1
  599. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +11 -4
  600. package/src/modules/sales/components/documents/ShipmentDialog.tsx +23 -8
  601. package/src/modules/sales/components/documents/ShipmentsSection.tsx +20 -10
  602. package/src/modules/sales/components/documents/optimisticLock.ts +34 -0
  603. package/src/modules/sales/components/documents/shipmentTypes.ts +1 -0
  604. package/src/modules/sales/di.ts +35 -0
  605. package/src/modules/sales/i18n/de.json +3 -0
  606. package/src/modules/sales/i18n/en.json +3 -0
  607. package/src/modules/sales/i18n/es.json +3 -0
  608. package/src/modules/sales/i18n/pl.json +3 -0
  609. package/src/modules/staff/api/job-histories.ts +12 -2
  610. package/src/modules/staff/api/timesheets/time-entries/route.ts +16 -4
  611. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +12 -7
  612. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +2 -0
  613. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +16 -5
  614. package/src/modules/staff/backend/staff/team-members/page.tsx +6 -2
  615. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +8 -0
  616. package/src/modules/staff/backend/staff/team-roles/page.tsx +6 -2
  617. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +13 -3
  618. package/src/modules/staff/backend/staff/teams/page.tsx +9 -3
  619. package/src/modules/staff/backend/staff/timesheets/page.tsx +10 -1
  620. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +4 -0
  621. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +9 -3
  622. package/src/modules/staff/commands/job-histories.ts +42 -3
  623. package/src/modules/staff/components/LeaveRequestForm.tsx +2 -0
  624. package/src/modules/staff/components/TeamForm.tsx +2 -0
  625. package/src/modules/staff/components/TeamMemberForm.tsx +2 -0
  626. package/src/modules/staff/components/TeamRoleForm.tsx +2 -0
  627. package/src/modules/staff/components/detail/JobHistorySection.tsx +28 -6
  628. package/src/modules/staff/data/validators.ts +6 -0
  629. package/src/modules/staff/i18n/de.json +1 -0
  630. package/src/modules/staff/i18n/en.json +1 -0
  631. package/src/modules/staff/i18n/es.json +1 -0
  632. package/src/modules/staff/i18n/pl.json +1 -0
  633. package/src/modules/staff/lib/leaveRequestHelpers.ts +4 -0
  634. package/src/modules/translations/components/TranslationManager.tsx +13 -8
  635. package/src/modules/workflows/api/definitions/[id]/route.ts +112 -0
  636. package/src/modules/workflows/backend/definitions/[id]/page.tsx +20 -4
  637. package/src/modules/workflows/backend/definitions/page.tsx +20 -9
  638. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +29 -16
  639. package/src/modules/workflows/components/formConfig.tsx +5 -0
  640. package/src/modules/workflows/di.ts +20 -0
  641. package/src/modules/workflows/i18n/de.json +1 -0
  642. package/src/modules/workflows/i18n/en.json +1 -0
  643. package/src/modules/workflows/i18n/es.json +1 -0
  644. package/src/modules/workflows/i18n/pl.json +1 -0
@@ -8,13 +8,17 @@ import {
8
8
  sidebarPreferencesScopeSchema
9
9
  } from "../../../data/validators.js";
10
10
  import {
11
+ loadRoleSidebarPreferenceUpdatedAt,
11
12
  loadRoleSidebarPreferences,
12
13
  loadSidebarPreference,
14
+ loadSidebarPreferenceUpdatedAt,
13
15
  saveRoleSidebarPreference,
14
16
  saveSidebarPreference
15
17
  } from "../../../services/sidebarPreferencesService.js";
16
18
  import { SIDEBAR_PREFERENCES_VERSION } from "@open-mercato/shared/modules/navigation/sidebarPreferences";
17
19
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
20
+ import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
21
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
18
22
  import { Role, RoleSidebarPreference } from "../../../data/entities.js";
19
23
  import { z } from "zod";
20
24
  const metadata = {
@@ -40,7 +44,8 @@ const sidebarPreferencesResponseSchema = z.object({
40
44
  settings: sidebarSettingsSchema,
41
45
  canApplyToRoles: z.boolean(),
42
46
  roles: z.array(sidebarRoleEntrySchema),
43
- scope: sidebarPreferencesScopeSchema
47
+ scope: sidebarPreferencesScopeSchema,
48
+ updatedAt: z.string().datetime().nullable()
44
49
  });
45
50
  const sidebarPreferencesUpdateResponseSchema = sidebarPreferencesResponseSchema.extend({
46
51
  appliedRoles: z.array(z.string().uuid()),
@@ -127,6 +132,11 @@ async function GET(req) {
127
132
  });
128
133
  const pref = rolePrefs.get(role.id) ?? null;
129
134
  const rolesPayload2 = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale });
135
+ const roleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {
136
+ roleId: role.id,
137
+ tenantId: auth.tenantId ?? null,
138
+ locale
139
+ });
130
140
  return NextResponse.json({
131
141
  locale,
132
142
  settings: pref ? {
@@ -139,7 +149,8 @@ async function GET(req) {
139
149
  } : emptySettings(),
140
150
  canApplyToRoles,
141
151
  roles: rolesPayload2,
142
- scope: { type: "role", roleId: role.id }
152
+ scope: { type: "role", roleId: role.id },
153
+ updatedAt: roleVersion?.updatedAt ? roleVersion.updatedAt.toISOString() : null
143
154
  });
144
155
  }
145
156
  const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub;
@@ -150,6 +161,12 @@ async function GET(req) {
150
161
  locale
151
162
  }) : null;
152
163
  const rolesPayload = canApplyToRoles ? await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale }) : [];
164
+ const userVersion = effectiveUserId ? await loadSidebarPreferenceUpdatedAt(em, {
165
+ userId: effectiveUserId,
166
+ tenantId: auth.tenantId ?? null,
167
+ organizationId: auth.orgId ?? null,
168
+ locale
169
+ }) : null;
153
170
  return NextResponse.json({
154
171
  locale,
155
172
  settings: {
@@ -162,7 +179,8 @@ async function GET(req) {
162
179
  },
163
180
  canApplyToRoles,
164
181
  roles: rolesPayload,
165
- scope: { type: "user" }
182
+ scope: { type: "user" },
183
+ updatedAt: userVersion?.updatedAt ? userVersion.updatedAt.toISOString() : null
166
184
  });
167
185
  }
168
186
  async function PUT(req) {
@@ -257,11 +275,34 @@ async function PUT(req) {
257
275
  if (!role) {
258
276
  return NextResponse.json({ error: "Role not found" }, { status: 404 });
259
277
  }
278
+ const existingRolePref = await loadRoleSidebarPreferenceUpdatedAt(em, {
279
+ roleId: role.id,
280
+ tenantId: auth.tenantId ?? null,
281
+ locale
282
+ });
283
+ if (existingRolePref) {
284
+ try {
285
+ enforceCommandOptimisticLock({
286
+ resourceKind: "auth.role_sidebar_preference",
287
+ resourceId: existingRolePref.id,
288
+ current: existingRolePref.updatedAt ?? null,
289
+ request: req
290
+ });
291
+ } catch (err) {
292
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
293
+ throw err;
294
+ }
295
+ }
260
296
  const saved = await saveRoleSidebarPreference(em, {
261
297
  roleId: role.id,
262
298
  tenantId: auth.tenantId ?? null,
263
299
  locale
264
300
  }, payload);
301
+ const savedRoleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {
302
+ roleId: role.id,
303
+ tenantId: auth.tenantId ?? null,
304
+ locale
305
+ });
265
306
  if (cache?.deleteByTags) {
266
307
  try {
267
308
  await cache.deleteByTags([`nav:sidebar:role:${role.id}`]);
@@ -282,6 +323,7 @@ async function PUT(req) {
282
323
  canApplyToRoles,
283
324
  roles: rolesPayload2,
284
325
  scope: { type: "role", roleId: role.id },
326
+ updatedAt: savedRoleVersion?.updatedAt ? savedRoleVersion.updatedAt.toISOString() : null,
285
327
  appliedRoles: [],
286
328
  clearedRoles: []
287
329
  });
@@ -293,6 +335,25 @@ async function PUT(req) {
293
335
  if ((applyToRoles.length > 0 || clearRoleIds.length > 0) && !canApplyToRoles) {
294
336
  return NextResponse.json({ error: "Forbidden", requiredFeatures: [FEATURE_MANAGE] }, { status: 403 });
295
337
  }
338
+ const existingUserPref = await loadSidebarPreferenceUpdatedAt(em, {
339
+ userId: effectiveUserId,
340
+ tenantId: auth.tenantId ?? null,
341
+ organizationId: auth.orgId ?? null,
342
+ locale
343
+ });
344
+ if (existingUserPref) {
345
+ try {
346
+ enforceCommandOptimisticLock({
347
+ resourceKind: "auth.sidebar_preference",
348
+ resourceId: existingUserPref.id,
349
+ current: existingUserPref.updatedAt ?? null,
350
+ request: req
351
+ });
352
+ } catch (err) {
353
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
354
+ throw err;
355
+ }
356
+ }
296
357
  const settings = await saveSidebarPreference(em, {
297
358
  userId: effectiveUserId,
298
359
  tenantId: auth.tenantId ?? null,
@@ -367,12 +428,19 @@ async function PUT(req) {
367
428
  hasPreference: rolePrefs.has(role.id)
368
429
  }));
369
430
  }
431
+ const savedUserVersion = await loadSidebarPreferenceUpdatedAt(em, {
432
+ userId: effectiveUserId,
433
+ tenantId: auth.tenantId ?? null,
434
+ organizationId: auth.orgId ?? null,
435
+ locale
436
+ });
370
437
  return NextResponse.json({
371
438
  locale,
372
439
  settings,
373
440
  canApplyToRoles,
374
441
  roles: rolesPayload,
375
442
  scope: { type: "user" },
443
+ updatedAt: savedUserVersion?.updatedAt ? savedUserVersion.updatedAt.toISOString() : null,
376
444
  appliedRoles: updatedRoleIds,
377
445
  clearedRoles: filteredClearRoleIds
378
446
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/sidebar/preferences/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n sidebarPreferencesInputSchema,\n sidebarPreferencesScopeSchema,\n} from '../../../data/validators'\nimport {\n loadRoleSidebarPreferences,\n loadSidebarPreference,\n saveRoleSidebarPreference,\n saveSidebarPreference,\n} from '../../../services/sidebarPreferencesService'\nimport { SIDEBAR_PREFERENCES_VERSION } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { Role, RoleSidebarPreference } from '../../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n}\n\nconst sidebarSettingsSchema = z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n})\n\nconst sidebarRoleEntrySchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPreference: z.boolean(),\n})\n\nconst sidebarPreferencesResponseSchema = z.object({\n locale: z.string(),\n settings: sidebarSettingsSchema,\n canApplyToRoles: z.boolean(),\n roles: z.array(sidebarRoleEntrySchema),\n scope: sidebarPreferencesScopeSchema,\n})\n\nconst sidebarPreferencesUpdateResponseSchema = sidebarPreferencesResponseSchema.extend({\n appliedRoles: z.array(z.string().uuid()),\n clearedRoles: z.array(z.string().uuid()),\n})\n\nconst sidebarPreferencesDeleteResponseSchema = z.object({\n ok: z.literal(true),\n scope: sidebarPreferencesScopeSchema,\n})\n\nconst sidebarErrorSchema = z.object({\n error: z.string(),\n})\n\nconst FEATURE_MANAGE = 'auth.sidebar.manage'\n\ntype EmptySettings = {\n version: number\n groupOrder: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItems: string[]\n itemOrder: Record<string, string[]>\n}\n\nfunction emptySettings(): EmptySettings {\n return {\n version: SIDEBAR_PREFERENCES_VERSION,\n groupOrder: [],\n groupLabels: {},\n itemLabels: {},\n hiddenItems: [],\n itemOrder: {},\n }\n}\n\nasync function loadRolesPayload(\n em: EntityManager,\n options: { tenantId: string | null; locale: string },\n): Promise<Array<{ id: string; name: string; hasPreference: boolean }>> {\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: options.tenantId, organizationId: null },\n )\n if (roles.length === 0) return []\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: roles.map((r: Role) => r.id),\n tenantId: options.tenantId,\n locale: options.locale,\n })\n return roles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n}\n\nasync function findRoleInScope(\n em: EntityManager,\n options: { roleId: string; tenantId: string | null },\n): Promise<Role | null> {\n const role = await findOneWithDecryption(\n em,\n Role,\n { id: options.roleId },\n undefined,\n { tenantId: options.tenantId, organizationId: null },\n )\n if (!role) return null\n // Cross-tenant guard: a role belongs to either the auth tenant or the global (null tenant) pool.\n // Reject the lookup otherwise so a multi-tenant deployment can't leak across tenants.\n if (role.tenantId && options.tenantId && role.tenantId !== options.tenantId) return null\n if (role.tenantId && !options.tenantId) return null\n return role\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 url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n\n const { locale } = await resolveTranslations()\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const rbac = resolve('rbacService') as any\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n // Role-scoped read: requires `auth.sidebar.manage`.\n if (roleIdParam) {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: [role.id],\n tenantId: auth.tenantId ?? null,\n locale,\n })\n const pref = rolePrefs.get(role.id) ?? null\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n return NextResponse.json({\n locale,\n settings: pref\n ? {\n version: pref.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: pref.groupOrder ?? [],\n groupLabels: pref.groupLabels ?? {},\n itemLabels: pref.itemLabels ?? {},\n hiddenItems: pref.hiddenItems ?? [],\n itemOrder: pref.itemOrder ?? {},\n }\n : emptySettings(),\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n })\n }\n\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n const settings = effectiveUserId\n ? await loadSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n const rolesPayload = canApplyToRoles\n ? await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n : []\n\n return NextResponse.json({\n locale,\n settings: {\n version: settings?.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: settings?.groupOrder ?? [],\n groupLabels: settings?.groupLabels ?? {},\n itemLabels: settings?.itemLabels ?? {},\n hiddenItems: settings?.hiddenItems ?? [],\n itemOrder: settings?.itemOrder ?? {},\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n })\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 // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (!effectiveUserId) {\n return NextResponse.json({ error: 'Cannot save preferences: no user associated with this API key' }, { status: 403 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = sidebarPreferencesInputSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const sanitizeRecord = (record?: Record<string, string>) => {\n if (!record) return {}\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n const trimmedKey = key.trim()\n const trimmedValue = value.trim()\n if (!trimmedKey || !trimmedValue) continue\n result[trimmedKey] = trimmedValue\n }\n return result\n }\n\n const groupOrderSource = parsed.data.groupOrder ?? []\n const seen = new Set<string>()\n const groupOrder: string[] = []\n for (const id of groupOrderSource) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n groupOrder.push(trimmed)\n }\n\n const payload = {\n version: parsed.data.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder,\n groupLabels: sanitizeRecord(parsed.data.groupLabels),\n itemLabels: sanitizeRecord(parsed.data.itemLabels),\n hiddenItems: (() => {\n const source = parsed.data.hiddenItems ?? []\n const seenHidden = new Set<string>()\n const values: string[] = []\n for (const href of source) {\n const trimmed = href.trim()\n if (!trimmed || seenHidden.has(trimmed)) continue\n seenHidden.add(trimmed)\n values.push(trimmed)\n }\n return values\n })(),\n itemOrder: (() => {\n const source = parsed.data.itemOrder ?? {}\n const out: Record<string, string[]> = {}\n for (const [groupKey, list] of Object.entries(source)) {\n const trimmedGroup = groupKey.trim()\n if (!trimmedGroup) continue\n const seenItem = new Set<string>()\n const values: string[] = []\n for (const itemKey of list) {\n const trimmedItem = itemKey.trim()\n if (!trimmedItem || seenItem.has(trimmedItem)) continue\n seenItem.add(trimmedItem)\n values.push(trimmedItem)\n }\n if (values.length > 0) out[trimmedGroup] = values\n }\n return out\n })(),\n }\n\n const { locale } = await resolveTranslations()\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const scope = parsed.data.scope ?? { type: 'user' as const }\n\n // Role-scoped write: requires `auth.sidebar.manage` and a role visible to this tenant.\n // applyToRoles/clearRoleIds are forbidden in role scope (validator already rejects them).\n if (scope.type === 'role') {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: scope.roleId, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const saved = await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n return NextResponse.json({\n locale,\n settings: {\n version: saved?.version ?? payload.version,\n groupOrder: saved?.groupOrder ?? payload.groupOrder,\n groupLabels: saved?.groupLabels ?? payload.groupLabels,\n itemLabels: saved?.itemLabels ?? payload.itemLabels,\n hiddenItems: saved?.hiddenItems ?? payload.hiddenItems,\n itemOrder: saved?.itemOrder ?? payload.itemOrder,\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n appliedRoles: [],\n clearedRoles: [],\n })\n }\n\n const applyToRolesSource = parsed.data.applyToRoles ?? []\n const applyToRoles = Array.from(new Set(applyToRolesSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n const clearRoleIdsSource = parsed.data.clearRoleIds ?? []\n const clearRoleIds = Array.from(new Set(clearRoleIdsSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n\n if ((applyToRoles.length > 0 || clearRoleIds.length > 0) && !canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const settings = await saveSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n }, payload)\n\n const roleScope: FilterQuery<Role> = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: auth.tenantId ?? null, organizationId: null },\n )\n : []\n const roleMap = new Map<string, Role>(availableRoles.map((role: Role) => [String(role.id), role]))\n\n if (applyToRoles.length > 0) {\n const missing = applyToRoles.filter((id) => !roleMap.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const updatedRoleIds: string[] = []\n const filteredClearRoleIds: string[] = []\n await withAtomicFlush(em, [\n async () => {\n for (const roleId of applyToRoles) {\n const role = roleMap.get(roleId)!\n await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n updatedRoleIds.push(role.id)\n }\n\n const clearTargets = clearRoleIds.filter((id) => !updatedRoleIds.includes(id) && !applyToRoles.includes(id))\n filteredClearRoleIds.push(...clearTargets)\n\n if (filteredClearRoleIds.length > 0) {\n // Cross-locale: role preferences are unique per (role, tenantId); keep the delete\n // filter aligned with save/load helpers so a clear from one locale does not leave\n // a row created under another locale orphaned.\n await em.nativeDelete(RoleSidebarPreference, {\n role: { $in: filteredClearRoleIds },\n tenantId: auth.tenantId ?? null,\n })\n }\n },\n ], { transaction: true })\n\n if (filteredClearRoleIds.length > 0 && cache?.deleteByTags) {\n try {\n await cache.deleteByTags(filteredClearRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`))\n } catch {}\n }\n\n if (cache?.deleteByTags) {\n const tags = [\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${auth.tenantId ?? 'null'}:${auth.orgId ?? 'null'}:${locale}`,\n ...updatedRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`),\n ]\n try {\n await cache.deleteByTags(tags)\n } catch {}\n }\n\n let rolesPayload: Array<{ id: string; name: string; hasPreference: boolean }> = []\n if (canApplyToRoles) {\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: availableRoles.map((role: Role) => role.id),\n tenantId: auth.tenantId ?? null,\n locale,\n })\n rolesPayload = availableRoles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n }\n\n return NextResponse.json({\n locale,\n settings,\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n appliedRoles: updatedRoleIds,\n clearedRoles: filteredClearRoleIds,\n })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n if (!roleIdParam) {\n return NextResponse.json({ error: 'roleId query parameter is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n // Cross-locale: keep the delete filter aligned with save/load helpers (no locale).\n await em.nativeDelete(RoleSidebarPreference, {\n role: role.id,\n tenantId: auth.tenantId ?? null,\n })\n\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n\n return NextResponse.json({ ok: true, scope: { type: 'role', roleId: role.id } })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Sidebar preferences',\n methods: {\n GET: {\n summary: 'Get sidebar preferences',\n description: 'Returns sidebar customization for the current user (default) or the specified role (`?roleId=\u2026`, requires `auth.sidebar.manage`).',\n responses: [\n { status: 200, description: 'Current sidebar configuration', schema: sidebarPreferencesResponseSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-scope read', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar preferences',\n description: 'Updates sidebar configuration. With `scope.type === \"user\"` (default) writes the calling user\\'s personal preferences and may optionally apply the same settings to selected roles via `applyToRoles[]`. With `scope.type === \"role\"` writes the named role variant directly (requires `auth.sidebar.manage`); `applyToRoles[]` and `clearRoleIds[]` are rejected in this mode.',\n requestBody: {\n contentType: 'application/json',\n schema: sidebarPreferencesInputSchema,\n },\n responses: [\n { status: 200, description: 'Preferences saved', schema: sidebarPreferencesUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-wide updates', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete a role sidebar variant',\n description: 'Removes the role variant for the current tenant + locale. Idempotent. Requires `auth.sidebar.manage`.',\n responses: [\n { status: 200, description: 'Variant deleted (or never existed)', schema: sidebarPreferencesDeleteResponseSchema },\n { status: 400, description: 'Missing roleId query parameter', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n sidebarPreferencesInputSchema,\n sidebarPreferencesScopeSchema,\n} from '../../../data/validators'\nimport {\n loadRoleSidebarPreferenceUpdatedAt,\n loadRoleSidebarPreferences,\n loadSidebarPreference,\n loadSidebarPreferenceUpdatedAt,\n saveRoleSidebarPreference,\n saveSidebarPreference,\n} from '../../../services/sidebarPreferencesService'\nimport { SIDEBAR_PREFERENCES_VERSION } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { Role, RoleSidebarPreference } from '../../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n}\n\nconst sidebarSettingsSchema = z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n})\n\nconst sidebarRoleEntrySchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPreference: z.boolean(),\n})\n\nconst sidebarPreferencesResponseSchema = z.object({\n locale: z.string(),\n settings: sidebarSettingsSchema,\n canApplyToRoles: z.boolean(),\n roles: z.array(sidebarRoleEntrySchema),\n scope: sidebarPreferencesScopeSchema,\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst sidebarPreferencesUpdateResponseSchema = sidebarPreferencesResponseSchema.extend({\n appliedRoles: z.array(z.string().uuid()),\n clearedRoles: z.array(z.string().uuid()),\n})\n\nconst sidebarPreferencesDeleteResponseSchema = z.object({\n ok: z.literal(true),\n scope: sidebarPreferencesScopeSchema,\n})\n\nconst sidebarErrorSchema = z.object({\n error: z.string(),\n})\n\nconst FEATURE_MANAGE = 'auth.sidebar.manage'\n\ntype EmptySettings = {\n version: number\n groupOrder: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItems: string[]\n itemOrder: Record<string, string[]>\n}\n\nfunction emptySettings(): EmptySettings {\n return {\n version: SIDEBAR_PREFERENCES_VERSION,\n groupOrder: [],\n groupLabels: {},\n itemLabels: {},\n hiddenItems: [],\n itemOrder: {},\n }\n}\n\nasync function loadRolesPayload(\n em: EntityManager,\n options: { tenantId: string | null; locale: string },\n): Promise<Array<{ id: string; name: string; hasPreference: boolean }>> {\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: options.tenantId, organizationId: null },\n )\n if (roles.length === 0) return []\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: roles.map((r: Role) => r.id),\n tenantId: options.tenantId,\n locale: options.locale,\n })\n return roles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n}\n\nasync function findRoleInScope(\n em: EntityManager,\n options: { roleId: string; tenantId: string | null },\n): Promise<Role | null> {\n const role = await findOneWithDecryption(\n em,\n Role,\n { id: options.roleId },\n undefined,\n { tenantId: options.tenantId, organizationId: null },\n )\n if (!role) return null\n // Cross-tenant guard: a role belongs to either the auth tenant or the global (null tenant) pool.\n // Reject the lookup otherwise so a multi-tenant deployment can't leak across tenants.\n if (role.tenantId && options.tenantId && role.tenantId !== options.tenantId) return null\n if (role.tenantId && !options.tenantId) return null\n return role\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 url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n\n const { locale } = await resolveTranslations()\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const rbac = resolve('rbacService') as any\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n // Role-scoped read: requires `auth.sidebar.manage`.\n if (roleIdParam) {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: [role.id],\n tenantId: auth.tenantId ?? null,\n locale,\n })\n const pref = rolePrefs.get(role.id) ?? null\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n const roleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n return NextResponse.json({\n locale,\n settings: pref\n ? {\n version: pref.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: pref.groupOrder ?? [],\n groupLabels: pref.groupLabels ?? {},\n itemLabels: pref.itemLabels ?? {},\n hiddenItems: pref.hiddenItems ?? [],\n itemOrder: pref.itemOrder ?? {},\n }\n : emptySettings(),\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: roleVersion?.updatedAt ? roleVersion.updatedAt.toISOString() : null,\n })\n }\n\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n const settings = effectiveUserId\n ? await loadSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n const rolesPayload = canApplyToRoles\n ? await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n : []\n\n const userVersion = effectiveUserId\n ? await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n return NextResponse.json({\n locale,\n settings: {\n version: settings?.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: settings?.groupOrder ?? [],\n groupLabels: settings?.groupLabels ?? {},\n itemLabels: settings?.itemLabels ?? {},\n hiddenItems: settings?.hiddenItems ?? [],\n itemOrder: settings?.itemOrder ?? {},\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: userVersion?.updatedAt ? userVersion.updatedAt.toISOString() : null,\n })\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 // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (!effectiveUserId) {\n return NextResponse.json({ error: 'Cannot save preferences: no user associated with this API key' }, { status: 403 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = sidebarPreferencesInputSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const sanitizeRecord = (record?: Record<string, string>) => {\n if (!record) return {}\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n const trimmedKey = key.trim()\n const trimmedValue = value.trim()\n if (!trimmedKey || !trimmedValue) continue\n result[trimmedKey] = trimmedValue\n }\n return result\n }\n\n const groupOrderSource = parsed.data.groupOrder ?? []\n const seen = new Set<string>()\n const groupOrder: string[] = []\n for (const id of groupOrderSource) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n groupOrder.push(trimmed)\n }\n\n const payload = {\n version: parsed.data.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder,\n groupLabels: sanitizeRecord(parsed.data.groupLabels),\n itemLabels: sanitizeRecord(parsed.data.itemLabels),\n hiddenItems: (() => {\n const source = parsed.data.hiddenItems ?? []\n const seenHidden = new Set<string>()\n const values: string[] = []\n for (const href of source) {\n const trimmed = href.trim()\n if (!trimmed || seenHidden.has(trimmed)) continue\n seenHidden.add(trimmed)\n values.push(trimmed)\n }\n return values\n })(),\n itemOrder: (() => {\n const source = parsed.data.itemOrder ?? {}\n const out: Record<string, string[]> = {}\n for (const [groupKey, list] of Object.entries(source)) {\n const trimmedGroup = groupKey.trim()\n if (!trimmedGroup) continue\n const seenItem = new Set<string>()\n const values: string[] = []\n for (const itemKey of list) {\n const trimmedItem = itemKey.trim()\n if (!trimmedItem || seenItem.has(trimmedItem)) continue\n seenItem.add(trimmedItem)\n values.push(trimmedItem)\n }\n if (values.length > 0) out[trimmedGroup] = values\n }\n return out\n })(),\n }\n\n const { locale } = await resolveTranslations()\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const scope = parsed.data.scope ?? { type: 'user' as const }\n\n // Role-scoped write: requires `auth.sidebar.manage` and a role visible to this tenant.\n // applyToRoles/clearRoleIds are forbidden in role scope (validator already rejects them).\n if (scope.type === 'role') {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: scope.roleId, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const existingRolePref = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (existingRolePref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.role_sidebar_preference',\n resourceId: existingRolePref.id,\n current: existingRolePref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const saved = await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n const savedRoleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n return NextResponse.json({\n locale,\n settings: {\n version: saved?.version ?? payload.version,\n groupOrder: saved?.groupOrder ?? payload.groupOrder,\n groupLabels: saved?.groupLabels ?? payload.groupLabels,\n itemLabels: saved?.itemLabels ?? payload.itemLabels,\n hiddenItems: saved?.hiddenItems ?? payload.hiddenItems,\n itemOrder: saved?.itemOrder ?? payload.itemOrder,\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: savedRoleVersion?.updatedAt ? savedRoleVersion.updatedAt.toISOString() : null,\n appliedRoles: [],\n clearedRoles: [],\n })\n }\n\n const applyToRolesSource = parsed.data.applyToRoles ?? []\n const applyToRoles = Array.from(new Set(applyToRolesSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n const clearRoleIdsSource = parsed.data.clearRoleIds ?? []\n const clearRoleIds = Array.from(new Set(clearRoleIdsSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n\n if ((applyToRoles.length > 0 || clearRoleIds.length > 0) && !canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const existingUserPref = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n if (existingUserPref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.sidebar_preference',\n resourceId: existingUserPref.id,\n current: existingUserPref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const settings = await saveSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n }, payload)\n\n const roleScope: FilterQuery<Role> = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: auth.tenantId ?? null, organizationId: null },\n )\n : []\n const roleMap = new Map<string, Role>(availableRoles.map((role: Role) => [String(role.id), role]))\n\n if (applyToRoles.length > 0) {\n const missing = applyToRoles.filter((id) => !roleMap.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const updatedRoleIds: string[] = []\n const filteredClearRoleIds: string[] = []\n await withAtomicFlush(em, [\n async () => {\n for (const roleId of applyToRoles) {\n const role = roleMap.get(roleId)!\n await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n updatedRoleIds.push(role.id)\n }\n\n const clearTargets = clearRoleIds.filter((id) => !updatedRoleIds.includes(id) && !applyToRoles.includes(id))\n filteredClearRoleIds.push(...clearTargets)\n\n if (filteredClearRoleIds.length > 0) {\n // Cross-locale: role preferences are unique per (role, tenantId); keep the delete\n // filter aligned with save/load helpers so a clear from one locale does not leave\n // a row created under another locale orphaned.\n await em.nativeDelete(RoleSidebarPreference, {\n role: { $in: filteredClearRoleIds },\n tenantId: auth.tenantId ?? null,\n })\n }\n },\n ], { transaction: true })\n\n if (filteredClearRoleIds.length > 0 && cache?.deleteByTags) {\n try {\n await cache.deleteByTags(filteredClearRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`))\n } catch {}\n }\n\n if (cache?.deleteByTags) {\n const tags = [\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${auth.tenantId ?? 'null'}:${auth.orgId ?? 'null'}:${locale}`,\n ...updatedRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`),\n ]\n try {\n await cache.deleteByTags(tags)\n } catch {}\n }\n\n let rolesPayload: Array<{ id: string; name: string; hasPreference: boolean }> = []\n if (canApplyToRoles) {\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: availableRoles.map((role: Role) => role.id),\n tenantId: auth.tenantId ?? null,\n locale,\n })\n rolesPayload = availableRoles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n }\n\n const savedUserVersion = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n\n return NextResponse.json({\n locale,\n settings,\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: savedUserVersion?.updatedAt ? savedUserVersion.updatedAt.toISOString() : null,\n appliedRoles: updatedRoleIds,\n clearedRoles: filteredClearRoleIds,\n })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n if (!roleIdParam) {\n return NextResponse.json({ error: 'roleId query parameter is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n // Cross-locale: keep the delete filter aligned with save/load helpers (no locale).\n await em.nativeDelete(RoleSidebarPreference, {\n role: role.id,\n tenantId: auth.tenantId ?? null,\n })\n\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n\n return NextResponse.json({ ok: true, scope: { type: 'role', roleId: role.id } })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Sidebar preferences',\n methods: {\n GET: {\n summary: 'Get sidebar preferences',\n description: 'Returns sidebar customization for the current user (default) or the specified role (`?roleId=\u2026`, requires `auth.sidebar.manage`).',\n responses: [\n { status: 200, description: 'Current sidebar configuration', schema: sidebarPreferencesResponseSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-scope read', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar preferences',\n description: 'Updates sidebar configuration. With `scope.type === \"user\"` (default) writes the calling user\\'s personal preferences and may optionally apply the same settings to selected roles via `applyToRoles[]`. With `scope.type === \"role\"` writes the named role variant directly (requires `auth.sidebar.manage`); `applyToRoles[]` and `clearRoleIds[]` are rejected in this mode.',\n requestBody: {\n contentType: 'application/json',\n schema: sidebarPreferencesInputSchema,\n },\n responses: [\n { status: 200, description: 'Preferences saved', schema: sidebarPreferencesUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-wide updates', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete a role sidebar variant',\n description: 'Removes the role variant for the current tenant + locale. Idempotent. Requires `auth.sidebar.manage`.',\n responses: [\n { status: 200, description: 'Variant deleted (or never existed)', schema: sidebarPreferencesDeleteResponseSchema },\n { status: 400, description: 'Missing roleId query parameter', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["rolesPayload"]
7
7
  }
@@ -4,8 +4,9 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { logCrudAccess } from "@open-mercato/shared/lib/crud/factory";
6
6
  import { forbidden, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
- import { UserAcl } from "@open-mercato/core/modules/auth/data/entities";
7
+ import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
8
8
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
9
+ import { UserAcl } from "@open-mercato/core/modules/auth/data/entities";
9
10
  import { assertActorCanModifySuperAdminUserTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
10
11
  const getSchema = z.object({ userId: z.string().uuid() });
11
12
  const putSchema = z.object({
@@ -22,7 +23,8 @@ const userAclResponseSchema = z.object({
22
23
  hasCustomAcl: z.boolean(),
23
24
  isSuperAdmin: z.boolean(),
24
25
  features: z.array(z.string()),
25
- organizations: z.array(z.string()).nullable()
26
+ organizations: z.array(z.string()).nullable(),
27
+ updatedAt: z.string().nullable()
26
28
  });
27
29
  const userAclUpdateResponseSchema = z.object({
28
30
  ok: z.literal(true),
@@ -60,8 +62,9 @@ async function GET(req) {
60
62
  hasCustomAcl: true,
61
63
  isSuperAdmin: !!acl.isSuperAdmin,
62
64
  features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],
63
- organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null
64
- } : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null };
65
+ organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,
66
+ updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null
67
+ } : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: null };
65
68
  await logCrudAccess({
66
69
  container,
67
70
  auth,
@@ -106,6 +109,19 @@ async function PUT(req) {
106
109
  const requestedFeatures = normalizeFeatureList(parsed.data.features);
107
110
  const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null;
108
111
  let acl = await em.findOne(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
112
+ if (acl) {
113
+ try {
114
+ enforceCommandOptimisticLock({
115
+ resourceKind: "auth.user_acl",
116
+ resourceId: acl.id,
117
+ current: acl.updatedAt ?? null,
118
+ request: req
119
+ });
120
+ } catch (err) {
121
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
122
+ throw err;
123
+ }
124
+ }
109
125
  const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false;
110
126
  const existingFeatures = acl && Array.isArray(acl.featuresJson) ? normalizeFeatureList(acl.featuresJson) : [];
111
127
  const effectiveFeatures = actorIsSuperAdmin ? requestedFeatures : sanitizeTenantFeatures(requestedFeatures);
@@ -122,22 +138,29 @@ async function PUT(req) {
122
138
  }
123
139
  }
124
140
  const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0;
125
- await withAtomicFlush(em, [
126
- () => {
127
- if (!hasCustomAcl) {
128
- if (acl) em.remove(acl);
129
- } else {
130
- if (!acl) {
131
- acl = em.create(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
132
- }
133
- const aclRecord = acl;
134
- aclRecord.isSuperAdmin = effectiveIsSuperAdmin;
135
- aclRecord.featuresJson = effectiveFeatures;
136
- aclRecord.organizationsJson = organizations;
137
- em.persist(acl);
138
- }
141
+ if (!hasCustomAcl) {
142
+ if (acl) {
143
+ const aclToRemove = acl;
144
+ await withAtomicFlush(em, [() => em.remove(aclToRemove)], { transaction: true });
139
145
  }
140
- ], { transaction: true });
146
+ } else {
147
+ if (!acl) {
148
+ acl = em.create(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
149
+ }
150
+ const aclRecord = acl;
151
+ await withAtomicFlush(
152
+ em,
153
+ [
154
+ () => {
155
+ aclRecord.isSuperAdmin = effectiveIsSuperAdmin;
156
+ aclRecord.featuresJson = effectiveFeatures;
157
+ aclRecord.organizationsJson = organizations;
158
+ em.persist(aclRecord);
159
+ }
160
+ ],
161
+ { transaction: true }
162
+ );
163
+ }
141
164
  await rbacService.invalidateUserCache(parsed.data.userId);
142
165
  try {
143
166
  const cache = container.resolve("cache");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/users/acl/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { assertActorCanModifySuperAdminUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst getSchema = z.object({ userId: z.string().uuid() })\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n if (!actorAcl?.isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n query: { userId: parsed.data.userId },\n accessType: 'read:item',\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 const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const requestedFeatures = normalizeFeatureList(parsed.data.features)\n const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl && Array.isArray(acl.featuresJson) ? normalizeFeatureList(acl.featuresJson) : []\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n await withAtomicFlush(em, [\n () => {\n if (!hasCustomAcl) {\n if (acl) em.remove(acl)\n } else {\n if (!acl) {\n acl = em.create(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n }\n const aclRecord = acl as any\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n em.persist(acl)\n }\n },\n ], { transaction: true })\n\n // Invalidate cache for this user\n await rbacService.invalidateUserCache(parsed.data.userId)\n try {\n const cache = container.resolve('cache') as any\n if (cache) await cache.deleteByTags([`rbac:user:${parsed.data.userId}`])\n } catch {}\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeFeatureList(features: unknown): string[] {\n if (!Array.isArray(features)) return []\n const dedup = new Set<string>()\n for (const value of features) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed) continue\n dedup.add(trimmed)\n }\n return Array.from(dedup)\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user within the current tenant, if any.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Configures per-user ACL overrides, including super admin access, feature list, and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport { assertActorCanModifySuperAdminUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst getSchema = z.object({ userId: z.string().uuid() })\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n updatedAt: z.string().nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n if (!actorAcl?.isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n query: { userId: parsed.data.userId },\n accessType: 'read:item',\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 const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const requestedFeatures = normalizeFeatureList(parsed.data.features)\n const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n // Optimistic lock: refuse a stale per-user ACL overwrite so concurrent edits\n // cannot silently clobber each other (#2055). Strictly additive \u2014 a no-op when\n // the client sends no expected-version header; skipped when no ACL row exists.\n if (acl) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.user_acl',\n resourceId: acl.id,\n current: acl.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl && Array.isArray(acl.featuresJson) ? normalizeFeatureList(acl.featuresJson) : []\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n // Persist the ACL mutation inside a transaction so the per-user permission\n // write (or removal) commits atomically (proper ACL-edit transaction handling).\n if (!hasCustomAcl) {\n if (acl) {\n const aclToRemove = acl\n await withAtomicFlush(em, [() => em.remove(aclToRemove)], { transaction: true })\n }\n } else {\n if (!acl) {\n acl = em.create(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n }\n const aclRecord = acl as any\n await withAtomicFlush(\n em,\n [\n () => {\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n em.persist(aclRecord)\n },\n ],\n { transaction: true },\n )\n }\n\n // Invalidate cache for this user\n await rbacService.invalidateUserCache(parsed.data.userId)\n try {\n const cache = container.resolve('cache') as any\n if (cache) await cache.deleteByTags([`rbac:user:${parsed.data.userId}`])\n } catch {}\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeFeatureList(features: unknown): string[] {\n if (!Array.isArray(features)) return []\n const dedup = new Set<string>()\n for (const value of features) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed) continue\n dedup.add(trimmed)\n }\n return Array.from(dedup)\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user within the current tenant, if any.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Configures per-user ACL overrides, including super admin access, feature list, and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -68,7 +68,8 @@ const userListItemSchema = z.object({
68
68
  tenantId: z.string().uuid().nullable(),
69
69
  tenantName: z.string().nullable(),
70
70
  roles: z.array(z.string()),
71
- roleIds: z.array(z.string().uuid()).optional()
71
+ roleIds: z.array(z.string().uuid()).optional(),
72
+ updatedAt: z.string().nullable().optional()
72
73
  });
73
74
  const userListResponseSchema = z.object({
74
75
  items: z.array(userListItemSchema),
@@ -385,6 +386,7 @@ async function GET(req) {
385
386
  roles: roleMap[uid] || [],
386
387
  roleIds: roleIdMap[uid] || [],
387
388
  hasPassword: !!u.passwordHash,
389
+ updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : null,
388
390
  ...cfByUser[uid] || {}
389
391
  };
390
392
  });