@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
|
@@ -4,6 +4,7 @@ import React from 'react'
|
|
|
4
4
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
5
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
7
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
9
|
import type { ActionLogItem } from '../../components/AuditLogsActions'
|
|
9
10
|
import { AuditLogsActions } from '../../components/AuditLogsActions'
|
|
@@ -182,28 +183,17 @@ export default function AuditLogsPage() {
|
|
|
182
183
|
return (
|
|
183
184
|
<Page>
|
|
184
185
|
<PageBody>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<button
|
|
197
|
-
type="button"
|
|
198
|
-
role="tab"
|
|
199
|
-
aria-selected={tab === 'access'}
|
|
200
|
-
className={`relative -mb-px border-b-2 px-0 pb-3 pt-2 font-medium transition-colors ${tab === 'access' ? 'border-accent-indigo text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}`}
|
|
201
|
-
onClick={() => setTab('access')}
|
|
202
|
-
>
|
|
203
|
-
{t('audit_logs.access.title')}
|
|
204
|
-
</button>
|
|
205
|
-
</nav>
|
|
206
|
-
</div>
|
|
186
|
+
<Tabs
|
|
187
|
+
value={tab}
|
|
188
|
+
onValueChange={(value) => setTab(value as TabOption)}
|
|
189
|
+
variant="underline"
|
|
190
|
+
className="mb-6"
|
|
191
|
+
>
|
|
192
|
+
<TabsList className="w-full flex-wrap" aria-label={t('audit_logs.tabs.label')}>
|
|
193
|
+
<TabsTrigger value="actions">{t('audit_logs.actions.title')}</TabsTrigger>
|
|
194
|
+
<TabsTrigger value="access">{t('audit_logs.access.title')}</TabsTrigger>
|
|
195
|
+
</TabsList>
|
|
196
|
+
</Tabs>
|
|
207
197
|
|
|
208
198
|
{error && <div className="mb-4 rounded-md border border-status-error-border bg-status-error-bg p-3 text-sm text-status-error-text">{error}</div>}
|
|
209
199
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
2
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { AuthService } from '@open-mercato/core/modules/auth/services/authService'
|
|
5
|
+
import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
6
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
|
+
import { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'
|
|
8
|
+
import { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'
|
|
9
|
+
import { resolveAutoLoginCredentials } from '@open-mercato/core/modules/auth/lib/autologin'
|
|
10
|
+
import { emitAuthEvent } from '@open-mercato/core/modules/auth/events'
|
|
11
|
+
|
|
12
|
+
export const metadata = { requireAuth: false }
|
|
13
|
+
|
|
14
|
+
const accessTokenMaxAgeSeconds = 60 * 60 * 8
|
|
15
|
+
const logger = createLogger('auth').child({ component: 'autologin' })
|
|
16
|
+
|
|
17
|
+
// Demo-only convenience: when OM_AUTOLOGIN_* credentials are configured, sign the
|
|
18
|
+
// visitor in with those credentials and redirect into the app. Gated entirely
|
|
19
|
+
// behind env vars — the homepage only routes here when they are set. On any
|
|
20
|
+
// failure (feature off, bad credentials) it falls back to the manual login form
|
|
21
|
+
// so a misconfigured demo can never loop.
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const url = new URL(req.url)
|
|
24
|
+
const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin
|
|
25
|
+
const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/backend')
|
|
26
|
+
|
|
27
|
+
const credentials = resolveAutoLoginCredentials()
|
|
28
|
+
if (!credentials) {
|
|
29
|
+
return buildSafeRedirectResponse(req, '/login')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Already signed in — go straight to the app instead of re-issuing a session.
|
|
33
|
+
const existing = await getAuthFromRequest(req)
|
|
34
|
+
if (existing) {
|
|
35
|
+
return buildSafeRedirectResponse(req, redirectTo)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const container = await createRequestContainer()
|
|
39
|
+
const auth = container.resolve<AuthService>('authService')
|
|
40
|
+
|
|
41
|
+
let user = null
|
|
42
|
+
if (credentials.tenantId) {
|
|
43
|
+
user = await auth.findUserByEmailAndTenant(credentials.email, credentials.tenantId)
|
|
44
|
+
} else {
|
|
45
|
+
const users = await auth.findUsersByEmail(credentials.email)
|
|
46
|
+
user = users.length === 1 ? users[0] : null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ok = await auth.verifyPassword(user, credentials.password)
|
|
50
|
+
if (!user || !ok) {
|
|
51
|
+
// Misconfigured demo credentials — never loop; drop to the manual form.
|
|
52
|
+
logger.warn('OM_AUTOLOGIN credentials did not resolve to a single valid user; falling back to login')
|
|
53
|
+
return buildSafeRedirectResponse(req, '/login')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolvedTenantId = credentials.tenantId ?? (user.tenantId ? String(user.tenantId) : null)
|
|
57
|
+
const roles = await auth.getUserRoles(user, resolvedTenantId)
|
|
58
|
+
await auth.updateLastLoginAt(user)
|
|
59
|
+
const expiresAt = new Date(Date.now() + accessTokenMaxAgeSeconds * 1000)
|
|
60
|
+
const { session } = await auth.createSession(user, expiresAt)
|
|
61
|
+
const token = signJwt({
|
|
62
|
+
sub: String(user.id),
|
|
63
|
+
sid: String(session.id),
|
|
64
|
+
tenantId: resolvedTenantId,
|
|
65
|
+
orgId: user.organizationId ? String(user.organizationId) : null,
|
|
66
|
+
email: user.email,
|
|
67
|
+
roles,
|
|
68
|
+
})
|
|
69
|
+
void emitAuthEvent('auth.login.success', {
|
|
70
|
+
id: String(user.id),
|
|
71
|
+
email: user.email,
|
|
72
|
+
tenantId: resolvedTenantId,
|
|
73
|
+
organizationId: user.organizationId ? String(user.organizationId) : null,
|
|
74
|
+
}).catch(() => undefined)
|
|
75
|
+
|
|
76
|
+
const res = buildSafeRedirectResponse(req, redirectTo)
|
|
77
|
+
res.cookies.set('auth_token', token, {
|
|
78
|
+
httpOnly: true,
|
|
79
|
+
path: '/',
|
|
80
|
+
sameSite: 'lax',
|
|
81
|
+
secure: process.env.NODE_ENV === 'production',
|
|
82
|
+
maxAge: accessTokenMaxAgeSeconds,
|
|
83
|
+
})
|
|
84
|
+
return res
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const openApi: OpenApiRouteDoc = {
|
|
88
|
+
tag: 'Authentication & Accounts',
|
|
89
|
+
summary: 'Demo autologin',
|
|
90
|
+
methods: {
|
|
91
|
+
GET: {
|
|
92
|
+
summary: 'Auto sign-in using env-configured demo credentials',
|
|
93
|
+
description:
|
|
94
|
+
'When OM_AUTOLOGIN_EMAIL / OM_AUTOLOGIN_PASSWORD are configured, signs the visitor in with those credentials and redirects into the app. Intended for single-tenant demo instances only. Falls back to the login page when disabled or misconfigured.',
|
|
95
|
+
responses: [
|
|
96
|
+
{ status: 307, description: 'Redirect into the app (or to /login on failure)', mediaType: 'text/html' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
23
23
|
import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
24
24
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
25
|
+
import { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'
|
|
25
26
|
import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
|
|
26
27
|
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
27
28
|
import { sql } from 'kysely'
|
|
@@ -41,6 +42,7 @@ const querySchema = z.object({
|
|
|
41
42
|
search: z.string().optional(),
|
|
42
43
|
name: z.string().optional(),
|
|
43
44
|
organizationId: z.string().uuid().optional(),
|
|
45
|
+
scopeToActiveOrganization: z.boolean().optional(),
|
|
44
46
|
roleIds: z.array(z.string().uuid()).optional(),
|
|
45
47
|
}).passthrough()
|
|
46
48
|
|
|
@@ -180,6 +182,7 @@ export async function GET(req: Request) {
|
|
|
180
182
|
search: url.searchParams.get('search') || undefined,
|
|
181
183
|
name: url.searchParams.get('name') || undefined,
|
|
182
184
|
organizationId: url.searchParams.get('organizationId') || undefined,
|
|
185
|
+
scopeToActiveOrganization: parseBooleanFlag(url.searchParams.get('scopeToActiveOrganization') || undefined),
|
|
183
186
|
roleIds: rawRoleIds.length ? rawRoleIds : undefined,
|
|
184
187
|
})
|
|
185
188
|
if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })
|
|
@@ -195,7 +198,7 @@ export async function GET(req: Request) {
|
|
|
195
198
|
} catch (err) {
|
|
196
199
|
logger.error('Failed to resolve rbac', { err })
|
|
197
200
|
}
|
|
198
|
-
const { id, page, pageSize, search, name, organizationId, roleIds } = parsed.data
|
|
201
|
+
const { id, page, pageSize, search, name, organizationId, scopeToActiveOrganization, roleIds } = parsed.data
|
|
199
202
|
const filters: any[] = [{ deletedAt: null }]
|
|
200
203
|
const actorTenantId = auth.tenantId ? String(auth.tenantId) : null
|
|
201
204
|
let effectiveTenantId: string | null = null
|
|
@@ -244,6 +247,12 @@ export async function GET(req: Request) {
|
|
|
244
247
|
? effectiveSelectedOrganizationId
|
|
245
248
|
: auth.orgId ?? null
|
|
246
249
|
if (organizationId) filters.push({ organizationId })
|
|
250
|
+
// Recipient/assignee pickers scope to the caller's active organization so they never
|
|
251
|
+
// suggest users outside it. A message composed here is stamped with the caller's
|
|
252
|
+
// active org (auth.orgId), and the message detail endpoint enforces
|
|
253
|
+
// hasOrganizationAccess(scope.organizationId, message.organizationId); scoping the
|
|
254
|
+
// suggestions to the same org keeps a picked recipient able to open what they were sent.
|
|
255
|
+
if (scopeToActiveOrganization) filters.push({ organizationId: auth.orgId ?? null })
|
|
247
256
|
const trimmedName = typeof name === 'string' ? name.trim() : ''
|
|
248
257
|
if (trimmedName) {
|
|
249
258
|
const searchPattern = `%${escapeLikePattern(trimmedName)}%`
|
|
@@ -620,7 +629,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
620
629
|
GET: {
|
|
621
630
|
summary: 'List users',
|
|
622
631
|
description:
|
|
623
|
-
'Returns users for the effective selected tenant and organization scope. Search matches email, organization name, and role name. Super administrators may scope the response via the topbar context, organization filters, or role filters.',
|
|
632
|
+
'Returns users for the effective selected tenant and organization scope. Search matches email, organization name, and role name. Super administrators may scope the response via the topbar context, organization filters, or role filters. Pass scopeToActiveOrganization=1 to restrict results to the caller\'s active organization (used by recipient/assignee pickers so suggestions stay within the org that owns the resulting record).',
|
|
624
633
|
query: querySchema,
|
|
625
634
|
responses: [
|
|
626
635
|
{ status: 200, description: 'User collection', schema: userListResponseSchema },
|
package/src/modules/auth/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Tenant, Organization } from '@open-mercato/core/modules/directory/data/
|
|
|
9
9
|
import { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'
|
|
10
10
|
import { ensureRoles, setupInitialTenant, ensureDefaultRoleAcls, ensureCustomRoleAcls, OrgSlugExistsError, DerivedUserPasswordRequiredError } from './lib/setup-app'
|
|
11
11
|
import { normalizeTenantId } from './lib/tenantAccess'
|
|
12
|
+
import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
|
|
12
13
|
import { computeEmailHash, emailHashLookupValues } from './lib/emailHash'
|
|
13
14
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
14
15
|
import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
|
|
@@ -71,7 +72,7 @@ const addUser: ModuleCli = {
|
|
|
71
72
|
})
|
|
72
73
|
await em.persist(u).flush()
|
|
73
74
|
if (rolesCsv) {
|
|
74
|
-
const names = rolesCsv
|
|
75
|
+
const names = parseCommaSeparatedList(rolesCsv)
|
|
75
76
|
for (const name of names) {
|
|
76
77
|
const role = await resolveTenantScopedRole(em, name, normalizedTenantId)
|
|
77
78
|
const link = em.create(UserRole, { user: u, role })
|
|
@@ -493,7 +494,7 @@ const setupApp: ModuleCli = {
|
|
|
493
494
|
const container = await createRequestContainer()
|
|
494
495
|
const em = container.resolve<EntityManager>('em')
|
|
495
496
|
const roleNames = rolesCsv
|
|
496
|
-
? rolesCsv
|
|
497
|
+
? parseCommaSeparatedList(rolesCsv)
|
|
497
498
|
: undefined
|
|
498
499
|
|
|
499
500
|
try {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AutoLoginCredentials = {
|
|
2
|
+
email: string
|
|
3
|
+
password: string
|
|
4
|
+
tenantId: string | null
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readEnvValue(env: Record<string, string | undefined>, key: string): string {
|
|
8
|
+
const value = env[key]
|
|
9
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads the optional demo-autologin credentials from env. When both
|
|
14
|
+
* `OM_AUTOLOGIN_EMAIL` and `OM_AUTOLOGIN_PASSWORD` are set, the homepage hands
|
|
15
|
+
* unauthenticated visitors to the autologin route which signs them in and drops
|
|
16
|
+
* them into the app. Unset (the default) → `null` → normal login flow.
|
|
17
|
+
*
|
|
18
|
+
* `OM_AUTOLOGIN_TENANT` is optional and only needed when the same email exists
|
|
19
|
+
* across multiple tenants. Pure: pass the env bag so it stays testable.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveAutoLoginCredentials(
|
|
22
|
+
env: Record<string, string | undefined> = process.env,
|
|
23
|
+
): AutoLoginCredentials | null {
|
|
24
|
+
const email = readEnvValue(env, 'OM_AUTOLOGIN_EMAIL')
|
|
25
|
+
const password = env.OM_AUTOLOGIN_PASSWORD ?? ''
|
|
26
|
+
if (!email || !password) return null
|
|
27
|
+
const tenantId = readEnvValue(env, 'OM_AUTOLOGIN_TENANT')
|
|
28
|
+
return { email, password, tenantId: tenantId || null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when demo autologin credentials are configured via env. */
|
|
32
|
+
export function isAutoLoginEnabled(
|
|
33
|
+
env: Record<string, string | undefined> = process.env,
|
|
34
|
+
): boolean {
|
|
35
|
+
return resolveAutoLoginCredentials(env) !== null
|
|
36
|
+
}
|
|
@@ -189,5 +189,9 @@
|
|
|
189
189
|
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup-Drosselfenster",
|
|
190
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatisch Query-Index-Neuindizierungsaufträge bei Lücken planen.",
|
|
191
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Automatische Neuindizierung",
|
|
192
|
-
"configs.systemStatus.viewDocs": "Dokumentation anzeigen"
|
|
192
|
+
"configs.systemStatus.viewDocs": "Dokumentation anzeigen",
|
|
193
|
+
"configs.upgrades.attachmentsOrgReconcile.cta": "Anhang-Organisationen abgleichen",
|
|
194
|
+
"configs.upgrades.attachmentsOrgReconcile.loading": "Anhang-Organisationen werden abgeglichen…",
|
|
195
|
+
"configs.upgrades.attachmentsOrgReconcile.message": "Version {{version}} behebt Anhänge, die unter der falschen Organisation gespeichert wurden. Führen Sie den Abgleich aus, um vorhandene Anhänge in die Organisation des zugehörigen Datensatzes zu verschieben.",
|
|
196
|
+
"configs.upgrades.attachmentsOrgReconcile.success": "Anhang-Organisationen abgeglichen."
|
|
193
197
|
}
|
|
@@ -189,5 +189,9 @@
|
|
|
189
189
|
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup throttle window",
|
|
190
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatically schedule query index reindex jobs when gaps appear.",
|
|
191
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Auto reindex",
|
|
192
|
-
"configs.systemStatus.viewDocs": "View documentation"
|
|
192
|
+
"configs.systemStatus.viewDocs": "View documentation",
|
|
193
|
+
"configs.upgrades.attachmentsOrgReconcile.cta": "Reconcile attachment organizations",
|
|
194
|
+
"configs.upgrades.attachmentsOrgReconcile.loading": "Reconciling attachment organizations…",
|
|
195
|
+
"configs.upgrades.attachmentsOrgReconcile.message": "Version {{version}} fixes attachments that were saved under the wrong organization. Run the reconciliation to move existing attachments to the organization of the record they belong to.",
|
|
196
|
+
"configs.upgrades.attachmentsOrgReconcile.success": "Attachment organizations reconciled."
|
|
193
197
|
}
|
|
@@ -189,5 +189,9 @@
|
|
|
189
189
|
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Ventana de limitación del calentamiento",
|
|
190
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Programar automáticamente trabajos de reindexación cuando se detecten brechas.",
|
|
191
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Reindexación automática",
|
|
192
|
-
"configs.systemStatus.viewDocs": "Ver documentación"
|
|
192
|
+
"configs.systemStatus.viewDocs": "Ver documentación",
|
|
193
|
+
"configs.upgrades.attachmentsOrgReconcile.cta": "Reconciliar organizaciones de adjuntos",
|
|
194
|
+
"configs.upgrades.attachmentsOrgReconcile.loading": "Reconciliando organizaciones de adjuntos…",
|
|
195
|
+
"configs.upgrades.attachmentsOrgReconcile.message": "La versión {{version}} corrige los adjuntos guardados en la organización incorrecta. Ejecuta la reconciliación para mover los adjuntos existentes a la organización del registro al que pertenecen.",
|
|
196
|
+
"configs.upgrades.attachmentsOrgReconcile.success": "Organizaciones de adjuntos reconciliadas."
|
|
193
197
|
}
|
|
@@ -189,5 +189,9 @@
|
|
|
189
189
|
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Okno throttlingu rozgrzewki",
|
|
190
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatycznie uruchamia przebudowę indeksu zapytań, gdy wykryte zostaną luki.",
|
|
191
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Automatyczna przebudowa indeksu",
|
|
192
|
-
"configs.systemStatus.viewDocs": "Zobacz dokumentację"
|
|
192
|
+
"configs.systemStatus.viewDocs": "Zobacz dokumentację",
|
|
193
|
+
"configs.upgrades.attachmentsOrgReconcile.cta": "Uzgodnij organizacje załączników",
|
|
194
|
+
"configs.upgrades.attachmentsOrgReconcile.loading": "Uzgadnianie organizacji załączników…",
|
|
195
|
+
"configs.upgrades.attachmentsOrgReconcile.message": "Wersja {{version}} naprawia załączniki zapisane pod niewłaściwą organizacją. Uruchom uzgadnianie, aby przenieść istniejące załączniki do organizacji rekordu, do którego należą.",
|
|
196
|
+
"configs.upgrades.attachmentsOrgReconcile.success": "Organizacje załączników zostały uzgodnione."
|
|
193
197
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import * as semver from 'semver'
|
|
3
3
|
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
6
|
+
import { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('configs').child({ component: 'upgrade-actions' })
|
|
4
9
|
|
|
5
10
|
export type UpgradeActionContext = {
|
|
6
11
|
tenantId: string
|
|
@@ -38,6 +43,25 @@ export function compareVersions(a: string, b: string): number {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export const upgradeActions: UpgradeActionDefinition[] = [
|
|
46
|
+
{
|
|
47
|
+
id: 'attachments.reconcile-organization',
|
|
48
|
+
version: '0.6.6',
|
|
49
|
+
messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',
|
|
50
|
+
ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',
|
|
51
|
+
successKey: 'configs.upgrades.attachmentsOrgReconcile.success',
|
|
52
|
+
loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',
|
|
53
|
+
async run({ container, em, tenantId }) {
|
|
54
|
+
const queryEngine = container.resolve('queryEngine') as QueryEngine
|
|
55
|
+
const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })
|
|
56
|
+
logger.info('attachments organization reconcile completed', {
|
|
57
|
+
tenantId,
|
|
58
|
+
scanned: report.scanned,
|
|
59
|
+
updated: report.updated,
|
|
60
|
+
unresolved: report.unresolved,
|
|
61
|
+
skippedVirtual: report.skippedVirtual,
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
},
|
|
41
65
|
{
|
|
42
66
|
id: 'customers.seed-interaction-statuses',
|
|
43
67
|
version: '0.6.5',
|
|
@@ -306,15 +306,17 @@ Both link to `/backend/customer_accounts/{sourceEntityId}` for staff review.
|
|
|
306
306
|
|
|
307
307
|
| Spot ID | Widget | Purpose |
|
|
308
308
|
|---------|--------|---------|
|
|
309
|
-
| `customers.person` | `account-status` |
|
|
310
|
-
| `crud-form:customers.person` | `account-status` |
|
|
311
|
-
| `
|
|
312
|
-
| `customers.company` | `company-users` | Shows portal users on
|
|
313
|
-
| `crud-form:customers.company` | `company-users` | Alias for CrudForm hosts deriving the spot from `customers.company` |
|
|
314
|
-
| `crud-form:customers:customer_company_profile:fields` | `company-users` | Shows portal users linked to a CRM company |
|
|
309
|
+
| `customers.person` | `account-status` | Alias for hosts requesting the bare `customers.person` entity spot |
|
|
310
|
+
| `crud-form:customers.person` | `account-status` | Shows portal account status on the person v2 detail page (`people-v2/[id]`) |
|
|
311
|
+
| `customers.company` | `company-users` | Alias for hosts requesting the bare `customers.company` entity spot |
|
|
312
|
+
| `crud-form:customers.company` | `company-users` | Shows portal users on the company v2 detail page (`companies-v2/[id]`) |
|
|
315
313
|
|
|
316
314
|
Both inject as column 2 groups with priority 200, gated by `customer_accounts.view` feature.
|
|
317
315
|
|
|
316
|
+
CrudForm derives its spot id as `crud-form:${entityId}` with every `:` normalized to `.`, so a
|
|
317
|
+
registered `crud-form:` key whose entity segment still contains `:` can never be requested by any
|
|
318
|
+
host. Register against the normalized (dot) form only — `injection-table.test.ts` enforces this.
|
|
319
|
+
|
|
318
320
|
## Backend Pages
|
|
319
321
|
|
|
320
322
|
| Path | Purpose |
|
|
@@ -36,6 +36,7 @@ export async function GET(req: Request, { params }: { params: { id: string } })
|
|
|
36
36
|
const role = await em.findOne(CustomerRole, {
|
|
37
37
|
id: params.id,
|
|
38
38
|
tenantId: auth.tenantId,
|
|
39
|
+
organizationId: auth.orgId,
|
|
39
40
|
deletedAt: null,
|
|
40
41
|
})
|
|
41
42
|
if (!role) {
|
|
@@ -94,6 +95,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
94
95
|
const role = await em.findOne(CustomerRole, {
|
|
95
96
|
id: params.id,
|
|
96
97
|
tenantId: auth.tenantId,
|
|
98
|
+
organizationId: auth.orgId,
|
|
97
99
|
deletedAt: null,
|
|
98
100
|
})
|
|
99
101
|
if (!role) {
|
|
@@ -127,7 +129,11 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
127
129
|
if (parsed.data.isDefault !== undefined) updates.isDefault = parsed.data.isDefault
|
|
128
130
|
if (parsed.data.customerAssignable !== undefined) updates.customerAssignable = parsed.data.customerAssignable
|
|
129
131
|
|
|
130
|
-
await em.nativeUpdate(CustomerRole, {
|
|
132
|
+
await em.nativeUpdate(CustomerRole, {
|
|
133
|
+
id: role.id,
|
|
134
|
+
tenantId: auth.tenantId,
|
|
135
|
+
organizationId: auth.orgId,
|
|
136
|
+
}, updates)
|
|
131
137
|
|
|
132
138
|
void emitCustomerAccountsEvent('customer_accounts.role.updated', {
|
|
133
139
|
id: role.id,
|
|
@@ -157,6 +163,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
157
163
|
const role = await em.findOne(CustomerRole, {
|
|
158
164
|
id: params.id,
|
|
159
165
|
tenantId: auth.tenantId,
|
|
166
|
+
organizationId: auth.orgId,
|
|
160
167
|
deletedAt: null,
|
|
161
168
|
})
|
|
162
169
|
if (!role) {
|
|
@@ -191,8 +198,15 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
191
198
|
return NextResponse.json({ ok: false, error: `Cannot delete role with ${assignedUsersCount} assigned user(s). Reassign users first.` }, { status: 400 })
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
await em.nativeUpdate(CustomerRole, {
|
|
195
|
-
|
|
201
|
+
await em.nativeUpdate(CustomerRole, {
|
|
202
|
+
id: role.id,
|
|
203
|
+
tenantId: auth.tenantId,
|
|
204
|
+
organizationId: auth.orgId,
|
|
205
|
+
}, { deletedAt: new Date() })
|
|
206
|
+
await em.nativeUpdate(CustomerRoleAcl, {
|
|
207
|
+
role: role.id as any,
|
|
208
|
+
tenantId: auth.tenantId,
|
|
209
|
+
}, { deletedAt: new Date() })
|
|
196
210
|
|
|
197
211
|
void emitCustomerAccountsEvent('customer_accounts.role.deleted', {
|
|
198
212
|
id: role.id,
|
|
@@ -60,7 +60,12 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
60
60
|
? await findWithDecryption(
|
|
61
61
|
em,
|
|
62
62
|
CustomerRole,
|
|
63
|
-
{
|
|
63
|
+
{
|
|
64
|
+
id: { $in: requestedRoleIds },
|
|
65
|
+
tenantId: auth.tenantId,
|
|
66
|
+
organizationId: auth.orgId,
|
|
67
|
+
deletedAt: null,
|
|
68
|
+
} as any,
|
|
64
69
|
undefined,
|
|
65
70
|
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
66
71
|
)
|
|
@@ -67,7 +67,12 @@ export async function POST(req: Request) {
|
|
|
67
67
|
? await findWithDecryption(
|
|
68
68
|
em,
|
|
69
69
|
CustomerRole,
|
|
70
|
-
{
|
|
70
|
+
{
|
|
71
|
+
id: { $in: requestedRoleIds },
|
|
72
|
+
tenantId: auth.tenantId,
|
|
73
|
+
organizationId: auth.orgId,
|
|
74
|
+
deletedAt: null,
|
|
75
|
+
} as any,
|
|
71
76
|
undefined,
|
|
72
77
|
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
73
78
|
)
|
|
@@ -29,7 +29,9 @@ export class CustomerRbacService {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
private getCacheKey(userId: string, scope: { tenantId: string; organizationId: string }): string {
|
|
32
|
-
|
|
32
|
+
// Keep the authorization query versioned in the key so cache entries created
|
|
33
|
+
// before scope hardening cannot preserve grants that are no longer valid.
|
|
34
|
+
return `customer_rbac:v2:${userId}:${scope.tenantId}:${scope.organizationId}`
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
private getUserTag(userId: string): string {
|
|
@@ -89,8 +91,13 @@ export class CustomerRbacService {
|
|
|
89
91
|
// Aggregate role ACLs
|
|
90
92
|
const links = await em.find(CustomerUserRole, {
|
|
91
93
|
user: userId as any,
|
|
94
|
+
role: {
|
|
95
|
+
tenantId: scope.tenantId,
|
|
96
|
+
organizationId: scope.organizationId,
|
|
97
|
+
deletedAt: null,
|
|
98
|
+
},
|
|
92
99
|
deletedAt: null,
|
|
93
|
-
}, { populate: ['role'] })
|
|
100
|
+
} as any, { populate: ['role'] })
|
|
94
101
|
const roleIds = links.map((l) => (l.role as any)?.id).filter(Boolean)
|
|
95
102
|
|
|
96
103
|
let isPortalAdmin = false
|
|
@@ -19,10 +19,8 @@ const companyUsersWidget = {
|
|
|
19
19
|
export const injectionTable: ModuleInjectionTable = {
|
|
20
20
|
'customers.person': [accountStatusWidget],
|
|
21
21
|
'crud-form:customers.person': [accountStatusWidget],
|
|
22
|
-
'crud-form:customers:customer_person_profile:fields': [accountStatusWidget],
|
|
23
22
|
'customers.company': [companyUsersWidget],
|
|
24
23
|
'crud-form:customers.company': [companyUsersWidget],
|
|
25
|
-
'crud-form:customers:customer_company_profile:fields': [companyUsersWidget],
|
|
26
24
|
// Step 4.10 — Portal AiChat injection example.
|
|
27
25
|
// Mapped to the portal profile page's `pageAfter('profile')` spot;
|
|
28
26
|
// third-party modules targeting other portal pages can copy this entry.
|
|
@@ -31,6 +31,7 @@ import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
|
31
31
|
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
32
32
|
import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
|
|
33
33
|
import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
34
|
+
import { useDealsAccess } from '../../../../components/detail/useDealsAccess'
|
|
34
35
|
import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
|
|
35
36
|
import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
|
|
36
37
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
@@ -69,6 +70,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
69
70
|
}, [searchParams])
|
|
70
71
|
const [activeTab, setActiveTab] = React.useState<CompanyTabId>(initialTab)
|
|
71
72
|
const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
|
|
73
|
+
const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
|
|
72
74
|
|
|
73
75
|
// Form state
|
|
74
76
|
const [isDirty, setIsDirty] = React.useState(false)
|
|
@@ -328,6 +330,14 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
328
330
|
setSectionAction(null)
|
|
329
331
|
}, [activeTab])
|
|
330
332
|
|
|
333
|
+
// A `?tab=deals` deep link must not strand users without `customers.deals.view`
|
|
334
|
+
// on a tab that no longer exists for them. Wait for the granted features to load
|
|
335
|
+
// so a permitted user is never bounced off the tab mid-fetch.
|
|
336
|
+
React.useEffect(() => {
|
|
337
|
+
if (!isDealsAccessReady || canViewDeals) return
|
|
338
|
+
setActiveTab((current) => (current === 'deals' ? 'people' : current))
|
|
339
|
+
}, [isDealsAccessReady, canViewDeals])
|
|
340
|
+
|
|
331
341
|
// Deals scope
|
|
332
342
|
const dealsScope = React.useMemo(
|
|
333
343
|
() => (currentCompanyId ? ({ kind: 'company', entityId: currentCompanyId } as const) : null),
|
|
@@ -544,7 +554,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
544
554
|
/>
|
|
545
555
|
)}
|
|
546
556
|
|
|
547
|
-
{activeTab === 'deals' && (
|
|
557
|
+
{activeTab === 'deals' && canViewDeals && (
|
|
548
558
|
<DealsSection
|
|
549
559
|
scope={dealsScope}
|
|
550
560
|
emptyLabel={t('customers.companies.detail.empty.deals', 'No deals linked to this company.')}
|
|
@@ -24,9 +24,13 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
|
|
|
24
24
|
map: translateWithFallback(t, 'customers.deals.kanban.view.map', 'Map'),
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Link-based tab row (two routes), so the Tabs primitive (state-driven,
|
|
28
|
+
// onValueChange) does not fit — real <Link> semantics must stay. Classes
|
|
29
|
+
// mirror the Tabs underline variant: accent-indigo active border,
|
|
30
|
+
// shadow-focus halo.
|
|
27
31
|
const baseTab =
|
|
28
|
-
'inline-flex items-center px-3.5 py-2.5 text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:
|
|
29
|
-
const activeTab = 'border-b-2 border-
|
|
32
|
+
'inline-flex items-center px-3.5 py-2.5 text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:shadow-focus'
|
|
33
|
+
const activeTab = 'border-b-2 border-accent-indigo font-semibold text-foreground'
|
|
30
34
|
const inactiveTab = 'border-b-2 border-transparent font-normal text-muted-foreground hover:text-foreground'
|
|
31
35
|
|
|
32
36
|
// Renders the active tab as a non-navigating `<span>` and the inactive tab as a `<Link>`,
|