@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b

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 (253) hide show
  1. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
  2. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  3. package/dist/modules/auth/api/logout.js +2 -3
  4. package/dist/modules/auth/api/logout.js.map +2 -2
  5. package/dist/modules/auth/api/roles/acl/route.js +2 -2
  6. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  7. package/dist/modules/auth/api/session/refresh.js +5 -6
  8. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  9. package/dist/modules/auth/api/users/acl/route.js +2 -2
  10. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  11. package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
  12. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/data/entities.js +2 -2
  16. package/dist/modules/auth/data/entities.js.map +2 -2
  17. package/dist/modules/auth/lib/requestRedirect.js +31 -3
  18. package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
  19. package/dist/modules/business_rules/api/rules/route.js +3 -3
  20. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  21. package/dist/modules/business_rules/api/sets/route.js +3 -3
  22. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  23. package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
  24. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  25. package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
  26. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  27. package/dist/modules/catalog/api/product-media/route.js.map +2 -2
  28. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
  29. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  30. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  31. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  32. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
  33. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  34. package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
  35. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  36. package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
  37. package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
  38. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  39. package/dist/modules/currencies/backend/currencies/page.js +2 -1
  40. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  41. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
  42. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  43. package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
  44. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  45. package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
  46. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  47. package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
  48. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  49. package/dist/modules/customers/api/addresses/route.js +4 -2
  50. package/dist/modules/customers/api/addresses/route.js.map +2 -2
  51. package/dist/modules/customers/api/tags/route.js +8 -3
  52. package/dist/modules/customers/api/tags/route.js.map +2 -2
  53. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
  54. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  55. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
  56. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  57. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
  58. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  59. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
  60. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
  61. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
  62. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  63. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
  64. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  65. package/dist/modules/customers/commands/interactions.js +5 -5
  66. package/dist/modules/customers/commands/interactions.js.map +2 -2
  67. package/dist/modules/customers/commands/pipeline-stages.js +24 -2
  68. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  69. package/dist/modules/customers/commands/pipelines.js +24 -2
  70. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  71. package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
  72. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  73. package/dist/modules/customers/components/detail/DealForm.js +5 -1
  74. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  75. package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
  76. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  77. package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
  78. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  79. package/dist/modules/data_sync/api/schedules/route.js +1 -1
  80. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  81. package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
  82. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  83. package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
  84. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  85. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
  86. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  87. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
  88. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  89. package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
  90. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  91. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  92. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
  93. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  94. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
  95. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  96. package/dist/modules/entities/api/entities.js +2 -2
  97. package/dist/modules/entities/api/entities.js.map +2 -2
  98. package/dist/modules/entities/api/records.js +7 -5
  99. package/dist/modules/entities/api/records.js.map +2 -2
  100. package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
  101. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  102. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  103. package/dist/modules/feature_toggles/data/entities.js +1 -1
  104. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  105. package/dist/modules/feature_toggles/data/validators.js +2 -1
  106. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  107. package/dist/modules/feature_toggles/lib/queries.js +2 -1
  108. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  109. package/dist/modules/inbox_ops/api/settings/route.js +2 -2
  110. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  111. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
  112. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
  116. package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
  118. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  119. package/dist/modules/sales/commands/documents.js +11 -10
  120. package/dist/modules/sales/commands/documents.js.map +2 -2
  121. package/dist/modules/sales/commands/payments.js +6 -1
  122. package/dist/modules/sales/commands/payments.js.map +2 -2
  123. package/dist/modules/sales/commands/returns.js +3 -3
  124. package/dist/modules/sales/commands/returns.js.map +2 -2
  125. package/dist/modules/sales/commands/shared.js +3 -3
  126. package/dist/modules/sales/commands/shared.js.map +2 -2
  127. package/dist/modules/sales/commands/shipments.js +6 -1
  128. package/dist/modules/sales/commands/shipments.js.map +2 -2
  129. package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
  130. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  131. package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
  132. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  133. package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
  134. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  135. package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
  136. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  137. package/dist/modules/sales/di.js +39 -3
  138. package/dist/modules/sales/di.js.map +2 -2
  139. package/dist/modules/sales/setup.js +2 -0
  140. package/dist/modules/sales/setup.js.map +2 -2
  141. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
  142. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  143. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
  144. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  145. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
  146. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
  148. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  149. package/dist/modules/staff/commands/job-histories.js +3 -3
  150. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  151. package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
  152. package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
  153. package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
  154. package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
  155. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
  156. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  157. package/dist/modules/translations/components/TranslationManager.js +7 -1
  158. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  159. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
  160. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  161. package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
  162. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  163. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
  164. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  165. package/package.json +7 -7
  166. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
  167. package/src/modules/auth/api/logout.ts +2 -3
  168. package/src/modules/auth/api/roles/acl/route.ts +2 -2
  169. package/src/modules/auth/api/session/refresh.ts +5 -6
  170. package/src/modules/auth/api/users/acl/route.ts +2 -2
  171. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
  172. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
  173. package/src/modules/auth/data/entities.ts +2 -2
  174. package/src/modules/auth/lib/requestRedirect.ts +44 -2
  175. package/src/modules/business_rules/api/rules/route.ts +3 -3
  176. package/src/modules/business_rules/api/sets/route.ts +3 -3
  177. package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
  178. package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
  179. package/src/modules/catalog/api/product-media/route.ts +4 -0
  180. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
  181. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
  182. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
  183. package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
  184. package/src/modules/catalog/lib/bulkDelete.ts +6 -0
  185. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
  186. package/src/modules/currencies/backend/currencies/page.tsx +2 -1
  187. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
  188. package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
  189. package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
  190. package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
  191. package/src/modules/customers/api/addresses/route.ts +2 -0
  192. package/src/modules/customers/api/tags/route.ts +6 -1
  193. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
  194. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
  195. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
  196. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
  197. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  198. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
  199. package/src/modules/customers/commands/interactions.ts +5 -5
  200. package/src/modules/customers/commands/pipeline-stages.ts +26 -2
  201. package/src/modules/customers/commands/pipelines.ts +26 -2
  202. package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
  203. package/src/modules/customers/components/detail/DealForm.tsx +12 -0
  204. package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
  205. package/src/modules/customers/i18n/de.json +2 -0
  206. package/src/modules/customers/i18n/en.json +2 -0
  207. package/src/modules/customers/i18n/es.json +2 -0
  208. package/src/modules/customers/i18n/pl.json +2 -0
  209. package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
  210. package/src/modules/data_sync/api/schedules/route.ts +1 -1
  211. package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
  212. package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
  213. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
  214. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
  215. package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
  216. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
  217. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
  218. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
  219. package/src/modules/entities/api/entities.ts +2 -2
  220. package/src/modules/entities/api/records.ts +7 -5
  221. package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
  222. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
  223. package/src/modules/feature_toggles/data/entities.ts +1 -1
  224. package/src/modules/feature_toggles/data/validators.ts +1 -0
  225. package/src/modules/feature_toggles/lib/queries.ts +1 -0
  226. package/src/modules/inbox_ops/api/settings/route.ts +2 -2
  227. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
  228. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
  229. package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
  230. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
  231. package/src/modules/sales/commands/documents.ts +11 -10
  232. package/src/modules/sales/commands/payments.ts +11 -0
  233. package/src/modules/sales/commands/returns.ts +3 -3
  234. package/src/modules/sales/commands/shared.ts +10 -4
  235. package/src/modules/sales/commands/shipments.ts +14 -0
  236. package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
  237. package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
  238. package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
  239. package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
  240. package/src/modules/sales/di.ts +68 -9
  241. package/src/modules/sales/setup.ts +2 -0
  242. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
  243. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
  244. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
  245. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
  246. package/src/modules/staff/commands/job-histories.ts +3 -3
  247. package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
  248. package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
  249. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
  250. package/src/modules/translations/components/TranslationManager.tsx +14 -1
  251. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
  252. package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
  253. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
@@ -23,6 +23,7 @@ import {
23
23
  } from "@open-mercato/ui/primitives/dialog";
24
24
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
25
25
  import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
26
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
26
27
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
27
28
  import { raiseCrudError } from "@open-mercato/ui/backend/utils/serverErrors";
28
29
  import { useT } from "@open-mercato/shared/lib/i18n/context";
@@ -205,6 +206,7 @@ function AttachmentPartitionSettings({ s3Enabled }) {
205
206
  await loadItems();
206
207
  } catch (err) {
207
208
  console.error("[attachments.partitions] save failed", err);
209
+ if (surfaceRecordConflict(err, t)) return;
208
210
  const message = err instanceof Error ? err.message : t("attachments.partitions.errors.save", "Failed to save partition.");
209
211
  setError(message);
210
212
  } finally {
@@ -239,6 +241,7 @@ function AttachmentPartitionSettings({ s3Enabled }) {
239
241
  await loadItems();
240
242
  } catch (err) {
241
243
  console.error("[attachments.partitions] delete failed", err);
244
+ if (surfaceRecordConflict(err, t)) return;
242
245
  flash(t("attachments.partitions.errors.delete", "Failed to delete partition."), "error");
243
246
  }
244
247
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/components/AttachmentPartitionSettings.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { resolvePartitionEnvKey } from '@open-mercato/core/modules/attachments/lib/partitionEnv'\n\ntype Partition = {\n id: string\n code: string\n title: string\n description: string | null\n isPublic: boolean\n requiresOcr: boolean\n ocrModel: string | null\n storageDriver: string\n configJson: Record<string, unknown> | null\n envKey: string\n createdAt: string | null\n updatedAt?: string | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: Partition }\n\ntype S3CredentialsSource = 'integration' | 'env'\n\nconst DEFAULT_FORM = {\n code: '',\n title: '',\n description: '',\n isPublic: false,\n requiresOcr: true,\n ocrModel: '',\n storageDriver: 'local',\n s3CredentialsSource: 'integration' as S3CredentialsSource,\n s3CredentialsEnvPrefix: '',\n s3Bucket: '',\n s3Region: '',\n s3Endpoint: '',\n s3ForcePathStyle: false,\n}\n\nconst ALL_STORAGE_DRIVER_OPTIONS = [\n { value: 'local', label: 'Local filesystem' },\n { value: 's3', label: 'Amazon S3 / S3-compatible', requiresModule: 's3' },\n]\n\nconst OCR_MODEL_DEFAULT_VALUE = '__default__'\n\nconst OCR_MODEL_OPTIONS = [\n { value: OCR_MODEL_DEFAULT_VALUE, label: 'Default (from environment)' },\n { value: 'gpt-4o', label: 'GPT-4o (Recommended)' },\n { value: 'gpt-4o-mini', label: 'GPT-4o Mini (Faster, Lower Cost)' },\n]\n\nexport type AttachmentPartitionSettingsProps = {\n s3Enabled: boolean\n}\n\nexport function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSettingsProps) {\n const storageDriverOptions = ALL_STORAGE_DRIVER_OPTIONS.filter(\n (opt) => !opt.requiresModule || (opt.requiresModule === 's3' && s3Enabled),\n )\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [items, setItems] = React.useState<Partition[]>([])\n const [loading, setLoading] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [form, setForm] = React.useState(DEFAULT_FORM)\n const [submitting, setSubmitting] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const s3Unavailable = !s3Enabled\n const s3SelectedWhileUnavailable = s3Unavailable && form.storageDriver === 's3'\n\n const loadErrorMessage = t('attachments.partitions.errors.load', 'Failed to load partitions.')\n\n const loadItems = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: Partition[] }>(\n '/api/attachments/partitions',\n undefined,\n { errorMessage: loadErrorMessage },\n )\n const normalized = Array.isArray(payload.items) ? payload.items : []\n const withDefaults = normalized.map((entry) => ({\n ...entry,\n requiresOcr: typeof entry.requiresOcr === 'boolean' ? entry.requiresOcr : true,\n }))\n setItems(withDefaults)\n } catch (err) {\n console.error('[attachments.partitions] list failed', err)\n flash(loadErrorMessage, 'error')\n } finally {\n setLoading(false)\n }\n }, [loadErrorMessage])\n\n React.useEffect(() => {\n loadItems().catch(() => {})\n }, [loadItems])\n\n const filteredItems = React.useMemo(() => {\n if (!search.trim()) return items\n const term = search.trim().toLowerCase()\n return items.filter(\n (entry) =>\n entry.code.toLowerCase().includes(term) ||\n entry.title.toLowerCase().includes(term) ||\n (entry.description ?? '').toLowerCase().includes(term),\n )\n }, [items, search])\n\n const openDialog = React.useCallback((state: DialogState) => {\n if (state.mode === 'edit') {\n const cfg = state.entry.configJson ?? {}\n const credsPrefix = typeof cfg.credentialsEnvPrefix === 'string' ? cfg.credentialsEnvPrefix : ''\n setForm({\n code: state.entry.code,\n title: state.entry.title,\n description: state.entry.description ?? '',\n isPublic: state.entry.isPublic,\n requiresOcr: state.entry.requiresOcr,\n ocrModel: state.entry.ocrModel ?? '',\n storageDriver: state.entry.storageDriver ?? 'local',\n s3CredentialsSource: credsPrefix ? 'env' : 'integration',\n s3CredentialsEnvPrefix: credsPrefix,\n s3Bucket: typeof cfg.bucket === 'string' ? cfg.bucket : '',\n s3Region: typeof cfg.region === 'string' ? cfg.region : '',\n s3Endpoint: typeof cfg.endpoint === 'string' ? cfg.endpoint : '',\n s3ForcePathStyle: cfg.forcePathStyle === true,\n })\n } else {\n setForm(DEFAULT_FORM)\n }\n setError(null)\n setDialog(state)\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n setError(null)\n setSubmitting(false)\n setForm(DEFAULT_FORM)\n }, [])\n\n const handleSubmit = React.useCallback(async () => {\n if (!dialog) return\n const trimmedCode = form.code.trim()\n const trimmedTitle = form.title.trim()\n if (!trimmedCode || !trimmedTitle) {\n setError(t('attachments.partitions.errors.required', 'Code and title are required.'))\n return\n }\n if (form.storageDriver === 's3' && form.s3CredentialsSource === 'env') {\n if (!form.s3CredentialsEnvPrefix.trim()) {\n setError(\n t(\n 'attachments.partitions.errors.s3EnvPrefixRequired',\n 'Credentials env prefix is required when using a separate credentials source.',\n ),\n )\n return\n }\n if (!form.s3Bucket.trim()) {\n setError(\n t(\n 'attachments.partitions.errors.s3BucketRequired',\n 'Bucket is required when using a separate credentials source.',\n ),\n )\n return\n }\n }\n setSubmitting(true)\n setError(null)\n try {\n const s3ConfigJson =\n form.storageDriver === 's3'\n ? {\n bucket: form.s3Bucket.trim() || undefined,\n region: form.s3Region.trim() || undefined,\n endpoint: form.s3Endpoint.trim() || undefined,\n forcePathStyle: form.s3ForcePathStyle || undefined,\n ...(form.s3CredentialsSource === 'env'\n ? { credentialsEnvPrefix: form.s3CredentialsEnvPrefix.trim() || undefined }\n : {}),\n }\n : null\n\n const payload = {\n code: trimmedCode,\n title: trimmedTitle,\n description: form.description.trim() || undefined,\n isPublic: form.isPublic,\n requiresOcr: form.requiresOcr,\n ocrModel: form.ocrModel.trim() || null,\n storageDriver: form.storageDriver,\n configJson: s3ConfigJson,\n }\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n const body =\n dialog.mode === 'edit'\n ? JSON.stringify({ id: dialog.entry.id, ...payload })\n : JSON.stringify(payload)\n const lockHeader =\n dialog.mode === 'edit' ? buildOptimisticLockHeader(dialog.entry.updatedAt) : {}\n const call = await withScopedApiRequestHeaders(lockHeader, () =>\n apiCall('/api/attachments/partitions', {\n method,\n headers: { 'content-type': 'application/json' },\n body,\n }),\n )\n if (!call.ok) {\n await raiseCrudError(\n call.response,\n t('attachments.partitions.errors.save', 'Failed to save partition.'),\n )\n }\n flash(\n dialog.mode === 'create'\n ? t('attachments.partitions.messages.created', 'Partition created.')\n : t('attachments.partitions.messages.updated', 'Partition updated.'),\n 'success',\n )\n closeDialog()\n await loadItems()\n } catch (err) {\n console.error('[attachments.partitions] save failed', err)\n const message =\n err instanceof Error ? err.message : t('attachments.partitions.errors.save', 'Failed to save partition.')\n setError(message)\n } finally {\n setSubmitting(false)\n }\n }, [dialog, form, t, closeDialog, loadItems])\n\n const handleDelete = React.useCallback(\n async (entry: Partition) => {\n const confirmMessage = t('attachments.partitions.confirm.delete', 'Delete partition \"{{code}}\"?').replace(\n '{{code}}',\n entry.code,\n )\n const confirmed = await confirm({\n title: confirmMessage,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(`/api/attachments/partitions?id=${encodeURIComponent(entry.id)}`, {\n method: 'DELETE',\n }),\n )\n if (!call.ok) {\n await raiseCrudError(\n call.response,\n t('attachments.partitions.errors.delete', 'Failed to delete partition.'),\n )\n }\n flash(t('attachments.partitions.messages.deleted', 'Partition removed.'), 'success')\n await loadItems()\n } catch (err) {\n console.error('[attachments.partitions] delete failed', err)\n flash(t('attachments.partitions.errors.delete', 'Failed to delete partition.'), 'error')\n }\n },\n [confirm, loadItems, t],\n )\n\n const columns = React.useMemo<ColumnDef<Partition>[]>(\n () => [\n {\n header: t('attachments.partitions.table.code', 'Code'),\n accessorKey: 'code',\n cell: ({ row }) => <code className=\"font-mono text-xs\">{row.original.code}</code>,\n },\n {\n header: t('attachments.partitions.table.title', 'Title'),\n accessorKey: 'title',\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.title}</span>\n {row.original.description ? (\n <span className=\"text-xs text-muted-foreground line-clamp-2\">{row.original.description}</span>\n ) : null}\n </div>\n ),\n },\n {\n header: t('attachments.partitions.table.visibility', 'Visibility'),\n accessorKey: 'isPublic',\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.isPublic\n ? t('attachments.partitions.table.public', 'Public')\n : t('attachments.partitions.table.private', 'Private')}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.ocr', 'OCR'),\n accessorKey: 'requiresOcr',\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.requiresOcr\n ? t('common.enabled', 'Enabled')\n : t('common.disabled', 'Disabled')}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.storageDriver', 'Storage'),\n accessorKey: 'storageDriver',\n cell: ({ row }) => (\n <span className=\"text-sm capitalize\">\n {row.original.storageDriver === 's3' ? 'S3' : row.original.storageDriver ?? 'local'}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.envKey', 'Env variable'),\n accessorKey: 'envKey',\n cell: ({ row }) => <code className=\"text-xs\">{row.original.envKey}</code>,\n },\n ],\n [t],\n )\n\n const tableLabels = React.useMemo(\n () => ({\n search: t('attachments.partitions.table.search', 'Search partitions\u2026'),\n empty: t('attachments.partitions.table.empty', 'No partitions configured.'),\n }),\n [t],\n )\n\n const formKeyHandler = React.useCallback(\n (event: React.KeyboardEvent<HTMLFormElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <div className=\"space-y-6 rounded-lg border bg-card p-6\">\n <div className=\"flex flex-col gap-2 md:flex-row md:items-center md:justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('attachments.partitions.title', 'Attachment partitions')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('attachments.partitions.description', 'Define storage partitions and visibility for uploads.')}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => openDialog({ mode: 'create' })}>\n {t('attachments.partitions.actions.add', 'Add partition')}\n </Button>\n </div>\n <DataTable<Partition>\n columns={columns}\n data={filteredItems}\n searchValue={search}\n onSearchChange={(value: string) => setSearch(value)}\n searchPlaceholder={tableLabels.search}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{tableLabels.empty}</p>}\n isLoading={loading}\n refreshButton={{\n label: t('attachments.partitions.actions.refresh', 'Refresh'),\n onRefresh: () => { void loadItems() },\n isRefreshing: loading,\n }}\n rowActions={(entry) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('attachments.partitions.actions.edit', 'Edit'),\n onSelect: () => openDialog({ mode: 'edit', entry }),\n },\n {\n id: 'delete',\n label: t('attachments.partitions.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(entry) },\n },\n ]}\n />\n )}\n />\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.dialog.editTitle', 'Edit partition')\n : t('attachments.partitions.dialog.createTitle', 'Create partition')}\n </DialogTitle>\n <DialogDescription>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.dialog.editDescription', 'Update partition metadata and visibility.')\n : t('attachments.partitions.dialog.createDescription', 'Define a storage partition for attachments.')}\n </DialogDescription>\n </DialogHeader>\n <form\n className=\"space-y-4\"\n onKeyDown={formKeyHandler}\n onSubmit={(event) => {\n event.preventDefault()\n void handleSubmit()\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-code\">{t('attachments.partitions.form.codeLabel', 'Code')}</Label>\n <Input\n id=\"partition-code\"\n value={form.code}\n onChange={(event) => setForm((prev) => ({ ...prev, code: event.target.value }))}\n placeholder={t('attachments.partitions.form.codePlaceholder', 'e.g. marketingAssets')}\n disabled={dialog?.mode === 'edit'}\n className=\"font-mono uppercase\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-title\">{t('attachments.partitions.form.titleLabel', 'Title')}</Label>\n <Input\n id=\"partition-title\"\n value={form.title}\n onChange={(event) => setForm((prev) => ({ ...prev, title: event.target.value }))}\n placeholder={t('attachments.partitions.form.titlePlaceholder', 'e.g. Marketing assets')}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-description\">{t('attachments.partitions.form.descriptionLabel', 'Description')}</Label>\n <textarea\n id=\"partition-description\"\n className=\"min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.description}\n onChange={(event) => setForm((prev) => ({ ...prev, description: event.target.value }))}\n placeholder={t('attachments.partitions.form.descriptionPlaceholder', 'Explain how this partition is used.')}\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.isPublic}\n onChange={(event) => setForm((prev) => ({ ...prev, isPublic: event.target.checked }))}\n />\n {t('attachments.partitions.form.publicLabel', 'Publicly accessible')}\n </label>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.requiresOcr}\n onChange={(event) => setForm((prev) => ({ ...prev, requiresOcr: event.target.checked }))}\n />\n {t('attachments.partitions.form.ocrLabel', 'Require OCR/text extraction')}\n </label>\n {form.requiresOcr && (\n <div className=\"space-y-2 pl-6\">\n <Label htmlFor=\"partition-ocr-model\">\n {t('attachments.partitions.form.ocrModelLabel', 'OCR Model')}\n </Label>\n <Select\n value={form.ocrModel ? form.ocrModel : OCR_MODEL_DEFAULT_VALUE}\n onValueChange={(value) =>\n setForm((prev) => ({\n ...prev,\n ocrModel: value === OCR_MODEL_DEFAULT_VALUE ? '' : value,\n }))\n }\n >\n <SelectTrigger id=\"partition-ocr-model\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {OCR_MODEL_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {t(\n `attachments.partitions.form.ocrModelOptions.${\n option.value === OCR_MODEL_DEFAULT_VALUE ? 'default' : option.value\n }`,\n option.label,\n )}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.ocrModelHelp',\n 'Choose the LLM model for OCR processing. Falls back to OCR_MODEL environment variable or gpt-4o.'\n )}\n </p>\n </div>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-storage-driver\">\n {t('attachments.partitions.form.storageDriverLabel', 'Storage Driver')}\n </Label>\n <select\n id=\"partition-storage-driver\"\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.storageDriver}\n onChange={(event) => setForm((prev) => ({ ...prev, storageDriver: event.target.value }))}\n >\n {storageDriverOptions.map((option) => (\n <option\n key={option.value}\n value={option.value}\n disabled={option.value === 's3' && s3Unavailable}\n >\n {option.label}\n </option>\n ))}\n </select>\n {s3Unavailable ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.storageDriverS3DisabledHelp',\n 'S3 is disabled. Set OM_ENABLE_STORAGE_S3=true and restart to enable S3 partitions.',\n )}\n </p>\n ) : null}\n </div>\n {form.storageDriver === 's3' && (\n <div className=\"space-y-3 rounded-md border bg-muted/30 p-3\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {t('attachments.partitions.form.s3ConfigTitle', 'S3 Configuration')}\n </p>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-creds-source\">\n {t('attachments.partitions.form.s3CredsSourceLabel', 'Credentials source')}\n </Label>\n <select\n id=\"partition-s3-creds-source\"\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.s3CredentialsSource}\n onChange={(event) =>\n setForm((prev) => ({\n ...prev,\n s3CredentialsSource: event.target.value as S3CredentialsSource,\n ...(event.target.value === 'integration' ? { s3CredentialsEnvPrefix: '' } : {}),\n }))\n }\n >\n <option value=\"integration\">\n {t(\n 'attachments.partitions.form.s3CredsSourceIntegration',\n 'Use Integration Marketplace credentials',\n )}\n </option>\n <option value=\"env\">\n {t(\n 'attachments.partitions.form.s3CredsSourceEnv',\n 'Use separate env-based credentials',\n )}\n </option>\n </select>\n <p className=\"text-xs text-muted-foreground\">\n {form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3CredsSourceIntegrationHelp',\n 'This partition reuses the credentials, bucket, region and endpoint configured in the S3 integration. Override individual fields below only when needed.',\n )\n : t(\n 'attachments.partitions.form.s3CredsSourceEnvHelp',\n 'This partition uses its own credentials read from environment variables. The integration credentials are ignored. Bucket and credentials prefix are required.',\n )}\n </p>\n </div>\n {form.s3CredentialsSource === 'env' && (\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-creds-prefix\">\n {t('attachments.partitions.form.s3CredsPrefixLabel', 'Credentials Env Prefix')}\n </Label>\n <Input\n id=\"partition-s3-creds-prefix\"\n value={form.s3CredentialsEnvPrefix}\n onChange={(event) => setForm((prev) => ({ ...prev, s3CredentialsEnvPrefix: event.target.value }))}\n placeholder=\"OM_INTEGRATION_STORAGE_S3\"\n className=\"font-mono\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.s3CredsPrefixHelp',\n 'Reads {PREFIX}_ACCESS_KEY_ID and {PREFIX}_SECRET_ACCESS_KEY from environment variables.',\n )}\n </p>\n </div>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-bucket\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3BucketOverrideLabel', 'Bucket override (optional)')\n : t('attachments.partitions.form.s3BucketLabel', 'Bucket')}\n </Label>\n <Input\n id=\"partition-s3-bucket\"\n value={form.s3Bucket}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Bucket: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3BucketOverridePlaceholder',\n 'Leave empty to use integration bucket',\n )\n : 'my-company-attachments'\n }\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-region\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3RegionOverrideLabel', 'Region override (optional)')\n : t('attachments.partitions.form.s3RegionLabel', 'Region')}\n </Label>\n <Input\n id=\"partition-s3-region\"\n value={form.s3Region}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Region: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3RegionOverridePlaceholder',\n 'Leave empty to use integration region',\n )\n : 'us-east-1'\n }\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-endpoint\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3EndpointOverrideLabel', 'Custom endpoint override (optional)')\n : t('attachments.partitions.form.s3EndpointLabel', 'Custom Endpoint')}\n </Label>\n <Input\n id=\"partition-s3-endpoint\"\n value={form.s3Endpoint}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Endpoint: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3EndpointOverridePlaceholder',\n 'Leave empty to use integration endpoint',\n )\n : 'https://fra1.digitaloceanspaces.com'\n }\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('attachments.partitions.form.s3EndpointHelp', 'Leave empty for AWS S3. Required for MinIO, DigitalOcean Spaces, etc.')}\n </p>\n </div>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.s3ForcePathStyle}\n onChange={(event) => setForm((prev) => ({ ...prev, s3ForcePathStyle: event.target.checked }))}\n />\n {t('attachments.partitions.form.s3ForcePathStyleLabel', 'Force Path Style (required for MinIO)')}\n </label>\n </div>\n )}\n {dialog ? (\n <div className=\"rounded-md border bg-muted/50 px-3 py-2 text-xs text-muted-foreground\">\n <div>\n {t('attachments.partitions.form.envKeyHelp', 'Set this env var to override storage path:')}\n </div>\n <code>\n {dialog.mode === 'edit'\n ? dialog.entry.envKey\n : form.code.trim()\n ? resolvePartitionEnvKey(form.code.trim())\n : 'ATTACHMENTS_PARTITION_CODE_ROOT'}\n </code>\n </div>\n ) : null}\n {s3SelectedWhileUnavailable ? (\n <p className=\"text-sm text-status-error-text\">\n {t(\n 'attachments.partitions.errors.s3Disabled',\n 'This partition uses S3, but S3 is disabled. Set OM_ENABLE_STORAGE_S3=true and restart the app.',\n )}\n </p>\n ) : null}\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n </form>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={closeDialog}>\n {t('attachments.partitions.actions.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void handleSubmit()} disabled={submitting || s3SelectedWhileUnavailable}>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.actions.save', 'Save changes')\n : t('attachments.partitions.actions.create', 'Create partition')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { resolvePartitionEnvKey } from '@open-mercato/core/modules/attachments/lib/partitionEnv'\n\ntype Partition = {\n id: string\n code: string\n title: string\n description: string | null\n isPublic: boolean\n requiresOcr: boolean\n ocrModel: string | null\n storageDriver: string\n configJson: Record<string, unknown> | null\n envKey: string\n createdAt: string | null\n updatedAt?: string | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: Partition }\n\ntype S3CredentialsSource = 'integration' | 'env'\n\nconst DEFAULT_FORM = {\n code: '',\n title: '',\n description: '',\n isPublic: false,\n requiresOcr: true,\n ocrModel: '',\n storageDriver: 'local',\n s3CredentialsSource: 'integration' as S3CredentialsSource,\n s3CredentialsEnvPrefix: '',\n s3Bucket: '',\n s3Region: '',\n s3Endpoint: '',\n s3ForcePathStyle: false,\n}\n\nconst ALL_STORAGE_DRIVER_OPTIONS = [\n { value: 'local', label: 'Local filesystem' },\n { value: 's3', label: 'Amazon S3 / S3-compatible', requiresModule: 's3' },\n]\n\nconst OCR_MODEL_DEFAULT_VALUE = '__default__'\n\nconst OCR_MODEL_OPTIONS = [\n { value: OCR_MODEL_DEFAULT_VALUE, label: 'Default (from environment)' },\n { value: 'gpt-4o', label: 'GPT-4o (Recommended)' },\n { value: 'gpt-4o-mini', label: 'GPT-4o Mini (Faster, Lower Cost)' },\n]\n\nexport type AttachmentPartitionSettingsProps = {\n s3Enabled: boolean\n}\n\nexport function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSettingsProps) {\n const storageDriverOptions = ALL_STORAGE_DRIVER_OPTIONS.filter(\n (opt) => !opt.requiresModule || (opt.requiresModule === 's3' && s3Enabled),\n )\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [items, setItems] = React.useState<Partition[]>([])\n const [loading, setLoading] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [form, setForm] = React.useState(DEFAULT_FORM)\n const [submitting, setSubmitting] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const s3Unavailable = !s3Enabled\n const s3SelectedWhileUnavailable = s3Unavailable && form.storageDriver === 's3'\n\n const loadErrorMessage = t('attachments.partitions.errors.load', 'Failed to load partitions.')\n\n const loadItems = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: Partition[] }>(\n '/api/attachments/partitions',\n undefined,\n { errorMessage: loadErrorMessage },\n )\n const normalized = Array.isArray(payload.items) ? payload.items : []\n const withDefaults = normalized.map((entry) => ({\n ...entry,\n requiresOcr: typeof entry.requiresOcr === 'boolean' ? entry.requiresOcr : true,\n }))\n setItems(withDefaults)\n } catch (err) {\n console.error('[attachments.partitions] list failed', err)\n flash(loadErrorMessage, 'error')\n } finally {\n setLoading(false)\n }\n }, [loadErrorMessage])\n\n React.useEffect(() => {\n loadItems().catch(() => {})\n }, [loadItems])\n\n const filteredItems = React.useMemo(() => {\n if (!search.trim()) return items\n const term = search.trim().toLowerCase()\n return items.filter(\n (entry) =>\n entry.code.toLowerCase().includes(term) ||\n entry.title.toLowerCase().includes(term) ||\n (entry.description ?? '').toLowerCase().includes(term),\n )\n }, [items, search])\n\n const openDialog = React.useCallback((state: DialogState) => {\n if (state.mode === 'edit') {\n const cfg = state.entry.configJson ?? {}\n const credsPrefix = typeof cfg.credentialsEnvPrefix === 'string' ? cfg.credentialsEnvPrefix : ''\n setForm({\n code: state.entry.code,\n title: state.entry.title,\n description: state.entry.description ?? '',\n isPublic: state.entry.isPublic,\n requiresOcr: state.entry.requiresOcr,\n ocrModel: state.entry.ocrModel ?? '',\n storageDriver: state.entry.storageDriver ?? 'local',\n s3CredentialsSource: credsPrefix ? 'env' : 'integration',\n s3CredentialsEnvPrefix: credsPrefix,\n s3Bucket: typeof cfg.bucket === 'string' ? cfg.bucket : '',\n s3Region: typeof cfg.region === 'string' ? cfg.region : '',\n s3Endpoint: typeof cfg.endpoint === 'string' ? cfg.endpoint : '',\n s3ForcePathStyle: cfg.forcePathStyle === true,\n })\n } else {\n setForm(DEFAULT_FORM)\n }\n setError(null)\n setDialog(state)\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n setError(null)\n setSubmitting(false)\n setForm(DEFAULT_FORM)\n }, [])\n\n const handleSubmit = React.useCallback(async () => {\n if (!dialog) return\n const trimmedCode = form.code.trim()\n const trimmedTitle = form.title.trim()\n if (!trimmedCode || !trimmedTitle) {\n setError(t('attachments.partitions.errors.required', 'Code and title are required.'))\n return\n }\n if (form.storageDriver === 's3' && form.s3CredentialsSource === 'env') {\n if (!form.s3CredentialsEnvPrefix.trim()) {\n setError(\n t(\n 'attachments.partitions.errors.s3EnvPrefixRequired',\n 'Credentials env prefix is required when using a separate credentials source.',\n ),\n )\n return\n }\n if (!form.s3Bucket.trim()) {\n setError(\n t(\n 'attachments.partitions.errors.s3BucketRequired',\n 'Bucket is required when using a separate credentials source.',\n ),\n )\n return\n }\n }\n setSubmitting(true)\n setError(null)\n try {\n const s3ConfigJson =\n form.storageDriver === 's3'\n ? {\n bucket: form.s3Bucket.trim() || undefined,\n region: form.s3Region.trim() || undefined,\n endpoint: form.s3Endpoint.trim() || undefined,\n forcePathStyle: form.s3ForcePathStyle || undefined,\n ...(form.s3CredentialsSource === 'env'\n ? { credentialsEnvPrefix: form.s3CredentialsEnvPrefix.trim() || undefined }\n : {}),\n }\n : null\n\n const payload = {\n code: trimmedCode,\n title: trimmedTitle,\n description: form.description.trim() || undefined,\n isPublic: form.isPublic,\n requiresOcr: form.requiresOcr,\n ocrModel: form.ocrModel.trim() || null,\n storageDriver: form.storageDriver,\n configJson: s3ConfigJson,\n }\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n const body =\n dialog.mode === 'edit'\n ? JSON.stringify({ id: dialog.entry.id, ...payload })\n : JSON.stringify(payload)\n const lockHeader =\n dialog.mode === 'edit' ? buildOptimisticLockHeader(dialog.entry.updatedAt) : {}\n const call = await withScopedApiRequestHeaders(lockHeader, () =>\n apiCall('/api/attachments/partitions', {\n method,\n headers: { 'content-type': 'application/json' },\n body,\n }),\n )\n if (!call.ok) {\n await raiseCrudError(\n call.response,\n t('attachments.partitions.errors.save', 'Failed to save partition.'),\n )\n }\n flash(\n dialog.mode === 'create'\n ? t('attachments.partitions.messages.created', 'Partition created.')\n : t('attachments.partitions.messages.updated', 'Partition updated.'),\n 'success',\n )\n closeDialog()\n await loadItems()\n } catch (err) {\n console.error('[attachments.partitions] save failed', err)\n if (surfaceRecordConflict(err, t)) return\n const message =\n err instanceof Error ? err.message : t('attachments.partitions.errors.save', 'Failed to save partition.')\n setError(message)\n } finally {\n setSubmitting(false)\n }\n }, [dialog, form, t, closeDialog, loadItems])\n\n const handleDelete = React.useCallback(\n async (entry: Partition) => {\n const confirmMessage = t('attachments.partitions.confirm.delete', 'Delete partition \"{{code}}\"?').replace(\n '{{code}}',\n entry.code,\n )\n const confirmed = await confirm({\n title: confirmMessage,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(`/api/attachments/partitions?id=${encodeURIComponent(entry.id)}`, {\n method: 'DELETE',\n }),\n )\n if (!call.ok) {\n await raiseCrudError(\n call.response,\n t('attachments.partitions.errors.delete', 'Failed to delete partition.'),\n )\n }\n flash(t('attachments.partitions.messages.deleted', 'Partition removed.'), 'success')\n await loadItems()\n } catch (err) {\n console.error('[attachments.partitions] delete failed', err)\n if (surfaceRecordConflict(err, t)) return\n flash(t('attachments.partitions.errors.delete', 'Failed to delete partition.'), 'error')\n }\n },\n [confirm, loadItems, t],\n )\n\n const columns = React.useMemo<ColumnDef<Partition>[]>(\n () => [\n {\n header: t('attachments.partitions.table.code', 'Code'),\n accessorKey: 'code',\n cell: ({ row }) => <code className=\"font-mono text-xs\">{row.original.code}</code>,\n },\n {\n header: t('attachments.partitions.table.title', 'Title'),\n accessorKey: 'title',\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.title}</span>\n {row.original.description ? (\n <span className=\"text-xs text-muted-foreground line-clamp-2\">{row.original.description}</span>\n ) : null}\n </div>\n ),\n },\n {\n header: t('attachments.partitions.table.visibility', 'Visibility'),\n accessorKey: 'isPublic',\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.isPublic\n ? t('attachments.partitions.table.public', 'Public')\n : t('attachments.partitions.table.private', 'Private')}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.ocr', 'OCR'),\n accessorKey: 'requiresOcr',\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.requiresOcr\n ? t('common.enabled', 'Enabled')\n : t('common.disabled', 'Disabled')}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.storageDriver', 'Storage'),\n accessorKey: 'storageDriver',\n cell: ({ row }) => (\n <span className=\"text-sm capitalize\">\n {row.original.storageDriver === 's3' ? 'S3' : row.original.storageDriver ?? 'local'}\n </span>\n ),\n },\n {\n header: t('attachments.partitions.table.envKey', 'Env variable'),\n accessorKey: 'envKey',\n cell: ({ row }) => <code className=\"text-xs\">{row.original.envKey}</code>,\n },\n ],\n [t],\n )\n\n const tableLabels = React.useMemo(\n () => ({\n search: t('attachments.partitions.table.search', 'Search partitions\u2026'),\n empty: t('attachments.partitions.table.empty', 'No partitions configured.'),\n }),\n [t],\n )\n\n const formKeyHandler = React.useCallback(\n (event: React.KeyboardEvent<HTMLFormElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <div className=\"space-y-6 rounded-lg border bg-card p-6\">\n <div className=\"flex flex-col gap-2 md:flex-row md:items-center md:justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('attachments.partitions.title', 'Attachment partitions')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('attachments.partitions.description', 'Define storage partitions and visibility for uploads.')}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => openDialog({ mode: 'create' })}>\n {t('attachments.partitions.actions.add', 'Add partition')}\n </Button>\n </div>\n <DataTable<Partition>\n columns={columns}\n data={filteredItems}\n searchValue={search}\n onSearchChange={(value: string) => setSearch(value)}\n searchPlaceholder={tableLabels.search}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{tableLabels.empty}</p>}\n isLoading={loading}\n refreshButton={{\n label: t('attachments.partitions.actions.refresh', 'Refresh'),\n onRefresh: () => { void loadItems() },\n isRefreshing: loading,\n }}\n rowActions={(entry) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('attachments.partitions.actions.edit', 'Edit'),\n onSelect: () => openDialog({ mode: 'edit', entry }),\n },\n {\n id: 'delete',\n label: t('attachments.partitions.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(entry) },\n },\n ]}\n />\n )}\n />\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.dialog.editTitle', 'Edit partition')\n : t('attachments.partitions.dialog.createTitle', 'Create partition')}\n </DialogTitle>\n <DialogDescription>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.dialog.editDescription', 'Update partition metadata and visibility.')\n : t('attachments.partitions.dialog.createDescription', 'Define a storage partition for attachments.')}\n </DialogDescription>\n </DialogHeader>\n <form\n className=\"space-y-4\"\n onKeyDown={formKeyHandler}\n onSubmit={(event) => {\n event.preventDefault()\n void handleSubmit()\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-code\">{t('attachments.partitions.form.codeLabel', 'Code')}</Label>\n <Input\n id=\"partition-code\"\n value={form.code}\n onChange={(event) => setForm((prev) => ({ ...prev, code: event.target.value }))}\n placeholder={t('attachments.partitions.form.codePlaceholder', 'e.g. marketingAssets')}\n disabled={dialog?.mode === 'edit'}\n className=\"font-mono uppercase\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-title\">{t('attachments.partitions.form.titleLabel', 'Title')}</Label>\n <Input\n id=\"partition-title\"\n value={form.title}\n onChange={(event) => setForm((prev) => ({ ...prev, title: event.target.value }))}\n placeholder={t('attachments.partitions.form.titlePlaceholder', 'e.g. Marketing assets')}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-description\">{t('attachments.partitions.form.descriptionLabel', 'Description')}</Label>\n <textarea\n id=\"partition-description\"\n className=\"min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.description}\n onChange={(event) => setForm((prev) => ({ ...prev, description: event.target.value }))}\n placeholder={t('attachments.partitions.form.descriptionPlaceholder', 'Explain how this partition is used.')}\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.isPublic}\n onChange={(event) => setForm((prev) => ({ ...prev, isPublic: event.target.checked }))}\n />\n {t('attachments.partitions.form.publicLabel', 'Publicly accessible')}\n </label>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.requiresOcr}\n onChange={(event) => setForm((prev) => ({ ...prev, requiresOcr: event.target.checked }))}\n />\n {t('attachments.partitions.form.ocrLabel', 'Require OCR/text extraction')}\n </label>\n {form.requiresOcr && (\n <div className=\"space-y-2 pl-6\">\n <Label htmlFor=\"partition-ocr-model\">\n {t('attachments.partitions.form.ocrModelLabel', 'OCR Model')}\n </Label>\n <Select\n value={form.ocrModel ? form.ocrModel : OCR_MODEL_DEFAULT_VALUE}\n onValueChange={(value) =>\n setForm((prev) => ({\n ...prev,\n ocrModel: value === OCR_MODEL_DEFAULT_VALUE ? '' : value,\n }))\n }\n >\n <SelectTrigger id=\"partition-ocr-model\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {OCR_MODEL_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {t(\n `attachments.partitions.form.ocrModelOptions.${\n option.value === OCR_MODEL_DEFAULT_VALUE ? 'default' : option.value\n }`,\n option.label,\n )}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.ocrModelHelp',\n 'Choose the LLM model for OCR processing. Falls back to OCR_MODEL environment variable or gpt-4o.'\n )}\n </p>\n </div>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-storage-driver\">\n {t('attachments.partitions.form.storageDriverLabel', 'Storage Driver')}\n </Label>\n <select\n id=\"partition-storage-driver\"\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.storageDriver}\n onChange={(event) => setForm((prev) => ({ ...prev, storageDriver: event.target.value }))}\n >\n {storageDriverOptions.map((option) => (\n <option\n key={option.value}\n value={option.value}\n disabled={option.value === 's3' && s3Unavailable}\n >\n {option.label}\n </option>\n ))}\n </select>\n {s3Unavailable ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.storageDriverS3DisabledHelp',\n 'S3 is disabled. Set OM_ENABLE_STORAGE_S3=true and restart to enable S3 partitions.',\n )}\n </p>\n ) : null}\n </div>\n {form.storageDriver === 's3' && (\n <div className=\"space-y-3 rounded-md border bg-muted/30 p-3\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {t('attachments.partitions.form.s3ConfigTitle', 'S3 Configuration')}\n </p>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-creds-source\">\n {t('attachments.partitions.form.s3CredsSourceLabel', 'Credentials source')}\n </Label>\n <select\n id=\"partition-s3-creds-source\"\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.s3CredentialsSource}\n onChange={(event) =>\n setForm((prev) => ({\n ...prev,\n s3CredentialsSource: event.target.value as S3CredentialsSource,\n ...(event.target.value === 'integration' ? { s3CredentialsEnvPrefix: '' } : {}),\n }))\n }\n >\n <option value=\"integration\">\n {t(\n 'attachments.partitions.form.s3CredsSourceIntegration',\n 'Use Integration Marketplace credentials',\n )}\n </option>\n <option value=\"env\">\n {t(\n 'attachments.partitions.form.s3CredsSourceEnv',\n 'Use separate env-based credentials',\n )}\n </option>\n </select>\n <p className=\"text-xs text-muted-foreground\">\n {form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3CredsSourceIntegrationHelp',\n 'This partition reuses the credentials, bucket, region and endpoint configured in the S3 integration. Override individual fields below only when needed.',\n )\n : t(\n 'attachments.partitions.form.s3CredsSourceEnvHelp',\n 'This partition uses its own credentials read from environment variables. The integration credentials are ignored. Bucket and credentials prefix are required.',\n )}\n </p>\n </div>\n {form.s3CredentialsSource === 'env' && (\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-creds-prefix\">\n {t('attachments.partitions.form.s3CredsPrefixLabel', 'Credentials Env Prefix')}\n </Label>\n <Input\n id=\"partition-s3-creds-prefix\"\n value={form.s3CredentialsEnvPrefix}\n onChange={(event) => setForm((prev) => ({ ...prev, s3CredentialsEnvPrefix: event.target.value }))}\n placeholder=\"OM_INTEGRATION_STORAGE_S3\"\n className=\"font-mono\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'attachments.partitions.form.s3CredsPrefixHelp',\n 'Reads {PREFIX}_ACCESS_KEY_ID and {PREFIX}_SECRET_ACCESS_KEY from environment variables.',\n )}\n </p>\n </div>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-bucket\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3BucketOverrideLabel', 'Bucket override (optional)')\n : t('attachments.partitions.form.s3BucketLabel', 'Bucket')}\n </Label>\n <Input\n id=\"partition-s3-bucket\"\n value={form.s3Bucket}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Bucket: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3BucketOverridePlaceholder',\n 'Leave empty to use integration bucket',\n )\n : 'my-company-attachments'\n }\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-region\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3RegionOverrideLabel', 'Region override (optional)')\n : t('attachments.partitions.form.s3RegionLabel', 'Region')}\n </Label>\n <Input\n id=\"partition-s3-region\"\n value={form.s3Region}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Region: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3RegionOverridePlaceholder',\n 'Leave empty to use integration region',\n )\n : 'us-east-1'\n }\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"partition-s3-endpoint\">\n {form.s3CredentialsSource === 'integration'\n ? t('attachments.partitions.form.s3EndpointOverrideLabel', 'Custom endpoint override (optional)')\n : t('attachments.partitions.form.s3EndpointLabel', 'Custom Endpoint')}\n </Label>\n <Input\n id=\"partition-s3-endpoint\"\n value={form.s3Endpoint}\n onChange={(event) => setForm((prev) => ({ ...prev, s3Endpoint: event.target.value }))}\n placeholder={\n form.s3CredentialsSource === 'integration'\n ? t(\n 'attachments.partitions.form.s3EndpointOverridePlaceholder',\n 'Leave empty to use integration endpoint',\n )\n : 'https://fra1.digitaloceanspaces.com'\n }\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('attachments.partitions.form.s3EndpointHelp', 'Leave empty for AWS S3. Required for MinIO, DigitalOcean Spaces, etc.')}\n </p>\n </div>\n <label className=\"flex items-center gap-2 text-sm font-medium\">\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border\"\n checked={form.s3ForcePathStyle}\n onChange={(event) => setForm((prev) => ({ ...prev, s3ForcePathStyle: event.target.checked }))}\n />\n {t('attachments.partitions.form.s3ForcePathStyleLabel', 'Force Path Style (required for MinIO)')}\n </label>\n </div>\n )}\n {dialog ? (\n <div className=\"rounded-md border bg-muted/50 px-3 py-2 text-xs text-muted-foreground\">\n <div>\n {t('attachments.partitions.form.envKeyHelp', 'Set this env var to override storage path:')}\n </div>\n <code>\n {dialog.mode === 'edit'\n ? dialog.entry.envKey\n : form.code.trim()\n ? resolvePartitionEnvKey(form.code.trim())\n : 'ATTACHMENTS_PARTITION_CODE_ROOT'}\n </code>\n </div>\n ) : null}\n {s3SelectedWhileUnavailable ? (\n <p className=\"text-sm text-status-error-text\">\n {t(\n 'attachments.partitions.errors.s3Disabled',\n 'This partition uses S3, but S3 is disabled. Set OM_ENABLE_STORAGE_S3=true and restart the app.',\n )}\n </p>\n ) : null}\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n </form>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={closeDialog}>\n {t('attachments.partitions.actions.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void handleSubmit()} disabled={submitting || s3SelectedWhileUnavailable}>\n {dialog?.mode === 'edit'\n ? t('attachments.partitions.actions.save', 'Save changes')\n : t('attachments.partitions.actions.create', 'Create partition')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,6 @@
1
- import { NextResponse } from "next/server";
2
1
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
2
  import { verifyJwt } from "@open-mercato/shared/lib/auth/jwt";
4
- import { buildRequestOriginUrl } from "@open-mercato/core/modules/auth/lib/requestRedirect";
3
+ import { buildSafeRedirectResponse } from "@open-mercato/core/modules/auth/lib/requestRedirect";
5
4
  function parseCookie(req, name) {
6
5
  const cookie = req.headers.get("cookie") || "";
7
6
  const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
@@ -35,7 +34,7 @@ async function POST(req) {
35
34
  } catch {
36
35
  }
37
36
  }
38
- const res = NextResponse.redirect(buildRequestOriginUrl(req, "/login"));
37
+ const res = buildSafeRedirectResponse(req, "/login");
39
38
  res.cookies.set("auth_token", "", { path: "/", maxAge: 0 });
40
39
  res.cookies.set("session_token", "", { path: "/", maxAge: 0 });
41
40
  return res;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/api/logout.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildRequestOriginUrl } from '@open-mercato/core/modules/auth/lib/requestRedirect'\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction extractSessionIdFromAuthToken(token: string | null): string | null {\n if (!token) return null\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload) return null\n const sid = payload.sid\n return typeof sid === 'string' && sid.length > 0 ? sid : null\n } catch {\n return null\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n const sessionId = extractSessionIdFromAuthToken(authToken)\n if (sessToken || sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (sessionId) {\n await auth.deleteSessionById(sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n } catch {}\n }\n const res = NextResponse.redirect(buildRequestOriginUrl(req, '/login'))\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Clears authentication cookies and redirects the browser to the login page.',\n responses: [\n { status: 302, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,SAAS,6BAA6B;AAEtC,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,IAAI;AACxC;AAEA,SAAS,8BAA8B,OAAqC;AAC1E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,MAAM,QAAQ;AACpB,WAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,YAAY,YAAY,KAAK,eAAe;AAClD,QAAM,YAAY,YAAY,KAAK,YAAY;AAC/C,QAAM,YAAY,8BAA8B,SAAS;AACzD,MAAI,aAAa,WAAW;AAC1B,QAAI;AACF,YAAM,IAAI,MAAM,uBAAuB;AACvC,YAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,UAAI,WAAW;AACb,cAAM,KAAK,kBAAkB,SAAS;AAAA,MACxC;AACA,UAAI,WAAW;AACb,cAAM,KAAK,qBAAqB,SAAS;AAAA,MAC3C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,QAAM,MAAM,aAAa,SAAS,sBAAsB,KAAK,QAAQ,CAAC;AACtE,MAAI,QAAQ,IAAI,cAAc,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC1D,MAAI,QAAQ,IAAI,iBAAiB,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC7D,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,6CAA6C,WAAW,YAAY;AAAA,MAClG;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction extractSessionIdFromAuthToken(token: string | null): string | null {\n if (!token) return null\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload) return null\n const sid = payload.sid\n return typeof sid === 'string' && sid.length > 0 ? sid : null\n } catch {\n return null\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n const sessionId = extractSessionIdFromAuthToken(authToken)\n if (sessToken || sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (sessionId) {\n await auth.deleteSessionById(sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n } catch {}\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Clears authentication cookies and redirects the browser to the login page.',\n responses: [\n { status: 302, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,SAAS,iCAAiC;AAE1C,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,IAAI;AACxC;AAEA,SAAS,8BAA8B,OAAqC;AAC1E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,MAAM,QAAQ;AACpB,WAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,YAAY,YAAY,KAAK,eAAe;AAClD,QAAM,YAAY,YAAY,KAAK,YAAY;AAC/C,QAAM,YAAY,8BAA8B,SAAS;AACzD,MAAI,aAAa,WAAW;AAC1B,QAAI;AACF,YAAM,IAAI,MAAM,uBAAuB;AACvC,YAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,UAAI,WAAW;AACb,cAAM,KAAK,kBAAkB,SAAS;AAAA,MACxC;AACA,UAAI,WAAW;AACb,cAAM,KAAK,qBAAqB,SAAS;AAAA,MAC3C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,QAAM,MAAM,0BAA0B,KAAK,QAAQ;AACnD,MAAI,QAAQ,IAAI,cAAc,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC1D,MAAI,QAAQ,IAAI,iBAAiB,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC7D,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,6CAA6C,WAAW,YAAY;AAAA,MAClG;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,7 @@ 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 { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
- import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
7
+ import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
8
8
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
9
9
  import { RoleAcl, Role } from "@open-mercato/core/modules/auth/data/entities";
10
10
  import { resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
@@ -152,7 +152,7 @@ async function PUT(req) {
152
152
  let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId });
153
153
  if (acl) {
154
154
  try {
155
- enforceCommandOptimisticLock({
155
+ await enforceCommandOptimisticLockWithGuards(container, {
156
156
  resourceKind: "auth.role_acl",
157
157
  resourceId: acl.id,
158
158
  current: acl.updatedAt ?? null,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/roles/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 { 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 { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminRoleTarget,\n normalizeGrantFeatureList,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\n\ntype TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }\n\nconst getSchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n roleId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().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 roleAclResponseSchema = z.object({\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 roleAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst roleAclErrorSchema = 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({\n roleId: url.searchParams.get('roleId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const em = container.resolve('em') as EntityManager\n const authTenantId = auth.tenantId ?? null\n const roleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n roleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, roleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let tenantScope = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== tenantScope) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) tenantScope = parsed.data.tenantId\n else return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: tenantScope,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\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 acl = tenantScope\n ? await em.findOne(RoleAcl, { role, tenantId: tenantScope })\n : null\n const response = acl\n ? {\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 : { isSuperAdmin: false, features: [], organizations: null, updatedAt: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.roleId, ...response }],\n idField: 'id',\n resourceKind: 'auth.role_acl',\n organizationId: auth.orgId ?? null,\n tenantId: tenantScope,\n query: { roleId: parsed.data.roleId, tenantId: tenantScope },\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 EntityManager\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const rbacService = container.resolve('rbacService') as RbacService\n const authTenantId = auth.tenantId ?? null\n const putRoleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n putRoleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, putRoleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let targetTenantId = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== targetTenantId) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) {\n targetTenantId = parsed.data.tenantId\n } else {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n if (!targetTenantId && !isSuperAdmin) targetTenantId = authTenantId ?? null\n if (!targetTenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!isSuperAdmin && targetTenantId !== authTenantId) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\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 let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })\n // Optimistic lock: refuse a stale ACL overwrite so two admins editing the same\n // role's features in parallel cannot silently clobber each other (#2055). The\n // check is strictly additive \u2014 when the client sends no expected-version header\n // it is a no-op. Skipped when the ACL row does not exist yet (first grant has\n // no prior version to conflict with).\n if (acl) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.role_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 } else {\n acl = em.create(RoleAcl, {\n role,\n tenantId: targetTenantId,\n createdAt: new Date(),\n isSuperAdmin: false,\n })\n }\n\n const existingIsSuperAdmin = !!acl.isSuperAdmin\n const existingFeatures = normalizeGrantFeatureList(acl.featuresJson)\n const existingOrganizations = normalizeOrganizations(acl.organizationsJson)\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n const requestedFeatures = parsed.data.features === undefined\n ? existingFeatures\n : normalizeGrantFeatureList(parsed.data.features)\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n try {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n // Persist the ACL mutation inside a transaction so the role-permission write\n // commits atomically (proper ACL-edit transaction handling).\n const aclToPersist = acl\n await withAtomicFlush(\n em,\n [\n () => {\n aclToPersist.organizationsJson = requestedOrganizations\n aclToPersist.isSuperAdmin = requestedIsSuperAdmin\n aclToPersist.featuresJson = requestedFeatures\n em.persist(aclToPersist)\n },\n ],\n { transaction: true },\n )\n\n // Invalidate cache for all users in this tenant since role ACL changed\n if (targetTenantId) {\n await rbacService.invalidateTenantCache(targetTenantId)\n // Sidebar nav caches depend on RBAC; invalidate tenant scope nav caches\n try {\n const cache = container.resolve('cache') as TaggableCache | undefined\n if (cache?.deleteByTags) await cache.deleteByTags([`rbac:tenant:${targetTenantId}`])\n } catch {}\n }\n \n return NextResponse.json({\n ok: true,\n sanitized: false,\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role ACL management',\n methods: {\n GET: {\n summary: 'Fetch role ACL',\n description: 'Returns the feature and organization assignments associated with a role within the current tenant.',\n query: getSchema,\n responses: [\n { status: 200, description: 'Role ACL entry', schema: roleAclResponseSchema },\n { status: 400, description: 'Invalid role id', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update role ACL',\n description: 'Replaces the feature list, super admin flag, and optional organization assignments for a role.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'Role ACL updated', schema: roleAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\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 { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminRoleTarget,\n normalizeGrantFeatureList,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\n\ntype TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }\n\nconst getSchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n roleId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().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 roleAclResponseSchema = z.object({\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 roleAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst roleAclErrorSchema = 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({\n roleId: url.searchParams.get('roleId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const em = container.resolve('em') as EntityManager\n const authTenantId = auth.tenantId ?? null\n const roleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n roleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, roleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let tenantScope = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== tenantScope) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) tenantScope = parsed.data.tenantId\n else return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: tenantScope,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\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 acl = tenantScope\n ? await em.findOne(RoleAcl, { role, tenantId: tenantScope })\n : null\n const response = acl\n ? {\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 : { isSuperAdmin: false, features: [], organizations: null, updatedAt: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.roleId, ...response }],\n idField: 'id',\n resourceKind: 'auth.role_acl',\n organizationId: auth.orgId ?? null,\n tenantId: tenantScope,\n query: { roleId: parsed.data.roleId, tenantId: tenantScope },\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 EntityManager\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const rbacService = container.resolve('rbacService') as RbacService\n const authTenantId = auth.tenantId ?? null\n const putRoleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n putRoleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, putRoleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let targetTenantId = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== targetTenantId) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) {\n targetTenantId = parsed.data.tenantId\n } else {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n if (!targetTenantId && !isSuperAdmin) targetTenantId = authTenantId ?? null\n if (!targetTenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!isSuperAdmin && targetTenantId !== authTenantId) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\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 let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })\n // Optimistic lock: refuse a stale ACL overwrite so two admins editing the same\n // role's features in parallel cannot silently clobber each other (#2055). The\n // check is strictly additive \u2014 when the client sends no expected-version header\n // it is a no-op. Skipped when the ACL row does not exist yet (first grant has\n // no prior version to conflict with).\n if (acl) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'auth.role_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 } else {\n acl = em.create(RoleAcl, {\n role,\n tenantId: targetTenantId,\n createdAt: new Date(),\n isSuperAdmin: false,\n })\n }\n\n const existingIsSuperAdmin = !!acl.isSuperAdmin\n const existingFeatures = normalizeGrantFeatureList(acl.featuresJson)\n const existingOrganizations = normalizeOrganizations(acl.organizationsJson)\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n const requestedFeatures = parsed.data.features === undefined\n ? existingFeatures\n : normalizeGrantFeatureList(parsed.data.features)\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n try {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n // Persist the ACL mutation inside a transaction so the role-permission write\n // commits atomically (proper ACL-edit transaction handling).\n const aclToPersist = acl\n await withAtomicFlush(\n em,\n [\n () => {\n aclToPersist.organizationsJson = requestedOrganizations\n aclToPersist.isSuperAdmin = requestedIsSuperAdmin\n aclToPersist.featuresJson = requestedFeatures\n em.persist(aclToPersist)\n },\n ],\n { transaction: true },\n )\n\n // Invalidate cache for all users in this tenant since role ACL changed\n if (targetTenantId) {\n await rbacService.invalidateTenantCache(targetTenantId)\n // Sidebar nav caches depend on RBAC; invalidate tenant scope nav caches\n try {\n const cache = container.resolve('cache') as TaggableCache | undefined\n if (cache?.deleteByTags) await cache.deleteByTags([`rbac:tenant:${targetTenantId}`])\n } catch {}\n }\n \n return NextResponse.json({\n ok: true,\n sanitized: false,\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role ACL management',\n methods: {\n GET: {\n summary: 'Fetch role ACL',\n description: 'Returns the feature and organization assignments associated with a role within the current tenant.',\n query: getSchema,\n responses: [\n { status: 200, description: 'Role ACL entry', schema: roleAclResponseSchema },\n { status: 400, description: 'Invalid role id', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update role ACL',\n description: 'Replaces the feature list, super admin flag, and optional organization assignments for a role.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'Role ACL updated', schema: roleAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,9 +4,8 @@ import { signJwt } from "@open-mercato/shared/lib/auth/jwt";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { refreshSessionRequestSchema } from "@open-mercato/core/modules/auth/data/validators";
6
6
  import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
7
- import { buildRequestOriginUrl } from "@open-mercato/core/modules/auth/lib/requestRedirect";
7
+ import { buildSafeRedirectResponse, resolveTrustedRedirectBase } from "@open-mercato/core/modules/auth/lib/requestRedirect";
8
8
  import { sanitizeRedirectPath } from "@open-mercato/core/modules/auth/lib/safeRedirect";
9
- import { getAppBaseUrl } from "@open-mercato/shared/lib/url";
10
9
  import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
11
10
  import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
12
11
  import { z } from "zod";
@@ -46,12 +45,12 @@ function clearStaffAuthCookies(response) {
46
45
  }
47
46
  async function GET(req) {
48
47
  const url = new URL(req.url);
49
- const baseUrl = getAppBaseUrl(req);
48
+ const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin;
50
49
  const redirectTo = sanitizeRedirectPath(url.searchParams.get("redirect"), baseUrl, "/");
51
50
  const token = parseCookie(req, "session_token");
52
51
  if (!token) {
53
52
  return clearStaffAuthCookies(
54
- NextResponse.redirect(buildRequestOriginUrl(req, "/login?redirect=" + encodeURIComponent(redirectTo)))
53
+ buildSafeRedirectResponse(req, "/login?redirect=" + encodeURIComponent(redirectTo))
55
54
  );
56
55
  }
57
56
  const c = await createRequestContainer();
@@ -59,12 +58,12 @@ async function GET(req) {
59
58
  const ctx = await auth.refreshFromSessionToken(token);
60
59
  if (!ctx) {
61
60
  return clearStaffAuthCookies(
62
- NextResponse.redirect(buildRequestOriginUrl(req, "/login?redirect=" + encodeURIComponent(redirectTo)))
61
+ buildSafeRedirectResponse(req, "/login?redirect=" + encodeURIComponent(redirectTo))
63
62
  );
64
63
  }
65
64
  const { user, roles, session } = ctx;
66
65
  const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : void 0, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles });
67
- const res = NextResponse.redirect(buildRequestOriginUrl(req, redirectTo));
66
+ const res = buildSafeRedirectResponse(req, redirectTo);
68
67
  res.cookies.set("auth_token", jwt, { httpOnly: true, path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 60 * 60 * 8 });
69
68
  return res;
70
69
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/session/refresh.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildRequestOriginUrl } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { getAppBaseUrl } from '@open-mercato/shared/lib/url'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = getAppBaseUrl(req)\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.redirect(buildRequestOriginUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.redirect(buildRequestOriginUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n )\n }\n const { user, roles, session } = ctx\n const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : undefined, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })\n const res = NextResponse.redirect(buildRequestOriginUrl(req, redirectTo))\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const { user, roles, session } = ctx\n const jwt = signJwt({\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\n tenantId: String(user.tenantId),\n orgId: String(user.organizationId),\n email: user.email,\n roles,\n })\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const { user, roles, session } = ctx\n const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : undefined, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })\n const res = buildSafeRedirectResponse(req, redirectTo)\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const { user, roles, session } = ctx\n const jwt = signJwt({\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\n tenantId: String(user.tenantId),\n orgId: String(user.organizationId),\n email: user.email,\n roles,\n })\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,7 @@ 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 { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
7
+ import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
8
8
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
9
9
  import { UserAcl } from "@open-mercato/core/modules/auth/data/entities";
10
10
  import {
@@ -134,7 +134,7 @@ async function PUT(req) {
134
134
  let acl = await em.findOne(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
135
135
  if (acl) {
136
136
  try {
137
- enforceCommandOptimisticLock({
137
+ await enforceCommandOptimisticLockWithGuards(container, {
138
138
  resourceKind: "auth.user_acl",
139
139
  resourceId: acl.id,
140
140
  current: acl.updatedAt ?? null,