@open-mercato/core 0.6.6 → 0.6.7-develop.6552.1.5208a19369
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/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/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
|
@@ -13,6 +13,7 @@ import { StorageDriverFactory } from '../lib/drivers'
|
|
|
13
13
|
import { OcrService, shouldUseLlmOcr } from '../lib/ocrService'
|
|
14
14
|
import { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'
|
|
15
15
|
import { assertAttachmentScopeInvariant } from '../lib/access'
|
|
16
|
+
import { resolveAttachmentOrganizationId } from '../lib/requestScope'
|
|
16
17
|
import {
|
|
17
18
|
mergeAttachmentMetadata,
|
|
18
19
|
normalizeAttachmentAssignments,
|
|
@@ -198,10 +199,11 @@ export async function GET(req: Request) {
|
|
|
198
199
|
}
|
|
199
200
|
const { entityId, recordId, page, pageSize } = parsedQuery.data
|
|
200
201
|
|
|
201
|
-
const
|
|
202
|
-
const em = resolve('em') as EntityManager
|
|
202
|
+
const container = await createRequestContainer()
|
|
203
|
+
const em = container.resolve('em') as EntityManager
|
|
204
|
+
const orgId = await resolveAttachmentOrganizationId(container, auth, req)
|
|
203
205
|
const filter: Record<string, unknown> = { entityId, recordId, tenantId: auth.tenantId! }
|
|
204
|
-
if (
|
|
206
|
+
if (orgId) filter.organizationId = orgId
|
|
205
207
|
const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC' }
|
|
206
208
|
const usePaging = typeof page === 'number' && typeof pageSize === 'number'
|
|
207
209
|
const total = usePaging ? await em.count(Attachment, filter) : null
|
|
@@ -224,7 +226,7 @@ export async function GET(req: Request) {
|
|
|
224
226
|
},
|
|
225
227
|
{
|
|
226
228
|
tenantId: auth.tenantId ?? null,
|
|
227
|
-
organizationId:
|
|
229
|
+
organizationId: orgId ?? null,
|
|
228
230
|
},
|
|
229
231
|
)
|
|
230
232
|
return NextResponse.json({
|
|
@@ -275,7 +277,6 @@ export async function POST(req: Request) {
|
|
|
275
277
|
}, { status: 400 })
|
|
276
278
|
}
|
|
277
279
|
const tenantId = auth.tenantId
|
|
278
|
-
const orgId = auth.orgId
|
|
279
280
|
|
|
280
281
|
const contentType = req.headers.get('content-type') || ''
|
|
281
282
|
if (!contentType.toLowerCase().includes('multipart/form-data')) {
|
|
@@ -301,11 +302,13 @@ export async function POST(req: Request) {
|
|
|
301
302
|
const tags = parseFormTags(form.get('tags'))
|
|
302
303
|
const assignmentsFromForm = parseFormAssignments(form.get('assignments'))
|
|
303
304
|
|
|
304
|
-
const
|
|
305
|
-
const em = resolve('em') as EntityManager
|
|
306
|
-
const dataEngine = resolve('dataEngine')
|
|
305
|
+
const container = await createRequestContainer()
|
|
306
|
+
const em = container.resolve('em') as EntityManager
|
|
307
|
+
const dataEngine = container.resolve('dataEngine')
|
|
307
308
|
const storageDriverFactory =
|
|
308
|
-
(resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
309
|
+
(container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
310
|
+
const orgId = await resolveAttachmentOrganizationId(container, auth, req)
|
|
311
|
+
if (!orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
309
312
|
await ensureDefaultPartitions(em)
|
|
310
313
|
// Optional per-field validations
|
|
311
314
|
let partitionFromField: string | null = null
|
|
@@ -436,12 +439,12 @@ export async function POST(req: Request) {
|
|
|
436
439
|
}
|
|
437
440
|
const metadata = mergeAttachmentMetadata(null, { assignments, tags })
|
|
438
441
|
const attachmentId = randomUUID()
|
|
439
|
-
assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId:
|
|
442
|
+
assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: orgId })
|
|
440
443
|
const att = em.create(Attachment, {
|
|
441
444
|
id: attachmentId,
|
|
442
445
|
entityId,
|
|
443
446
|
recordId,
|
|
444
|
-
organizationId:
|
|
447
|
+
organizationId: orgId,
|
|
445
448
|
tenantId: auth.tenantId!,
|
|
446
449
|
fileName: safeName,
|
|
447
450
|
mimeType: fileMimeType,
|
|
@@ -545,17 +548,19 @@ export async function DELETE(req: Request) {
|
|
|
545
548
|
const url = new URL(req.url)
|
|
546
549
|
const id = url.searchParams.get('id') || ''
|
|
547
550
|
if (!id) return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })
|
|
548
|
-
const
|
|
549
|
-
const em = resolve('em') as EntityManager
|
|
550
|
-
const dataEngine = resolve('dataEngine')
|
|
551
|
+
const container = await createRequestContainer()
|
|
552
|
+
const em = container.resolve('em') as EntityManager
|
|
553
|
+
const dataEngine = container.resolve('dataEngine')
|
|
551
554
|
const storageDriverFactory =
|
|
552
|
-
(resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
555
|
+
(container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
556
|
+
// Resolve the currently selected organization (#3765) so a multi-org admin who
|
|
557
|
+
// switched the header org deletes within that org, not their pinned home org.
|
|
558
|
+
// A superadmin browsing with "All organizations" selected has no concrete org,
|
|
559
|
+
// so resolution returns null and the delete falls back to a tenant-only scope
|
|
560
|
+
// (#3764) — letting them remove any attachment in the tenant.
|
|
561
|
+
const orgId = await resolveAttachmentOrganizationId(container, auth, req)
|
|
557
562
|
const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId! }
|
|
558
|
-
if (
|
|
563
|
+
if (orgId) deleteFilter.organizationId = orgId
|
|
559
564
|
const record = await em.findOne(Attachment, deleteFilter)
|
|
560
565
|
if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })
|
|
561
566
|
await em.remove(record).flush()
|
|
@@ -565,7 +570,7 @@ export async function DELETE(req: Request) {
|
|
|
565
570
|
if (record.storagePath) {
|
|
566
571
|
const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
567
572
|
tenantId: record.tenantId ?? auth.tenantId!,
|
|
568
|
-
organizationId: record.organizationId ??
|
|
573
|
+
organizationId: record.organizationId ?? orgId ?? '',
|
|
569
574
|
})
|
|
570
575
|
await delDriver.delete(record.partitionCode, record.storagePath)
|
|
571
576
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { MarkdownContent } from '@open-mercato/ui/backend/markdown'
|
|
3
3
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
4
5
|
|
|
5
6
|
type Props = {
|
|
6
7
|
content?: string | null
|
|
@@ -25,12 +26,6 @@ export function AttachmentContentPreview({
|
|
|
25
26
|
const [tab, setTab] = React.useState<'source' | 'preview'>('source')
|
|
26
27
|
const text = (content ?? '').trim()
|
|
27
28
|
|
|
28
|
-
// ARIA IDs for accessibility
|
|
29
|
-
const sourceTabId = 'attachment-content-preview-tab-source'
|
|
30
|
-
const previewTabId = 'attachment-content-preview-tab-preview'
|
|
31
|
-
const sourcePanelId = 'attachment-content-preview-panel-source'
|
|
32
|
-
const previewPanelId = 'attachment-content-preview-panel-preview'
|
|
33
|
-
|
|
34
29
|
if (!text) {
|
|
35
30
|
return <div className="text-xs text-muted-foreground italic">{emptyLabel}</div>
|
|
36
31
|
}
|
|
@@ -41,47 +36,18 @@ export function AttachmentContentPreview({
|
|
|
41
36
|
return (
|
|
42
37
|
<div className="space-y-2">
|
|
43
38
|
{/* Tab Navigation */}
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
aria-selected={tab === 'source'}
|
|
51
|
-
aria-controls={sourcePanelId}
|
|
52
|
-
className={`-mb-px border-b-2 px-0 pb-2 font-medium transition-colors ${
|
|
53
|
-
tab === 'source'
|
|
54
|
-
? 'border-accent-indigo text-foreground'
|
|
55
|
-
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
56
|
-
}`}
|
|
57
|
-
onClick={() => setTab('source')}
|
|
58
|
-
>
|
|
59
|
-
{sourceLabel}
|
|
60
|
-
</button>
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
id={previewTabId}
|
|
64
|
-
role="tab"
|
|
65
|
-
aria-selected={tab === 'preview'}
|
|
66
|
-
aria-controls={previewPanelId}
|
|
67
|
-
className={`-mb-px border-b-2 px-0 pb-2 font-medium transition-colors ${
|
|
68
|
-
tab === 'preview'
|
|
69
|
-
? 'border-accent-indigo text-foreground'
|
|
70
|
-
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
71
|
-
}`}
|
|
72
|
-
onClick={() => setTab('preview')}
|
|
73
|
-
>
|
|
74
|
-
{previewLabel}
|
|
75
|
-
</button>
|
|
76
|
-
</nav>
|
|
77
|
-
</div>
|
|
39
|
+
<Tabs value={tab} onValueChange={(value) => setTab(value as 'source' | 'preview')} variant="underline">
|
|
40
|
+
<TabsList className="w-full">
|
|
41
|
+
<TabsTrigger value="source">{sourceLabel}</TabsTrigger>
|
|
42
|
+
<TabsTrigger value="preview">{previewLabel}</TabsTrigger>
|
|
43
|
+
</TabsList>
|
|
44
|
+
</Tabs>
|
|
78
45
|
|
|
79
46
|
{/* Tab Panels */}
|
|
80
47
|
{tab === 'source' ? (
|
|
81
48
|
<div
|
|
82
49
|
role="tabpanel"
|
|
83
|
-
|
|
84
|
-
aria-labelledby={sourceTabId}
|
|
50
|
+
aria-label={sourceLabel}
|
|
85
51
|
data-testid="attachment-content-preview"
|
|
86
52
|
className="whitespace-pre-wrap text-sm text-muted-foreground"
|
|
87
53
|
>
|
|
@@ -90,8 +56,7 @@ export function AttachmentContentPreview({
|
|
|
90
56
|
) : (
|
|
91
57
|
<div
|
|
92
58
|
role="tabpanel"
|
|
93
|
-
|
|
94
|
-
aria-labelledby={previewTabId}
|
|
59
|
+
aria-label={previewLabel}
|
|
95
60
|
data-testid="markdown-preview"
|
|
96
61
|
>
|
|
97
62
|
<MarkdownContent
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
4
|
+
import { Attachment } from '../data/entities'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('attachments').child({ component: 'reconcileOrganization' })
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Virtual entity id for library attachments — these are not attached to a
|
|
10
|
+
* parent record that owns an organization, so there is nothing to reconcile
|
|
11
|
+
* against. They are counted as skipped rather than unresolved.
|
|
12
|
+
*/
|
|
13
|
+
const LIBRARY_ENTITY_ID = 'attachments:library'
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BATCH_SIZE = 500
|
|
16
|
+
|
|
17
|
+
export type AttachmentOrgReconcileEntityStat = {
|
|
18
|
+
scanned: number
|
|
19
|
+
updated: number
|
|
20
|
+
unresolved: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type AttachmentOrgReconcileReport = {
|
|
24
|
+
scanned: number
|
|
25
|
+
updated: number
|
|
26
|
+
unresolved: number
|
|
27
|
+
skippedVirtual: number
|
|
28
|
+
byEntity: Record<string, AttachmentOrgReconcileEntityStat>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AttachmentScanRow = {
|
|
32
|
+
id: string
|
|
33
|
+
entity_id: string | null
|
|
34
|
+
record_id: string | null
|
|
35
|
+
organization_id: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readParentOrganizationId(record: Record<string, unknown>): string | null {
|
|
39
|
+
const raw = record['organization_id'] ?? record['organizationId']
|
|
40
|
+
if (typeof raw === 'string' && raw.trim().length) return raw.trim()
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureBucket(
|
|
45
|
+
report: AttachmentOrgReconcileReport,
|
|
46
|
+
entityId: string,
|
|
47
|
+
): AttachmentOrgReconcileEntityStat {
|
|
48
|
+
const existing = report.byEntity[entityId]
|
|
49
|
+
if (existing) return existing
|
|
50
|
+
const bucket: AttachmentOrgReconcileEntityStat = { scanned: 0, updated: 0, unresolved: 0 }
|
|
51
|
+
report.byEntity[entityId] = bucket
|
|
52
|
+
return bucket
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reconcile the `organization_id` of existing attachments to the organization
|
|
57
|
+
* of the record they are attached to.
|
|
58
|
+
*
|
|
59
|
+
* Background (#3765): before the upload route became selected-organization
|
|
60
|
+
* aware, a multi-org admin who switched the header organization uploaded files
|
|
61
|
+
* that were silently stored under their *home* organization instead of the
|
|
62
|
+
* selected one (the organization of the parent record). Those rows are now
|
|
63
|
+
* invisible to org-scoped reads and stay orphaned. This heals them forward.
|
|
64
|
+
*
|
|
65
|
+
* The parent record's organization is the ground truth: an attachment is not
|
|
66
|
+
* distinguishable from a legitimately home-org one by looking at the attachment
|
|
67
|
+
* alone. Parent organizations are resolved generically through the Query Engine
|
|
68
|
+
* by `entityId` (works for both base and custom entities, tenant-scoped and
|
|
69
|
+
* organization-agnostic so the parent's real org is returned regardless of
|
|
70
|
+
* which org the caller sits in) — the same mechanism the attachments module
|
|
71
|
+
* already uses to enrich assignment details.
|
|
72
|
+
*
|
|
73
|
+
* The reconciliation is:
|
|
74
|
+
* - idempotent — rows already matching their parent are left untouched;
|
|
75
|
+
* - conservative — when the parent org cannot be resolved (unregistered
|
|
76
|
+
* entity id, hard-deleted parent, parent without an org column) the row is
|
|
77
|
+
* counted as `unresolved` and left as-is rather than guessed at;
|
|
78
|
+
* - tenant-scoped — it reconciles every attachment in the tenant across all
|
|
79
|
+
* organizations (the whole point is moving rows between orgs).
|
|
80
|
+
*
|
|
81
|
+
* Writes go through the passed `EntityManager` (which the caller runs inside a
|
|
82
|
+
* transaction) so the whole reconciliation commits atomically.
|
|
83
|
+
*/
|
|
84
|
+
export async function reconcileAttachmentOrganizations(opts: {
|
|
85
|
+
em: EntityManager
|
|
86
|
+
queryEngine: QueryEngine
|
|
87
|
+
tenantId: string
|
|
88
|
+
batchSize?: number
|
|
89
|
+
}): Promise<AttachmentOrgReconcileReport> {
|
|
90
|
+
const { em, queryEngine, tenantId } = opts
|
|
91
|
+
const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : DEFAULT_BATCH_SIZE
|
|
92
|
+
const report: AttachmentOrgReconcileReport = {
|
|
93
|
+
scanned: 0,
|
|
94
|
+
updated: 0,
|
|
95
|
+
unresolved: 0,
|
|
96
|
+
skippedVirtual: 0,
|
|
97
|
+
byEntity: {},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rows = (await em.getConnection().execute(
|
|
101
|
+
'select id, entity_id, record_id, organization_id from attachments where tenant_id = ?',
|
|
102
|
+
[tenantId],
|
|
103
|
+
)) as AttachmentScanRow[]
|
|
104
|
+
report.scanned = rows.length
|
|
105
|
+
if (!rows.length) return report
|
|
106
|
+
|
|
107
|
+
const groups = new Map<string, AttachmentScanRow[]>()
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
if (!row.id || !row.entity_id || !row.record_id) continue
|
|
110
|
+
const list = groups.get(row.entity_id) ?? []
|
|
111
|
+
list.push(row)
|
|
112
|
+
groups.set(row.entity_id, list)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pendingUpdates: Array<{ id: string; organizationId: string }> = []
|
|
116
|
+
|
|
117
|
+
for (const [entityId, group] of groups) {
|
|
118
|
+
const bucket = ensureBucket(report, entityId)
|
|
119
|
+
bucket.scanned += group.length
|
|
120
|
+
|
|
121
|
+
if (entityId === LIBRARY_ENTITY_ID) {
|
|
122
|
+
report.skippedVirtual += group.length
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const recordIds = Array.from(
|
|
127
|
+
new Set(group.map((row) => row.record_id).filter((value): value is string => !!value)),
|
|
128
|
+
)
|
|
129
|
+
const parentOrgByRecordId = new Map<string, string | null>()
|
|
130
|
+
let resolvable = true
|
|
131
|
+
|
|
132
|
+
for (let index = 0; index < recordIds.length; index += batchSize) {
|
|
133
|
+
const chunk = recordIds.slice(index, index + batchSize)
|
|
134
|
+
try {
|
|
135
|
+
const result = await queryEngine.query(entityId as any, {
|
|
136
|
+
fields: ['id', 'organization_id'],
|
|
137
|
+
filters: { id: chunk.length === 1 ? { $eq: chunk[0] } : { $in: chunk } },
|
|
138
|
+
tenantId,
|
|
139
|
+
withDeleted: true,
|
|
140
|
+
page: { pageSize: Math.max(chunk.length, 1) },
|
|
141
|
+
})
|
|
142
|
+
for (const item of result.items ?? []) {
|
|
143
|
+
const record = item as Record<string, unknown>
|
|
144
|
+
const recordId = record.id != null ? String(record.id) : null
|
|
145
|
+
if (!recordId) continue
|
|
146
|
+
parentOrgByRecordId.set(recordId, readParentOrganizationId(record))
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
resolvable = false
|
|
150
|
+
logger.warn('org reconcile: cannot resolve parent organization for entity', {
|
|
151
|
+
entityId,
|
|
152
|
+
error,
|
|
153
|
+
})
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!resolvable) {
|
|
159
|
+
bucket.unresolved += group.length
|
|
160
|
+
report.unresolved += group.length
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const row of group) {
|
|
165
|
+
const target = row.record_id ? parentOrgByRecordId.get(row.record_id) : null
|
|
166
|
+
if (!target) {
|
|
167
|
+
bucket.unresolved += 1
|
|
168
|
+
report.unresolved += 1
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if ((row.organization_id ?? null) === target) continue
|
|
172
|
+
pendingUpdates.push({ id: row.id, organizationId: target })
|
|
173
|
+
bucket.updated += 1
|
|
174
|
+
report.updated += 1
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < pendingUpdates.length; index += batchSize) {
|
|
179
|
+
const chunk = pendingUpdates.slice(index, index + batchSize)
|
|
180
|
+
for (const update of chunk) {
|
|
181
|
+
const reference = em.getReference(Attachment, update.id)
|
|
182
|
+
reference.organizationId = update.organizationId
|
|
183
|
+
}
|
|
184
|
+
await em.flush()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return report
|
|
188
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the organization an attachment request should act within.
|
|
7
|
+
*
|
|
8
|
+
* `auth.orgId` alone is NOT selected-organization aware for non-superadmin
|
|
9
|
+
* principals: `applySuperAdminScope` only rewrites `orgId` from the
|
|
10
|
+
* `om_selected_org` cookie for superadmins, so a regular multi-org admin who
|
|
11
|
+
* switches the header organization keeps `auth.orgId` pinned to their own home
|
|
12
|
+
* organization. The CRUD factory and other org-scoped routes derive the active
|
|
13
|
+
* organization via `resolveOrganizationScopeForRequest` (cookie-driven and
|
|
14
|
+
* RBAC-validated for ALL users, falling back to the home org when the selection
|
|
15
|
+
* is absent or inaccessible). Attachments must do the same so uploaded files
|
|
16
|
+
* land under — and are read back from — the currently selected organization
|
|
17
|
+
* rather than the uploader's home organization (#3765).
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveAttachmentOrganizationId(
|
|
20
|
+
container: AwilixContainer,
|
|
21
|
+
auth: AuthContext,
|
|
22
|
+
request: Request,
|
|
23
|
+
): Promise<string | null> {
|
|
24
|
+
if (!auth) return null
|
|
25
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
|
|
26
|
+
return scope?.selectedId ?? auth.orgId ?? null
|
|
27
|
+
}
|
|
@@ -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 {
|