@open-mercato/core 0.6.6 → 0.6.7-develop.6553.1.2ec7246e04
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/gateway_session_initialization/index.js +23 -0
- package/dist/generated/entities/gateway_session_initialization/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +12 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/api/keys/route.js +30 -13
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/cli.js +2 -1
- package/dist/modules/api_keys/cli.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +9 -7
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +9 -7
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +21 -17
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentContentPreview.js +6 -35
- package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
- package/dist/modules/attachments/lib/reconcileOrganization.js +110 -0
- package/dist/modules/attachments/lib/reconcileOrganization.js.map +7 -0
- package/dist/modules/attachments/lib/requestScope.js +10 -0
- package/dist/modules/attachments/lib/requestScope.js.map +7 -0
- package/dist/modules/audit_logs/backend/audit-logs/page.js +14 -24
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/auth/api/autologin.js +85 -0
- package/dist/modules/auth/api/autologin.js.map +7 -0
- package/dist/modules/auth/api/users/route.js +6 -2
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/cli.js +3 -2
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/lib/autologin.js +19 -0
- package/dist/modules/auth/lib/autologin.js.map +7 -0
- package/dist/modules/configs/lib/upgrade-actions.js +22 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +1 -0
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +17 -3
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +6 -1
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +6 -1
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerRbacService.js +6 -1
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +0 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -43
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +23 -35
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DetailTabsLayout.js +6 -21
- package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
- package/dist/modules/customers/components/detail/MobilePersonDetail.js +26 -32
- package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +29 -39
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
- package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
- package/dist/modules/dashboards/api/roles/widgets/route.js +22 -3
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +22 -3
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/cli.js +4 -3
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js +9 -1
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
- package/dist/modules/dictionaries/commands/entries.js +7 -14
- package/dist/modules/dictionaries/commands/entries.js.map +2 -2
- package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
- package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +9 -3
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +56 -41
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +10 -3
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +5 -4
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +1 -0
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +1 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/cli.js +2 -1
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/feature_toggles/api/check/boolean/route.js +0 -14
- package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
- package/dist/modules/feature_toggles/defaults.json +8 -0
- package/dist/modules/integrations/backend/integrations/[id]/page.js +9 -80
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +45 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +173 -38
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
- package/dist/modules/payment_gateways/lib/session-idempotency.js +104 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +7 -0
- package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +11 -0
- package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +7 -0
- package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +16 -14
- package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +18 -30
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/status.js +2 -8
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/create/page.js +6 -0
- package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/create/page.meta.js +2 -0
- package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +2 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +6 -0
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.meta.js +2 -0
- package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -4
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +44 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +2 -27
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +15 -0
- package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
- package/dist/modules/sales/components/documents/AddressesSection.js +6 -2
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +1 -0
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +9 -0
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +51 -3
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -8
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +59 -38
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +44 -0
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +7 -0
- package/dist/modules/sales/lib/calculations.js +6 -0
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/salesChannelsToggle.js +18 -0
- package/dist/modules/sales/lib/salesChannelsToggle.js.map +7 -0
- package/dist/modules/sales/lib/salesChannelsToggleId.js +5 -0
- package/dist/modules/sales/lib/salesChannelsToggleId.js.map +7 -0
- package/dist/modules/sales/lib/shipments/snapshots.js +27 -1
- package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +22 -25
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -14
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +10 -29
- package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +19 -20
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/sync_excel/lib/parser.js +185 -0
- package/dist/modules/sync_excel/lib/parser.js.map +2 -2
- package/dist/modules/sync_excel/lib/upload-storage.js +20 -1
- package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
- package/generated/entities/gateway_session_initialization/index.ts +10 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +12 -0
- package/package.json +9 -8
- package/src/modules/api_keys/api/keys/route.ts +36 -13
- package/src/modules/api_keys/cli.ts +2 -3
- package/src/modules/attachments/api/file/[id]/route.ts +11 -7
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +11 -7
- package/src/modules/attachments/api/route.ts +26 -21
- package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
- package/src/modules/attachments/lib/reconcileOrganization.ts +188 -0
- package/src/modules/attachments/lib/requestScope.ts +27 -0
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +12 -22
- package/src/modules/auth/api/autologin.ts +100 -0
- package/src/modules/auth/api/users/route.ts +11 -2
- package/src/modules/auth/cli.ts +3 -2
- package/src/modules/auth/lib/autologin.ts +36 -0
- package/src/modules/configs/i18n/de.json +5 -1
- package/src/modules/configs/i18n/en.json +5 -1
- package/src/modules/configs/i18n/es.json +5 -1
- package/src/modules/configs/i18n/pl.json +5 -1
- package/src/modules/configs/lib/upgrade-actions.ts +24 -0
- package/src/modules/customer_accounts/AGENTS.md +8 -6
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +1 -0
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +17 -3
- package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +6 -1
- package/src/modules/customer_accounts/api/portal/users-invite.ts +6 -1
- package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -0
- package/src/modules/customer_accounts/services/customerRbacService.ts +9 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +39 -55
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +24 -49
- package/src/modules/customers/components/detail/DetailTabsLayout.tsx +13 -24
- package/src/modules/customers/components/detail/MobilePersonDetail.tsx +25 -31
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +29 -50
- package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
- package/src/modules/dashboards/api/roles/widgets/route.ts +22 -3
- package/src/modules/dashboards/api/users/widgets/route.ts +22 -3
- package/src/modules/dashboards/cli.ts +4 -9
- package/src/modules/dashboards/lib/widgetAssignmentScope.ts +18 -0
- package/src/modules/dictionaries/commands/entries.ts +7 -16
- package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
- package/src/modules/dictionaries/commands/factory.ts +8 -2
- package/src/modules/dictionaries/fields/dictionary.tsx +55 -32
- package/src/modules/dictionaries/i18n/de.json +2 -0
- package/src/modules/dictionaries/i18n/en.json +2 -0
- package/src/modules/dictionaries/i18n/es.json +2 -0
- package/src/modules/dictionaries/i18n/pl.json +2 -0
- package/src/modules/directory/utils/organizationScope.ts +36 -2
- package/src/modules/entities/api/records.ts +5 -7
- package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +1 -0
- package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +1 -0
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +51 -49
- package/src/modules/entities/cli.ts +2 -1
- package/src/modules/entities/i18n/de.json +34 -0
- package/src/modules/entities/i18n/en.json +34 -0
- package/src/modules/entities/i18n/es.json +34 -0
- package/src/modules/entities/i18n/pl.json +34 -0
- package/src/modules/feature_toggles/api/check/boolean/route.ts +0 -15
- package/src/modules/feature_toggles/defaults.json +8 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +16 -59
- package/src/modules/payment_gateways/data/entities.ts +39 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +211 -40
- package/src/modules/payment_gateways/lib/session-idempotency.ts +142 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +236 -0
- package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +10 -0
- package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +14 -20
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +16 -34
- package/src/modules/query_index/api/status.ts +2 -8
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +27 -37
- package/src/modules/sales/backend/sales/channels/create/page.meta.ts +3 -0
- package/src/modules/sales/backend/sales/channels/create/page.tsx +7 -0
- package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +3 -0
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -0
- package/src/modules/sales/backend/sales/channels/page.meta.ts +4 -0
- package/src/modules/sales/backend/sales/channels/page.tsx +7 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +7 -5
- package/src/modules/sales/commands/documents.ts +65 -0
- package/src/modules/sales/commands/returns.ts +2 -32
- package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +21 -0
- package/src/modules/sales/components/documents/AddressesSection.tsx +6 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +5 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +10 -0
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +53 -3
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +65 -40
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
- package/src/modules/sales/i18n/de.json +5 -0
- package/src/modules/sales/i18n/en.json +5 -0
- package/src/modules/sales/i18n/es.json +5 -0
- package/src/modules/sales/i18n/pl.json +5 -0
- package/src/modules/sales/lib/calculations.ts +15 -0
- package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
- package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -0
- package/src/modules/sales/lib/shipments/snapshots.ts +30 -0
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +25 -34
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +11 -17
- package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +11 -28
- package/src/modules/sync_excel/lib/adapters/customers.ts +20 -22
- package/src/modules/sync_excel/lib/parser.ts +245 -0
- package/src/modules/sync_excel/lib/upload-storage.ts +23 -1
|
@@ -6,6 +6,7 @@ import { Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
|
6
6
|
import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
|
|
7
7
|
import { appendWidgetsToRoles, resolveAnalyticsWidgetIds } from '@open-mercato/core/modules/dashboards/lib/role-widgets'
|
|
8
8
|
import { seedAnalyticsData } from './seed/analytics'
|
|
9
|
+
import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
|
|
9
10
|
|
|
10
11
|
type Args = Record<string, string>
|
|
11
12
|
|
|
@@ -108,10 +109,7 @@ const seedDefaults: ModuleCli = {
|
|
|
108
109
|
return
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const roleNames = roleCsv
|
|
112
|
-
.split(',')
|
|
113
|
-
.map((name) => name.trim())
|
|
114
|
-
.filter(Boolean)
|
|
112
|
+
const roleNames = parseCommaSeparatedList(roleCsv)
|
|
115
113
|
|
|
116
114
|
if (!roleNames.length) {
|
|
117
115
|
console.log('No roles provided, nothing to seed.')
|
|
@@ -125,7 +123,7 @@ const seedDefaults: ModuleCli = {
|
|
|
125
123
|
tenantId,
|
|
126
124
|
organizationId,
|
|
127
125
|
roleNames,
|
|
128
|
-
widgetIds: widgetCsv ? widgetCsv
|
|
126
|
+
widgetIds: widgetCsv ? parseCommaSeparatedList(widgetCsv) : undefined,
|
|
129
127
|
logger: (message) => console.log(message),
|
|
130
128
|
})
|
|
131
129
|
},
|
|
@@ -143,10 +141,7 @@ const enableAnalyticsWidgets: ModuleCli = {
|
|
|
143
141
|
return
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
const roleNames = roleCsv
|
|
147
|
-
.split(',')
|
|
148
|
-
.map((name) => name.trim())
|
|
149
|
-
.filter(Boolean)
|
|
144
|
+
const roleNames = parseCommaSeparatedList(roleCsv)
|
|
150
145
|
|
|
151
146
|
if (!roleNames.length) {
|
|
152
147
|
console.log('No roles provided, nothing to update.')
|
|
@@ -8,6 +8,24 @@ export type ResolvedAssignmentScope = {
|
|
|
8
8
|
organizationId: string | null
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type WidgetAssignmentTarget = {
|
|
12
|
+
tenantId?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'
|
|
16
|
+
|
|
17
|
+
export function resolveWidgetAssignmentTargetAccess(params: {
|
|
18
|
+
isSuperAdmin: boolean
|
|
19
|
+
scopeTenantId: string | null
|
|
20
|
+
target: WidgetAssignmentTarget | null | undefined
|
|
21
|
+
}): WidgetAssignmentTargetAccess {
|
|
22
|
+
const { isSuperAdmin, scopeTenantId, target } = params
|
|
23
|
+
if (!isSuperAdmin && !scopeTenantId) return 'forbidden'
|
|
24
|
+
if (!target) return 'not-found'
|
|
25
|
+
if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'
|
|
26
|
+
return 'allowed'
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
export function resolveWidgetAssignmentReadScope(params: {
|
|
12
30
|
auth: AuthScopeInput
|
|
13
31
|
isSuperAdmin: boolean
|
|
@@ -4,22 +4,13 @@ import {
|
|
|
4
4
|
dictionaryEntryCommandCreateSchema,
|
|
5
5
|
dictionaryEntryCommandUpdateSchema,
|
|
6
6
|
} from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
ensureDictionaryEntryScope,
|
|
9
|
+
registerDictionaryEntryCommands,
|
|
10
|
+
} from '@open-mercato/core/modules/dictionaries/commands/factory'
|
|
9
11
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
12
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
13
|
|
|
12
|
-
function ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {
|
|
13
|
-
const tenantId = ctx.auth?.tenantId ?? null
|
|
14
|
-
if (tenantId && tenantId !== scope.tenantId) {
|
|
15
|
-
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
16
|
-
}
|
|
17
|
-
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
18
|
-
if (organizationId && organizationId !== scope.organizationId) {
|
|
19
|
-
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
async function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {
|
|
24
15
|
const dictionary = await em.findOne(Dictionary, {
|
|
25
16
|
id,
|
|
@@ -37,12 +28,12 @@ registerDictionaryEntryCommands({
|
|
|
37
28
|
translationKeyPrefix: 'dictionaries.entries',
|
|
38
29
|
createSchema: dictionaryEntryCommandCreateSchema,
|
|
39
30
|
updateSchema: dictionaryEntryCommandUpdateSchema,
|
|
40
|
-
ensureScope,
|
|
31
|
+
ensureScope: ensureDictionaryEntryScope,
|
|
41
32
|
duplicateError: 'An entry with this value already exists.',
|
|
42
33
|
resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
|
|
43
34
|
const dictionary = await ensureDictionary(em, parsed.dictionaryId)
|
|
44
35
|
const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
|
|
45
|
-
|
|
36
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
46
37
|
return { dictionary, scope }
|
|
47
38
|
},
|
|
48
39
|
resolveEntry: async ({ em, ctx, id }) => {
|
|
@@ -54,7 +45,7 @@ registerDictionaryEntryCommands({
|
|
|
54
45
|
throw new CrudHttpError(404, { error: 'Dictionary not found' })
|
|
55
46
|
}
|
|
56
47
|
const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }
|
|
57
|
-
|
|
48
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
58
49
|
return { entry, dictionary: entry.dictionary, scope }
|
|
59
50
|
},
|
|
60
51
|
ensureDictionaryForUndo: async ({ em, snapshot }) => {
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type ReorderDictionaryEntriesCommandInput,
|
|
7
7
|
type SetDefaultDictionaryEntryCommandInput,
|
|
8
8
|
} from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
9
|
-
import {
|
|
9
|
+
import { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'
|
|
10
|
+
import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
|
|
10
11
|
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
11
12
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
12
13
|
import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
@@ -36,17 +37,6 @@ const dictionaryCrudEvents: CrudEventsConfig = {
|
|
|
36
37
|
}),
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
|
|
40
|
-
const tenantId = ctx.auth?.tenantId ?? null
|
|
41
|
-
if (tenantId && tenantId !== scope.tenantId) {
|
|
42
|
-
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
43
|
-
}
|
|
44
|
-
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
45
|
-
if (organizationId && organizationId !== scope.organizationId) {
|
|
46
|
-
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
async function requireDictionary(
|
|
51
41
|
em: EntityManager,
|
|
52
42
|
id: string,
|
|
@@ -84,7 +74,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
|
|
|
84
74
|
async prepare(rawInput, ctx) {
|
|
85
75
|
const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
|
|
86
76
|
const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
87
|
-
|
|
77
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
88
78
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
89
79
|
const ids = parsed.entries.map((e) => e.id)
|
|
90
80
|
const existing = await findWithDecryption(
|
|
@@ -108,7 +98,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
|
|
|
108
98
|
async execute(rawInput, ctx) {
|
|
109
99
|
const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
|
|
110
100
|
const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
111
|
-
|
|
101
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
112
102
|
|
|
113
103
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
114
104
|
const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
|
|
@@ -201,7 +191,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
|
|
|
201
191
|
const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)
|
|
202
192
|
const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null
|
|
203
193
|
if (!before) return
|
|
204
|
-
|
|
194
|
+
ensureDictionaryEntryScope(ctx, before.scope)
|
|
205
195
|
|
|
206
196
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
207
197
|
const ids = before.positions.map((p) => p.id)
|
|
@@ -265,7 +255,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
|
|
|
265
255
|
async prepare(rawInput, ctx) {
|
|
266
256
|
const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
|
|
267
257
|
const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
268
|
-
|
|
258
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
269
259
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
270
260
|
const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
|
|
271
261
|
const previousDefaults = await findWithDecryption(
|
|
@@ -291,7 +281,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
|
|
|
291
281
|
async execute(rawInput, ctx) {
|
|
292
282
|
const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
|
|
293
283
|
const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
|
|
294
|
-
|
|
284
|
+
ensureDictionaryEntryScope(ctx, scope)
|
|
295
285
|
|
|
296
286
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
297
287
|
const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
|
|
@@ -399,7 +389,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
|
|
|
399
389
|
const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)
|
|
400
390
|
const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null
|
|
401
391
|
if (!before) return
|
|
402
|
-
|
|
392
|
+
ensureDictionaryEntryScope(ctx, before.scope)
|
|
403
393
|
|
|
404
394
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
405
395
|
const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@open-mercato/shared/lib/commands'
|
|
13
13
|
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
14
14
|
import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
|
|
15
|
+
import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
15
16
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
16
17
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
17
18
|
import type { z } from 'zod'
|
|
@@ -22,6 +23,11 @@ type DictionaryScope = {
|
|
|
22
23
|
organizationId: string
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
|
|
27
|
+
ensureTenantScope(ctx, scope.tenantId)
|
|
28
|
+
ensureOrganizationScope(ctx, scope.organizationId)
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export type DictionaryEntrySnapshot = {
|
|
26
32
|
id: string
|
|
27
33
|
dictionaryId: string
|
|
@@ -87,8 +93,8 @@ export type DictionaryEntryCommandConfig<TCreate, TUpdate> = {
|
|
|
87
93
|
labels?: DictionaryEntryCommandLabels
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
|
|
91
|
-
return typeof fn === 'function' ? fn :
|
|
96
|
+
export function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
|
|
97
|
+
return typeof fn === 'function' ? fn : ensureDictionaryEntryScope
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
async function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {
|
|
@@ -90,9 +90,12 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
|
|
|
90
90
|
const [error, setError] = React.useState<string | null>(null)
|
|
91
91
|
const multiId = React.useId()
|
|
92
92
|
const inlineCreateId = React.useId()
|
|
93
|
+
const inlineCreateHintId = React.useId()
|
|
94
|
+
const defaultValueHintId = React.useId()
|
|
93
95
|
const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''
|
|
94
96
|
const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false
|
|
95
97
|
const isMulti = def?.configJson?.multi === true
|
|
98
|
+
const inlineCreateDisabled = !selectedId || isMulti
|
|
96
99
|
const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')
|
|
97
100
|
|
|
98
101
|
React.useEffect(() => {
|
|
@@ -181,45 +184,65 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
|
|
|
181
184
|
</a>
|
|
182
185
|
</div>
|
|
183
186
|
) : null}
|
|
184
|
-
<div className="
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
187
|
+
<div className="space-y-2">
|
|
188
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4">
|
|
189
|
+
<div className="inline-flex items-center gap-2 text-xs">
|
|
190
|
+
<Checkbox
|
|
191
|
+
id={multiId}
|
|
192
|
+
checked={isMulti}
|
|
193
|
+
onCheckedChange={(checked) => {
|
|
194
|
+
const enabled = checked === true
|
|
195
|
+
onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
|
|
196
|
+
}}
|
|
197
|
+
disabled={!selectedId}
|
|
198
|
+
/>
|
|
199
|
+
<label
|
|
200
|
+
htmlFor={multiId}
|
|
201
|
+
className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
|
|
202
|
+
>
|
|
203
|
+
{t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
|
|
204
|
+
</label>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="inline-flex items-center gap-2 text-xs">
|
|
207
|
+
<Checkbox
|
|
208
|
+
id={inlineCreateId}
|
|
209
|
+
checked={!isMulti && inlineCreate}
|
|
210
|
+
onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
|
|
211
|
+
disabled={inlineCreateDisabled}
|
|
212
|
+
aria-describedby={isMulti ? inlineCreateHintId : undefined}
|
|
213
|
+
/>
|
|
214
|
+
<label
|
|
215
|
+
htmlFor={inlineCreateId}
|
|
216
|
+
className={!inlineCreateDisabled ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
|
|
217
|
+
>
|
|
218
|
+
{t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
|
|
219
|
+
</label>
|
|
220
|
+
</div>
|
|
215
221
|
</div>
|
|
216
|
-
|
|
222
|
+
{isMulti ? (
|
|
223
|
+
<p id={inlineCreateHintId} className="text-xs text-muted-foreground">
|
|
224
|
+
{t(
|
|
225
|
+
'dictionaries.customFields.inlineCreateSingleOnly',
|
|
226
|
+
'Inline creation is available for single-entry dictionary fields only.',
|
|
227
|
+
)}
|
|
228
|
+
</p>
|
|
229
|
+
) : null}
|
|
230
|
+
</div>
|
|
217
231
|
{selectedId && !isMulti ? (
|
|
218
232
|
<DictionaryDefaultSelector
|
|
219
233
|
dictionaryId={selectedId}
|
|
220
234
|
defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}
|
|
221
235
|
onChange={(value) => onChange({ defaultValue: value || undefined })}
|
|
222
236
|
/>
|
|
237
|
+
) : selectedId && isMulti ? (
|
|
238
|
+
<div className="inline-flex items-center gap-2 text-xs">
|
|
239
|
+
<p id={defaultValueHintId} className="text-xs text-muted-foreground">
|
|
240
|
+
{t(
|
|
241
|
+
'dictionaries.customFields.defaultValueMultiUnavailable',
|
|
242
|
+
'Default values are not available for multi-select dictionary fields.',
|
|
243
|
+
)}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
223
246
|
) : null}
|
|
224
247
|
</div>
|
|
225
248
|
)
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"dictionaries.customFields.allowInlineCreate": "Inline-Erstellung in Formularen erlauben",
|
|
83
83
|
"dictionaries.customFields.allowMultiple": "Mehrere Einträge auswählen erlauben",
|
|
84
84
|
"dictionaries.customFields.defaultValue": "Standardwert",
|
|
85
|
+
"dictionaries.customFields.defaultValueMultiUnavailable": "Standardwerte sind für Wörterbuchfelder mit Mehrfachauswahl nicht verfügbar.",
|
|
85
86
|
"dictionaries.customFields.defaultValueNone": "Kein Standardwert",
|
|
86
87
|
"dictionaries.customFields.defaultValueStale": "Standardeintrag nicht gefunden — er wurde möglicherweise gelöscht oder umbenannt.",
|
|
87
88
|
"dictionaries.customFields.dictionaryLabel": "Quelle des Wörterbuchs",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"dictionaries.customFields.empty": "Noch keine Wörterbücher verfügbar. Erstellen Sie zuerst eines.",
|
|
90
91
|
"dictionaries.customFields.errorLoad": "Wörterbücher konnten nicht geladen werden.",
|
|
91
92
|
"dictionaries.customFields.inactive": "inaktiv",
|
|
93
|
+
"dictionaries.customFields.inlineCreateSingleOnly": "Inline-Erstellung ist nur für Wörterbuchfelder mit Einzelauswahl verfügbar.",
|
|
92
94
|
"dictionaries.customFields.loading": "Wörterbücher werden geladen…",
|
|
93
95
|
"dictionaries.customFields.manageLink": "Wörterbücher verwalten",
|
|
94
96
|
"dictionaries.customFields.selectedHint": "Einträge aus diesem Wörterbuch füllen das Feld.",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"dictionaries.customFields.allowInlineCreate": "Allow inline creation inside forms",
|
|
83
83
|
"dictionaries.customFields.allowMultiple": "Allow selecting multiple entries",
|
|
84
84
|
"dictionaries.customFields.defaultValue": "Default value",
|
|
85
|
+
"dictionaries.customFields.defaultValueMultiUnavailable": "Default values are not available for multi-select dictionary fields.",
|
|
85
86
|
"dictionaries.customFields.defaultValueNone": "No default",
|
|
86
87
|
"dictionaries.customFields.defaultValueStale": "Default entry not found — it may have been deleted or renamed.",
|
|
87
88
|
"dictionaries.customFields.dictionaryLabel": "Dictionary source",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"dictionaries.customFields.empty": "No dictionaries available yet. Create one first.",
|
|
90
91
|
"dictionaries.customFields.errorLoad": "Failed to load dictionaries.",
|
|
91
92
|
"dictionaries.customFields.inactive": "inactive",
|
|
93
|
+
"dictionaries.customFields.inlineCreateSingleOnly": "Inline creation is available for single-entry dictionary fields only.",
|
|
92
94
|
"dictionaries.customFields.loading": "Loading dictionaries…",
|
|
93
95
|
"dictionaries.customFields.manageLink": "Manage dictionaries",
|
|
94
96
|
"dictionaries.customFields.selectedHint": "Entries from this dictionary populate the field.",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"dictionaries.customFields.allowInlineCreate": "Permitir creación en línea dentro de los formularios",
|
|
83
83
|
"dictionaries.customFields.allowMultiple": "Permitir seleccionar varias entradas",
|
|
84
84
|
"dictionaries.customFields.defaultValue": "Valor predeterminado",
|
|
85
|
+
"dictionaries.customFields.defaultValueMultiUnavailable": "Los valores predeterminados no están disponibles para campos de diccionario con selección múltiple.",
|
|
85
86
|
"dictionaries.customFields.defaultValueNone": "Sin valor predeterminado",
|
|
86
87
|
"dictionaries.customFields.defaultValueStale": "No se encontró la entrada predeterminada; puede haber sido eliminada o renombrada.",
|
|
87
88
|
"dictionaries.customFields.dictionaryLabel": "Fuente del diccionario",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"dictionaries.customFields.empty": "Aún no hay diccionarios disponibles. Crea uno primero.",
|
|
90
91
|
"dictionaries.customFields.errorLoad": "No se pudieron cargar los diccionarios.",
|
|
91
92
|
"dictionaries.customFields.inactive": "inactivo",
|
|
93
|
+
"dictionaries.customFields.inlineCreateSingleOnly": "La creación en línea solo está disponible para campos de diccionario de una sola entrada.",
|
|
92
94
|
"dictionaries.customFields.loading": "Cargando diccionarios…",
|
|
93
95
|
"dictionaries.customFields.manageLink": "Administrar diccionarios",
|
|
94
96
|
"dictionaries.customFields.selectedHint": "Las entradas de este diccionario rellenarán el campo.",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"dictionaries.customFields.allowInlineCreate": "Zezwól na tworzenie w formularzach",
|
|
83
83
|
"dictionaries.customFields.allowMultiple": "Zezwól na wybór wielu wpisów",
|
|
84
84
|
"dictionaries.customFields.defaultValue": "Wartość domyślna",
|
|
85
|
+
"dictionaries.customFields.defaultValueMultiUnavailable": "Wartości domyślne nie są dostępne dla wielokrotnego wyboru wpisów słownika.",
|
|
85
86
|
"dictionaries.customFields.defaultValueNone": "Brak wartości domyślnej",
|
|
86
87
|
"dictionaries.customFields.defaultValueStale": "Nie znaleziono domyślnego wpisu — mógł zostać usunięty albo przemianowany.",
|
|
87
88
|
"dictionaries.customFields.dictionaryLabel": "Źródło słownika",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"dictionaries.customFields.empty": "Brak dostępnych słowników. Najpierw utwórz jeden.",
|
|
90
91
|
"dictionaries.customFields.errorLoad": "Nie udało się wczytać słowników.",
|
|
91
92
|
"dictionaries.customFields.inactive": "nieaktywne",
|
|
93
|
+
"dictionaries.customFields.inlineCreateSingleOnly": "Tworzenie wpisów w formularzu jest dostępne tylko dla pól z pojedynczym wyborem.",
|
|
92
94
|
"dictionaries.customFields.loading": "Ładowanie słowników…",
|
|
93
95
|
"dictionaries.customFields.manageLink": "Zarządzaj słownikami",
|
|
94
96
|
"dictionaries.customFields.selectedHint": "Wpisy z tego słownika uzupełnią pole.",
|
|
@@ -18,6 +18,13 @@ export type OrganizationScope = {
|
|
|
18
18
|
filterIds: string[] | null
|
|
19
19
|
allowedIds: string[] | null
|
|
20
20
|
tenantId: string | null
|
|
21
|
+
// True when the caller explicitly selected a concrete organization (cookie /
|
|
22
|
+
// selectedId param) that could not be honored — it does not exist for the
|
|
23
|
+
// tenant or is not accessible. Reads degrade gracefully (filterIds/selectedId
|
|
24
|
+
// fall back to the caller's accessible orgs), but writes MUST fail loudly on
|
|
25
|
+
// this so a record never lands under an org the caller did not intend. Absent
|
|
26
|
+
// (undefined) means the selection — if any — was honored.
|
|
27
|
+
selectionRejected?: boolean
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
// Phase 4 — short-TTL cache for resolveOrganizationScopeForRequest.
|
|
@@ -79,7 +86,8 @@ function isValidCachedScope(value: unknown): value is OrganizationScope {
|
|
|
79
86
|
const record = value as Partial<OrganizationScope>
|
|
80
87
|
const idOk = (v: unknown) => v === null || typeof v === 'string'
|
|
81
88
|
const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))
|
|
82
|
-
|
|
89
|
+
const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'
|
|
90
|
+
return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {
|
|
@@ -320,9 +328,18 @@ export async function resolveOrganizationScope({
|
|
|
320
328
|
const initialSelected =
|
|
321
329
|
normalizedSelectedId
|
|
322
330
|
?? (widenToAllOrgs ? null : accountOrgId ?? null)
|
|
331
|
+
// A selection is only honored when it resolves to a real, non-deleted org for
|
|
332
|
+
// this tenant. `orgDescendants` holds exactly the existing orgs among the
|
|
333
|
+
// candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org
|
|
334
|
+
// that no longer resolves after a DB reset) has no entry and is dropped here.
|
|
335
|
+
// Without the existence guard an unrestricted (all-orgs) principal — whose
|
|
336
|
+
// `allowedSet` is null — would accept the dead id as `effectiveSelected`, and
|
|
337
|
+
// writes derived from `selectedId` would land under an org the read scope
|
|
338
|
+
// (`filterIds`) never filters to, silently orphaning the record.
|
|
339
|
+
const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)
|
|
323
340
|
let effectiveSelected: string | null = null
|
|
324
341
|
if (initialSelected) {
|
|
325
|
-
if (allowedSet === null || allowedSet.has(initialSelected)) {
|
|
342
|
+
if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {
|
|
326
343
|
effectiveSelected = initialSelected
|
|
327
344
|
}
|
|
328
345
|
}
|
|
@@ -336,6 +353,13 @@ export async function resolveOrganizationScope({
|
|
|
336
353
|
filterSet = null
|
|
337
354
|
} else if (auth.orgId) {
|
|
338
355
|
filterSet = loadFallbackSet()
|
|
356
|
+
// Keep the write target (`selectedId`) aligned with the read scope
|
|
357
|
+
// (`filterIds`): when an unrestricted principal's requested selection was
|
|
358
|
+
// dropped above, fall the selection back to the account org too so a record
|
|
359
|
+
// created here is always readable back by the same caller.
|
|
360
|
+
if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {
|
|
361
|
+
effectiveSelected = fallbackOrgId
|
|
362
|
+
}
|
|
339
363
|
}
|
|
340
364
|
|
|
341
365
|
if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
|
|
@@ -348,11 +372,21 @@ export async function resolveOrganizationScope({
|
|
|
348
372
|
}
|
|
349
373
|
}
|
|
350
374
|
|
|
375
|
+
// A concrete organization was explicitly requested (`normalizedSelectedId` is
|
|
376
|
+
// null for both "no selection" and the "all organizations" token) but the
|
|
377
|
+
// resolver could not honor it — it was dropped as non-existent/inaccessible
|
|
378
|
+
// and the effective selection fell back to something else. Surface this so the
|
|
379
|
+
// write layer can reject the request instead of silently creating the record
|
|
380
|
+
// under the fallback org. Only set the field when true to keep the scope shape
|
|
381
|
+
// unchanged for the common (honored) case.
|
|
382
|
+
const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId
|
|
383
|
+
|
|
351
384
|
return {
|
|
352
385
|
selectedId: effectiveSelected,
|
|
353
386
|
filterIds: filterSet ? Array.from(filterSet) : null,
|
|
354
387
|
allowedIds: allowedSet ? Array.from(allowedSet) : null,
|
|
355
388
|
tenantId,
|
|
389
|
+
...(selectionRejected ? { selectionRejected: true } : {}),
|
|
356
390
|
}
|
|
357
391
|
}
|
|
358
392
|
|
|
@@ -8,6 +8,7 @@ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacS
|
|
|
8
8
|
import { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
9
9
|
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
10
10
|
import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
11
|
+
import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
|
|
11
12
|
import { setRecordCustomFields } from '../lib/helpers'
|
|
12
13
|
import { CustomFieldValue } from '../data/entities'
|
|
13
14
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
@@ -151,10 +152,7 @@ export async function GET(req: Request) {
|
|
|
151
152
|
const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
|
|
152
153
|
const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)
|
|
153
154
|
const searchTerm = (url.searchParams.get('search') || '').trim()
|
|
154
|
-
const searchFields = (url.searchParams.get('searchFields')
|
|
155
|
-
.split(',')
|
|
156
|
-
.map((field) => field.trim())
|
|
157
|
-
.filter(Boolean)
|
|
155
|
+
const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))
|
|
158
156
|
|
|
159
157
|
const qpEntries: Array<[string, string]> = []
|
|
160
158
|
for (const [key, val] of url.searchParams.entries()) {
|
|
@@ -211,11 +209,11 @@ export async function GET(req: Request) {
|
|
|
211
209
|
if (key.startsWith('cf_')) {
|
|
212
210
|
if (key.endsWith('In')) {
|
|
213
211
|
const base = key.slice(0, -2)
|
|
214
|
-
const values = val
|
|
212
|
+
const values = parseCommaSeparatedList(val)
|
|
215
213
|
;(filtersObj as any)[base] = { $in: values }
|
|
216
214
|
} else {
|
|
217
215
|
if (val.includes(',')) {
|
|
218
|
-
const values = val
|
|
216
|
+
const values = parseCommaSeparatedList(val)
|
|
219
217
|
;(filtersObj as any)[key] = { $in: values }
|
|
220
218
|
} else {
|
|
221
219
|
const parsed = parseBooleanToken(val)
|
|
@@ -224,7 +222,7 @@ export async function GET(req: Request) {
|
|
|
224
222
|
}
|
|
225
223
|
} else if (allowAnyKey) {
|
|
226
224
|
if (val.includes(',')) {
|
|
227
|
-
const values = val
|
|
225
|
+
const values = parseCommaSeparatedList(val)
|
|
228
226
|
;(filtersObj as any)[key] = { $in: values }
|
|
229
227
|
} else {
|
|
230
228
|
const parsed = parseBooleanToken(val)
|