@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.
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/auth/api/logout.js +2 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +5 -6
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/requestRedirect.js +31 -3
- package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +3 -3
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/route.js +3 -3
- package/dist/modules/business_rules/api/sets/route.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
- package/dist/modules/catalog/api/product-media/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +2 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customers/api/addresses/route.js +4 -2
- package/dist/modules/customers/api/addresses/route.js.map +2 -2
- package/dist/modules/customers/api/tags/route.js +8 -3
- package/dist/modules/customers/api/tags/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +5 -5
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +24 -2
- package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
- package/dist/modules/customers/commands/pipelines.js +24 -2
- package/dist/modules/customers/commands/pipelines.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
- package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +1 -1
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
- package/dist/modules/entities/api/entities.js +2 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +7 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
- package/dist/modules/feature_toggles/data/entities.js +1 -1
- package/dist/modules/feature_toggles/data/entities.js.map +2 -2
- package/dist/modules/feature_toggles/data/validators.js +2 -1
- package/dist/modules/feature_toggles/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/lib/queries.js +2 -1
- package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
- package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -10
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +6 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +3 -3
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +3 -3
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +6 -1
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/di.js +39 -3
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +3 -3
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
- package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
- package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
- package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +7 -1
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
- package/src/modules/auth/api/logout.ts +2 -3
- package/src/modules/auth/api/roles/acl/route.ts +2 -2
- package/src/modules/auth/api/session/refresh.ts +5 -6
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/requestRedirect.ts +44 -2
- package/src/modules/business_rules/api/rules/route.ts +3 -3
- package/src/modules/business_rules/api/sets/route.ts +3 -3
- package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
- package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
- package/src/modules/catalog/api/product-media/route.ts +4 -0
- package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
- package/src/modules/catalog/lib/bulkDelete.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
- package/src/modules/currencies/backend/currencies/page.tsx +2 -1
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
- package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
- package/src/modules/customers/api/addresses/route.ts +2 -0
- package/src/modules/customers/api/tags/route.ts +6 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
- package/src/modules/customers/commands/interactions.ts +5 -5
- package/src/modules/customers/commands/pipeline-stages.ts +26 -2
- package/src/modules/customers/commands/pipelines.ts +26 -2
- package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
- package/src/modules/customers/components/detail/DealForm.tsx +12 -0
- package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
- package/src/modules/data_sync/api/schedules/route.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
- package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
- package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
- package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
- package/src/modules/entities/api/entities.ts +2 -2
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
- package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
- package/src/modules/feature_toggles/data/entities.ts +1 -1
- package/src/modules/feature_toggles/data/validators.ts +1 -0
- package/src/modules/feature_toggles/lib/queries.ts +1 -0
- package/src/modules/inbox_ops/api/settings/route.ts +2 -2
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
- package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
- package/src/modules/sales/commands/documents.ts +11 -10
- package/src/modules/sales/commands/payments.ts +11 -0
- package/src/modules/sales/commands/returns.ts +3 -3
- package/src/modules/sales/commands/shared.ts +10 -4
- package/src/modules/sales/commands/shipments.ts +14 -0
- package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
- package/src/modules/sales/di.ts +68 -9
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/commands/job-histories.ts +3 -3
- package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
- package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
- package/src/modules/translations/components/TranslationManager.tsx +14 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '@open-mercato/ui/primitives/dialog'
|
|
25
25
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
26
26
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
27
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
27
28
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
28
29
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
29
30
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -257,6 +258,7 @@ export function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSe
|
|
|
257
258
|
await loadItems()
|
|
258
259
|
} catch (err) {
|
|
259
260
|
console.error('[attachments.partitions] save failed', err)
|
|
261
|
+
if (surfaceRecordConflict(err, t)) return
|
|
260
262
|
const message =
|
|
261
263
|
err instanceof Error ? err.message : t('attachments.partitions.errors.save', 'Failed to save partition.')
|
|
262
264
|
setError(message)
|
|
@@ -294,6 +296,7 @@ export function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSe
|
|
|
294
296
|
await loadItems()
|
|
295
297
|
} catch (err) {
|
|
296
298
|
console.error('[attachments.partitions] delete failed', err)
|
|
299
|
+
if (surfaceRecordConflict(err, t)) return
|
|
297
300
|
flash(t('attachments.partitions.errors.delete', 'Failed to delete partition.'), 'error')
|
|
298
301
|
}
|
|
299
302
|
},
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
1
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
3
|
import { AuthService } from '@open-mercato/core/modules/auth/services/authService'
|
|
5
4
|
import { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
6
|
-
import {
|
|
5
|
+
import { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'
|
|
7
6
|
|
|
8
7
|
function parseCookie(req: Request, name: string): string | null {
|
|
9
8
|
const cookie = req.headers.get('cookie') || ''
|
|
@@ -39,7 +38,7 @@ export async function POST(req: Request) {
|
|
|
39
38
|
}
|
|
40
39
|
} catch {}
|
|
41
40
|
}
|
|
42
|
-
const res =
|
|
41
|
+
const res = buildSafeRedirectResponse(req, '/login')
|
|
43
42
|
res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })
|
|
44
43
|
res.cookies.set('session_token', '', { path: '/', maxAge: 0 })
|
|
45
44
|
return res
|
|
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
|
|
7
7
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
-
import {
|
|
8
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
9
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
10
10
|
import { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
11
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -183,7 +183,7 @@ export async function PUT(req: Request) {
|
|
|
183
183
|
// no prior version to conflict with).
|
|
184
184
|
if (acl) {
|
|
185
185
|
try {
|
|
186
|
-
|
|
186
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
187
187
|
resourceKind: 'auth.role_acl',
|
|
188
188
|
resourceId: acl.id,
|
|
189
189
|
current: acl.updatedAt ?? null,
|
|
@@ -6,9 +6,8 @@ import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
|
6
6
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
7
|
import { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'
|
|
8
8
|
import { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
|
|
9
|
-
import {
|
|
9
|
+
import { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'
|
|
10
10
|
import { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'
|
|
11
|
-
import { getAppBaseUrl } from '@open-mercato/shared/lib/url'
|
|
12
11
|
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
13
12
|
import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
14
13
|
import { z } from 'zod'
|
|
@@ -46,12 +45,12 @@ function clearStaffAuthCookies(response: NextResponse) {
|
|
|
46
45
|
|
|
47
46
|
export async function GET(req: Request) {
|
|
48
47
|
const url = new URL(req.url)
|
|
49
|
-
const baseUrl =
|
|
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
|
-
|
|
53
|
+
buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))
|
|
55
54
|
)
|
|
56
55
|
}
|
|
57
56
|
const c = await createRequestContainer()
|
|
@@ -59,12 +58,12 @@ export async function GET(req: Request) {
|
|
|
59
58
|
const ctx = await auth.refreshFromSessionToken(token)
|
|
60
59
|
if (!ctx) {
|
|
61
60
|
return clearStaffAuthCookies(
|
|
62
|
-
|
|
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) : undefined, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })
|
|
67
|
-
const res =
|
|
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
|
}
|
|
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
|
|
7
7
|
import { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
-
import {
|
|
8
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
9
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
10
10
|
import { UserAcl } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
11
|
import {
|
|
@@ -159,7 +159,7 @@ export async function PUT(req: Request) {
|
|
|
159
159
|
// the client sends no expected-version header; skipped when no ACL row exists.
|
|
160
160
|
if (acl) {
|
|
161
161
|
try {
|
|
162
|
-
|
|
162
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
163
163
|
resourceKind: 'auth.user_acl',
|
|
164
164
|
resourceId: acl.id,
|
|
165
165
|
current: acl.updatedAt ?? null,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
3
4
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
4
5
|
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
5
6
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
6
8
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
7
9
|
import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
8
10
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
@@ -37,6 +39,7 @@ type RoleListResponse = {
|
|
|
37
39
|
export default function EditRolePage({ params }: { params?: { id?: string } }) {
|
|
38
40
|
const id = params?.id
|
|
39
41
|
const t = useT()
|
|
42
|
+
const pathname = usePathname()
|
|
40
43
|
const [initial, setInitial] = React.useState<RoleRecord | null>(null)
|
|
41
44
|
const [loading, setLoading] = React.useState(true)
|
|
42
45
|
const [aclData, setAclData] = React.useState<AclData>({ isSuperAdmin: false, features: [], organizations: null })
|
|
@@ -189,6 +192,20 @@ export default function EditRolePage({ params }: { params?: { id?: string } }) {
|
|
|
189
192
|
},
|
|
190
193
|
]), [aclData, actorIsSuperAdmin, detailFieldIds, id, initial, loading, selectedTenantId, t])
|
|
191
194
|
|
|
195
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
196
|
+
// mount so the enterprise record_locks widget resolves `auth.role` + id explicitly.
|
|
197
|
+
// The resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
|
|
198
|
+
// the save-time conflict surface for the same role.
|
|
199
|
+
useSetCurrentRecordInjectionContext(
|
|
200
|
+
buildRecordInjectionContext({
|
|
201
|
+
resourceKind: 'auth.role',
|
|
202
|
+
resourceId: id || null,
|
|
203
|
+
updatedAt: initial?.updatedAt ?? null,
|
|
204
|
+
data: initial as Record<string, unknown> | null,
|
|
205
|
+
path: pathname,
|
|
206
|
+
}),
|
|
207
|
+
)
|
|
208
|
+
|
|
192
209
|
if (!id) return null
|
|
193
210
|
return (
|
|
194
211
|
<Page>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
3
4
|
import { E } from '#generated/entities.ids.generated'
|
|
4
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
6
|
import { CrudForm, type CrudField, type CrudFormGroup, type CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -19,6 +20,7 @@ import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-
|
|
|
19
20
|
import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
20
21
|
import { UserConsentsPanel } from '@open-mercato/core/modules/auth/components/UserConsentsPanel'
|
|
21
22
|
import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
23
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
22
24
|
import { normalizeDisplayNameInput } from '@open-mercato/core/modules/auth/lib/displayName'
|
|
23
25
|
|
|
24
26
|
type EditUserFormValues = {
|
|
@@ -123,6 +125,7 @@ function TenantAwareOrganizationSelectInput({
|
|
|
123
125
|
export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
124
126
|
const id = params?.id
|
|
125
127
|
const t = useT()
|
|
128
|
+
const pathname = usePathname()
|
|
126
129
|
const tRef = React.useRef(t)
|
|
127
130
|
tRef.current = t
|
|
128
131
|
const [initialUser, setInitialUser] = React.useState<LoadedUser | null>(null)
|
|
@@ -473,6 +476,20 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
473
476
|
}
|
|
474
477
|
}, [initialUser, customFieldValues, selectedTenantId])
|
|
475
478
|
|
|
479
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
480
|
+
// mount so the enterprise record_locks widget resolves `auth.user` + id explicitly.
|
|
481
|
+
// The resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
|
|
482
|
+
// the save-time conflict surface for the same user.
|
|
483
|
+
useSetCurrentRecordInjectionContext(
|
|
484
|
+
buildRecordInjectionContext({
|
|
485
|
+
resourceKind: 'auth.user',
|
|
486
|
+
resourceId: id || null,
|
|
487
|
+
updatedAt: initialUser?.updatedAt ?? null,
|
|
488
|
+
data: initialUser as Record<string, unknown> | null,
|
|
489
|
+
path: pathname,
|
|
490
|
+
}),
|
|
491
|
+
)
|
|
492
|
+
|
|
476
493
|
if (isNotFound) {
|
|
477
494
|
return (
|
|
478
495
|
<Page>
|
|
@@ -273,7 +273,7 @@ export class RoleAcl {
|
|
|
273
273
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
274
274
|
createdAt: Date = new Date()
|
|
275
275
|
|
|
276
|
-
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
|
|
276
|
+
@Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date(), nullable: true })
|
|
277
277
|
updatedAt?: Date
|
|
278
278
|
|
|
279
279
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
@@ -308,7 +308,7 @@ export class UserAcl {
|
|
|
308
308
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
309
309
|
createdAt: Date = new Date()
|
|
310
310
|
|
|
311
|
-
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
|
|
311
|
+
@Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date(), nullable: true })
|
|
312
312
|
updatedAt?: Date
|
|
313
313
|
|
|
314
314
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
@@ -1,5 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSecurityEmailBaseUrl } from '@open-mercato/shared/lib/url'
|
|
2
3
|
|
|
4
|
+
function toRelativeRedirectPath(path: string): string {
|
|
5
|
+
// Strip any scheme/authority so the value is always treated as a path on the
|
|
6
|
+
// current origin. Both `new URL(path, base)` and the browser would otherwise
|
|
7
|
+
// honour a protocol-relative (`//host`) or backslash-smuggled value as a
|
|
8
|
+
// cross-origin target supplied by a spoofed Host / X-Forwarded-Host.
|
|
9
|
+
const withoutBackslashes = path.replace(/\\/g, '/')
|
|
10
|
+
const rooted = withoutBackslashes.startsWith('/') ? withoutBackslashes : `/${withoutBackslashes}`
|
|
11
|
+
return rooted.replace(/^\/+/, '/')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveTrustedRedirectBase(req: Request): string | null {
|
|
15
|
+
try {
|
|
16
|
+
return getSecurityEmailBaseUrl(req)
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveSafeRedirectLocation(req: Request, path: string): string {
|
|
23
|
+
const safePath = toRelativeRedirectPath(path)
|
|
24
|
+
const base = resolveTrustedRedirectBase(req)
|
|
25
|
+
if (base) return new URL(safePath, base).toString()
|
|
26
|
+
return safePath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSafeRedirectResponse(req: Request, path: string, status: number = 307): NextResponse {
|
|
30
|
+
return new NextResponse(null, {
|
|
31
|
+
status,
|
|
32
|
+
headers: { Location: resolveSafeRedirectLocation(req, path) },
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Use {@link buildSafeRedirectResponse} or {@link resolveSafeRedirectLocation}.
|
|
38
|
+
* Previously derived the redirect origin from raw request headers, which
|
|
39
|
+
* allowed open redirects via a spoofed Host / X-Forwarded-Host. It now returns
|
|
40
|
+
* an allowlist-validated app origin when the request origin is trusted, and a
|
|
41
|
+
* host-relative path otherwise — the latter may be a relative URL, so callers
|
|
42
|
+
* MUST NOT pass the result to `NextResponse.redirect` (use
|
|
43
|
+
* {@link buildSafeRedirectResponse} instead).
|
|
44
|
+
*/
|
|
3
45
|
export function buildRequestOriginUrl(req: Request, path: string): string {
|
|
4
|
-
return
|
|
46
|
+
return resolveSafeRedirectLocation(req, path)
|
|
5
47
|
}
|
|
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
7
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
-
import {
|
|
8
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
9
|
import { BusinessRule } from '../../data/entities'
|
|
10
10
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
11
11
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
@@ -280,7 +280,7 @@ export async function PUT(req: Request) {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
try {
|
|
283
|
-
|
|
283
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
284
284
|
resourceKind: 'business_rules.rule',
|
|
285
285
|
resourceId: rule.id,
|
|
286
286
|
current: rule.updatedAt ?? null,
|
|
@@ -338,7 +338,7 @@ export async function DELETE(req: Request) {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
try {
|
|
341
|
-
|
|
341
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
342
342
|
resourceKind: 'business_rules.rule',
|
|
343
343
|
resourceId: rule.id,
|
|
344
344
|
current: rule.updatedAt ?? null,
|
|
@@ -4,7 +4,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
4
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
-
import {
|
|
7
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
8
8
|
import { RuleSet } from '../../data/entities'
|
|
9
9
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
10
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
@@ -224,7 +224,7 @@ export async function PUT(req: Request) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
try {
|
|
227
|
-
|
|
227
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
228
228
|
resourceKind: 'business_rules.ruleSet',
|
|
229
229
|
resourceId: ruleSet.id,
|
|
230
230
|
current: ruleSet.updatedAt ?? null,
|
|
@@ -271,7 +271,7 @@ export async function DELETE(req: Request) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
try {
|
|
274
|
-
|
|
274
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
275
275
|
resourceKind: 'business_rules.ruleSet',
|
|
276
276
|
resourceId: ruleSet.id,
|
|
277
277
|
current: ruleSet.updatedAt ?? null,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import { useRouter, useParams } from 'next/navigation'
|
|
5
|
+
import { useRouter, useParams, usePathname } from 'next/navigation'
|
|
6
6
|
import { useQuery } from '@tanstack/react-query'
|
|
7
7
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
8
8
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -10,6 +10,7 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
|
10
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
11
|
import { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'
|
|
12
12
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
13
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
13
14
|
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
14
15
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
15
16
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -27,6 +28,7 @@ import { buildRulePayload, parseRuleToFormValues } from '../../../components/uti
|
|
|
27
28
|
export default function EditBusinessRulePage() {
|
|
28
29
|
const router = useRouter()
|
|
29
30
|
const params = useParams()
|
|
31
|
+
const pathname = usePathname()
|
|
30
32
|
|
|
31
33
|
// Handle catch-all route: params.slug = ['rules', 'uuid']
|
|
32
34
|
let ruleId: string | undefined
|
|
@@ -103,6 +105,20 @@ export default function EditBusinessRulePage() {
|
|
|
103
105
|
[t]
|
|
104
106
|
)
|
|
105
107
|
|
|
108
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
109
|
+
// mount so the enterprise record_locks widget resolves `business_rules.rule` + id
|
|
110
|
+
// explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`
|
|
111
|
+
// call so the held lock matches the save-time conflict surface for the same rule.
|
|
112
|
+
useSetCurrentRecordInjectionContext(
|
|
113
|
+
buildRecordInjectionContext({
|
|
114
|
+
resourceKind: 'business_rules.rule',
|
|
115
|
+
resourceId: ruleId ?? null,
|
|
116
|
+
updatedAt: rule?.updatedAt ?? rule?.updated_at ?? null,
|
|
117
|
+
data: (rule ?? null) as Record<string, unknown> | null,
|
|
118
|
+
path: pathname,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
106
122
|
if (isLoading) {
|
|
107
123
|
return (
|
|
108
124
|
<Page>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import { useRouter, useParams } from 'next/navigation'
|
|
5
|
+
import { useRouter, useParams, usePathname } from 'next/navigation'
|
|
6
6
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
7
7
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
8
8
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -12,6 +12,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
12
12
|
import { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'
|
|
13
13
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
15
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
15
16
|
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
16
17
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
17
18
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
@@ -55,6 +56,7 @@ type RuleSetDetail = {
|
|
|
55
56
|
export default function EditRuleSetPage() {
|
|
56
57
|
const router = useRouter()
|
|
57
58
|
const params = useParams()
|
|
59
|
+
const pathname = usePathname()
|
|
58
60
|
|
|
59
61
|
// Handle catch-all route: params.slug = ['sets', 'uuid']
|
|
60
62
|
let setId: string | undefined
|
|
@@ -237,6 +239,21 @@ export default function EditRuleSetPage() {
|
|
|
237
239
|
]
|
|
238
240
|
}, [t, ruleSet, handleAddMember, handleUpdateMember, handleRemoveMember])
|
|
239
241
|
|
|
242
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
243
|
+
// mount so the enterprise record_locks widget resolves the rule set + id
|
|
244
|
+
// explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`
|
|
245
|
+
// call (`business_rules.ruleSet`) so the held lock matches the save-time conflict
|
|
246
|
+
// surface for the same rule set.
|
|
247
|
+
useSetCurrentRecordInjectionContext(
|
|
248
|
+
buildRecordInjectionContext({
|
|
249
|
+
resourceKind: 'business_rules.ruleSet',
|
|
250
|
+
resourceId: setId ?? null,
|
|
251
|
+
updatedAt: ruleSet?.updatedAt ?? null,
|
|
252
|
+
data: (ruleSet ?? null) as Record<string, unknown> | null,
|
|
253
|
+
path: pathname,
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
|
|
240
257
|
if (isLoading) {
|
|
241
258
|
return (
|
|
242
259
|
<Page>
|
|
@@ -10,6 +10,10 @@ import { CatalogProduct } from '../../data/entities'
|
|
|
10
10
|
import { E } from '#generated/entities.ids.generated'
|
|
11
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
12
|
|
|
13
|
+
// record_locks decision: this route is read-only (GET only) and projects the
|
|
14
|
+
// attachment side-table for a product. It is EXEMPT from the optimistic-lock /
|
|
15
|
+
// record-lock guard (Phase 4) — there is no mutating method here, and media
|
|
16
|
+
// uploads/deletes are owned by the attachments module's own write paths.
|
|
13
17
|
export const metadata = {
|
|
14
18
|
GET: { requireAuth: true, requireFeatures: ['catalog.products.view'] },
|
|
15
19
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
4
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
6
|
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
6
7
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -8,6 +9,7 @@ import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
|
8
9
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
9
10
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
10
11
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
12
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
11
13
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
14
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
13
15
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -81,6 +83,7 @@ async function submitCategoryUpdate(
|
|
|
81
83
|
export default function EditCatalogCategoryPage({ params }: { params?: { id?: string } }) {
|
|
82
84
|
const categoryId = params?.id ?? ''
|
|
83
85
|
const t = useT()
|
|
86
|
+
const pathname = usePathname()
|
|
84
87
|
const [initialValues, setInitialValues] = React.useState<CategoryFormValues | null>(null)
|
|
85
88
|
const [pathLabel, setPathLabel] = React.useState<string>('')
|
|
86
89
|
const [loading, setLoading] = React.useState<boolean>(true)
|
|
@@ -195,6 +198,20 @@ export default function EditCatalogCategoryPage({ params }: { params?: { id?: st
|
|
|
195
198
|
},
|
|
196
199
|
], [t])
|
|
197
200
|
|
|
201
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
202
|
+
// mount so the enterprise record_locks widget resolves `catalog.category` + id
|
|
203
|
+
// explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
|
|
204
|
+
// lock matches the save-time conflict surface for the same category.
|
|
205
|
+
useSetCurrentRecordInjectionContext(
|
|
206
|
+
buildRecordInjectionContext({
|
|
207
|
+
resourceKind: 'catalog.category',
|
|
208
|
+
resourceId: categoryId || null,
|
|
209
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
210
|
+
data: initialValues as Record<string, unknown> | null,
|
|
211
|
+
path: pathname,
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
|
|
198
215
|
if (!categoryId) {
|
|
199
216
|
return (
|
|
200
217
|
<Page>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
5
6
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
6
7
|
import { ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
7
8
|
import {
|
|
@@ -41,6 +42,10 @@ import {
|
|
|
41
42
|
} from "@open-mercato/ui/backend/utils/apiCall";
|
|
42
43
|
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
43
44
|
import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
|
|
45
|
+
import {
|
|
46
|
+
buildRecordInjectionContext,
|
|
47
|
+
useSetCurrentRecordInjectionContext,
|
|
48
|
+
} from "@open-mercato/ui/backend/injection/recordContext";
|
|
44
49
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
45
50
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
46
51
|
import { E } from "#generated/entities.ids.generated";
|
|
@@ -314,6 +319,7 @@ export default function EditCatalogProductPage({
|
|
|
314
319
|
}) {
|
|
315
320
|
const productId = params?.id ? String(params.id) : null;
|
|
316
321
|
const t = useT();
|
|
322
|
+
const pathname = usePathname();
|
|
317
323
|
const productSubpathPrefix = productId
|
|
318
324
|
? `/backend/catalog/products/${productId}/`
|
|
319
325
|
: null;
|
|
@@ -835,6 +841,21 @@ export default function EditCatalogProductPage({
|
|
|
835
841
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
836
842
|
})
|
|
837
843
|
|
|
844
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
845
|
+
// mount so the enterprise record_locks widget resolves `catalog.product` + id
|
|
846
|
+
// explicitly (presence/acquire/heartbeat on load; cleared on unmount). The
|
|
847
|
+
// resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
|
|
848
|
+
// the save-time conflict surface for the same product.
|
|
849
|
+
useSetCurrentRecordInjectionContext(
|
|
850
|
+
buildRecordInjectionContext({
|
|
851
|
+
resourceKind: "catalog.product",
|
|
852
|
+
resourceId: productId,
|
|
853
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
854
|
+
data: initialValues as Record<string, unknown> | null,
|
|
855
|
+
path: pathname,
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
|
|
838
859
|
const handleVariantDeleted = React.useCallback((variantId: string) => {
|
|
839
860
|
setVariants((prev) => prev.filter((variant) => variant.id !== variantId));
|
|
840
861
|
}, []);
|
package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
7
7
|
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
@@ -10,6 +10,7 @@ import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customF
|
|
|
10
10
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
11
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
12
12
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
13
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
13
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
15
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
15
16
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -100,6 +101,7 @@ function resolveVariantPriceLabel(prices: Record<string, VariantPriceDraft> | un
|
|
|
100
101
|
export default function EditVariantPage({ params }: { params?: { productId?: string; variantId?: string } }) {
|
|
101
102
|
const router = useRouter()
|
|
102
103
|
const t = useT()
|
|
104
|
+
const pathname = usePathname()
|
|
103
105
|
const productId = params?.productId ? String(params.productId) : null
|
|
104
106
|
const variantId = params?.variantId ? String(params.variantId) : null
|
|
105
107
|
const isCreateSentinel = variantId === 'create'
|
|
@@ -476,6 +478,19 @@ export default function EditVariantPage({ params }: { params?: { productId?: str
|
|
|
476
478
|
return list
|
|
477
479
|
}, [optionDefinitions, priceKinds, t, taxRates])
|
|
478
480
|
|
|
481
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
482
|
+
// mount so the enterprise record_locks widget resolves `catalog.variant` + id
|
|
483
|
+
// explicitly. Skipped on the create-delegation path (no record to lock).
|
|
484
|
+
useSetCurrentRecordInjectionContext(
|
|
485
|
+
buildRecordInjectionContext({
|
|
486
|
+
resourceKind: 'catalog.variant',
|
|
487
|
+
resourceId: isCreateSentinel ? null : variantId,
|
|
488
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
489
|
+
data: initialValues as Record<string, unknown> | null,
|
|
490
|
+
path: pathname,
|
|
491
|
+
}),
|
|
492
|
+
)
|
|
493
|
+
|
|
479
494
|
if (isCreateSentinel) {
|
|
480
495
|
if (!productId) {
|
|
481
496
|
return (
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from '@open-mercato/ui/primitives/dialog'
|
|
19
19
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
20
20
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
21
|
-
import { buildOptimisticLockHeader
|
|
21
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
22
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
22
23
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
23
24
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
24
25
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -236,9 +237,15 @@ export function PriceKindSettings() {
|
|
|
236
237
|
await loadItems()
|
|
237
238
|
} catch (err) {
|
|
238
239
|
console.error('catalog.price-kinds.save failed', err)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// Route a concurrent-edit 409 through the single conflict surface (unified
|
|
241
|
+
// conflict bar, or the enterprise merge dialog when its handler is mounted)
|
|
242
|
+
// and close the editor so that surface owns the resolution. Other errors
|
|
243
|
+
// stay inline in the dialog so the user can correct and resubmit.
|
|
244
|
+
if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadItems() } })) {
|
|
245
|
+
closeDialog()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const message = err instanceof Error ? err.message : t('catalog.priceKinds.errors.save', 'Failed to save price kind.')
|
|
242
249
|
setError(message)
|
|
243
250
|
} finally {
|
|
244
251
|
setSubmitting(false)
|
|
@@ -270,9 +277,10 @@ export function PriceKindSettings() {
|
|
|
270
277
|
await loadItems()
|
|
271
278
|
} catch (err) {
|
|
272
279
|
console.error('catalog.price-kinds.delete failed', err)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
280
|
+
// Route a concurrent-edit 409 through the single conflict surface; fall
|
|
281
|
+
// back to a flash for any other delete failure.
|
|
282
|
+
if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadItems() } })) return
|
|
283
|
+
const message = err instanceof Error ? err.message : t('catalog.priceKinds.errors.delete', 'Failed to delete price kind.')
|
|
276
284
|
flash(message, 'error')
|
|
277
285
|
}
|
|
278
286
|
},
|
|
@@ -82,6 +82,12 @@ export async function deleteCatalogProductsWithProgress(params: {
|
|
|
82
82
|
let affectedCount = 0
|
|
83
83
|
const deletedIds = new Set<string>()
|
|
84
84
|
|
|
85
|
+
// record_locks decision: bulk product delete is EXEMPT from the optimistic-lock
|
|
86
|
+
// / record-lock guard (Phase 4). It is an explicit destructive operation over a
|
|
87
|
+
// multi-selection, not a collaborative field edit, and the queued job has no
|
|
88
|
+
// per-row expected `updated_at` to compare against — mirroring the customers
|
|
89
|
+
// bulk pattern from Phase 2. Single-product delete on the detail screen still
|
|
90
|
+
// rides the CRUD mutation-guard decorator with the version header.
|
|
85
91
|
for (const [index, id] of ids.entries()) {
|
|
86
92
|
await commandBus.execute<{ body?: Record<string, unknown> }, { productId: string }>('catalog.products.delete', {
|
|
87
93
|
input: { body: { id } },
|