@open-mercato/core 0.6.6-develop.6542.1.fee3e68aa6 → 0.6.6-develop.6551.1.1511bfa890
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 +7 -4
- package/dist/modules/api_keys/api/keys/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/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/lib/autologin.js +19 -0
- package/dist/modules/auth/lib/autologin.js.map +7 -0
- 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/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 +27 -37
- 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/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/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/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/components/SalesChannelsDisabledNotice.js +15 -0
- package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -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/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/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 +7 -7
- package/src/modules/api_keys/api/keys/route.ts +9 -4
- package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
- 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/lib/autologin.ts +36 -0
- 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/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +27 -49
- 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/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/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/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/components/SalesChannelsDisabledNotice.tsx +21 -0
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +7 -3
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
- package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
1
|
+
import { createReadStream, promises as fs } from "fs";
|
|
2
|
+
import { Readable } from "stream";
|
|
2
3
|
import { randomUUID } from "crypto";
|
|
3
4
|
import { Attachment, AttachmentPartition } from "../../attachments/data/entities.js";
|
|
4
5
|
import { buildAttachmentFileUrl } from "../../attachments/lib/imageUrls.js";
|
|
@@ -59,8 +60,26 @@ async function readSyncExcelUploadBuffer(attachment) {
|
|
|
59
60
|
throw error;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
async function createSyncExcelUploadReadStream(attachment) {
|
|
64
|
+
const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64;
|
|
65
|
+
try {
|
|
66
|
+
const absolutePath = resolveAttachmentAbsolutePath(
|
|
67
|
+
attachment.partitionCode,
|
|
68
|
+
attachment.storagePath,
|
|
69
|
+
attachment.storageDriver
|
|
70
|
+
);
|
|
71
|
+
await fs.access(absolutePath);
|
|
72
|
+
return createReadStream(absolutePath);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (typeof inlineCsvBase64 === "string" && inlineCsvBase64.length > 0) {
|
|
75
|
+
return Readable.from([Buffer.from(inlineCsvBase64, "base64")]);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
export {
|
|
63
81
|
createSyncExcelUploadAttachment,
|
|
82
|
+
createSyncExcelUploadReadStream,
|
|
64
83
|
readSyncExcelUploadBuffer
|
|
65
84
|
};
|
|
66
85
|
//# sourceMappingURL=upload-storage.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sync_excel/lib/upload-storage.ts"],
|
|
4
|
-
"sourcesContent": ["import { promises as fs } from 'fs'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../attachments/data/entities'\nimport { buildAttachmentFileUrl } from '../../attachments/lib/imageUrls'\nimport { ensureDefaultPartitions } from '../../attachments/lib/partitions'\nimport { resolveAttachmentAbsolutePath, storePartitionFile } from '../../attachments/lib/storage'\n\nconst SYNC_EXCEL_ATTACHMENT_ENTITY_ID = 'sync_excel:upload'\nconst SYNC_EXCEL_PARTITION_CODE = 'privateAttachments'\n\nexport async function createSyncExcelUploadAttachment(input: {\n em: EntityManager\n uploadId: string\n organizationId: string\n tenantId: string\n fileName: string\n mimeType: string\n buffer: Buffer\n}): Promise<Attachment> {\n await ensureDefaultPartitions(input.em)\n\n // Attachment partitions are global storage configuration, not tenant-scoped payload data.\n const partition = await input.em.findOne(AttachmentPartition, { code: SYNC_EXCEL_PARTITION_CODE })\n if (!partition) {\n throw new Error('Storage partition is not configured.')\n }\n\n const stored = await storePartitionFile({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n buffer: input.buffer,\n })\n\n const attachmentId = randomUUID()\n const attachment = input.em.create(Attachment, {\n id: attachmentId,\n entityId: SYNC_EXCEL_ATTACHMENT_ENTITY_ID,\n recordId: input.uploadId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n partitionCode: partition.code,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n storageDriver: partition.storageDriver || 'local',\n storagePath: stored.storagePath,\n url: buildAttachmentFileUrl(attachmentId),\n storageMetadata: {\n module: 'sync_excel',\n temporary: true,\n uploadId: input.uploadId,\n },\n })\n\n input.em.persist(attachment)\n await input.em.flush()\n\n return attachment\n}\n\nexport async function readSyncExcelUploadBuffer(\n attachment: Pick<Attachment, 'partitionCode' | 'storagePath' | 'storageDriver' | 'storageMetadata'>,\n): Promise<Buffer> {\n const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64\n const absolutePath = resolveAttachmentAbsolutePath(\n attachment.partitionCode,\n attachment.storagePath,\n attachment.storageDriver,\n )\n try {\n return await fs.readFile(absolutePath)\n } catch (error) {\n if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {\n return Buffer.from(inlineCsvBase64, 'base64')\n }\n throw error\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,UAAU;
|
|
4
|
+
"sourcesContent": ["import { createReadStream, promises as fs } from 'fs'\nimport { Readable } from 'stream'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../attachments/data/entities'\nimport { buildAttachmentFileUrl } from '../../attachments/lib/imageUrls'\nimport { ensureDefaultPartitions } from '../../attachments/lib/partitions'\nimport { resolveAttachmentAbsolutePath, storePartitionFile } from '../../attachments/lib/storage'\n\nconst SYNC_EXCEL_ATTACHMENT_ENTITY_ID = 'sync_excel:upload'\nconst SYNC_EXCEL_PARTITION_CODE = 'privateAttachments'\n\nexport async function createSyncExcelUploadAttachment(input: {\n em: EntityManager\n uploadId: string\n organizationId: string\n tenantId: string\n fileName: string\n mimeType: string\n buffer: Buffer\n}): Promise<Attachment> {\n await ensureDefaultPartitions(input.em)\n\n // Attachment partitions are global storage configuration, not tenant-scoped payload data.\n const partition = await input.em.findOne(AttachmentPartition, { code: SYNC_EXCEL_PARTITION_CODE })\n if (!partition) {\n throw new Error('Storage partition is not configured.')\n }\n\n const stored = await storePartitionFile({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: input.fileName,\n buffer: input.buffer,\n })\n\n const attachmentId = randomUUID()\n const attachment = input.em.create(Attachment, {\n id: attachmentId,\n entityId: SYNC_EXCEL_ATTACHMENT_ENTITY_ID,\n recordId: input.uploadId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n partitionCode: partition.code,\n fileName: input.fileName,\n mimeType: input.mimeType,\n fileSize: input.buffer.length,\n storageDriver: partition.storageDriver || 'local',\n storagePath: stored.storagePath,\n url: buildAttachmentFileUrl(attachmentId),\n storageMetadata: {\n module: 'sync_excel',\n temporary: true,\n uploadId: input.uploadId,\n },\n })\n\n input.em.persist(attachment)\n await input.em.flush()\n\n return attachment\n}\n\nexport async function readSyncExcelUploadBuffer(\n attachment: Pick<Attachment, 'partitionCode' | 'storagePath' | 'storageDriver' | 'storageMetadata'>,\n): Promise<Buffer> {\n const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64\n const absolutePath = resolveAttachmentAbsolutePath(\n attachment.partitionCode,\n attachment.storagePath,\n attachment.storageDriver,\n )\n try {\n return await fs.readFile(absolutePath)\n } catch (error) {\n if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {\n return Buffer.from(inlineCsvBase64, 'base64')\n }\n throw error\n }\n}\n\nexport async function createSyncExcelUploadReadStream(\n attachment: Pick<Attachment, 'partitionCode' | 'storagePath' | 'storageDriver' | 'storageMetadata'>,\n): Promise<Readable> {\n const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64\n\n try {\n const absolutePath = resolveAttachmentAbsolutePath(\n attachment.partitionCode,\n attachment.storagePath,\n attachment.storageDriver,\n )\n await fs.access(absolutePath)\n return createReadStream(absolutePath)\n } catch (error) {\n if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {\n return Readable.from([Buffer.from(inlineCsvBase64, 'base64')])\n }\n throw error\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB,YAAY,UAAU;AACjD,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAE3B,SAAS,YAAY,2BAA2B;AAChD,SAAS,8BAA8B;AACvC,SAAS,+BAA+B;AACxC,SAAS,+BAA+B,0BAA0B;AAElE,MAAM,kCAAkC;AACxC,MAAM,4BAA4B;AAElC,eAAsB,gCAAgC,OAQ9B;AACtB,QAAM,wBAAwB,MAAM,EAAE;AAGtC,QAAM,YAAY,MAAM,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjG,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAEA,QAAM,SAAS,MAAM,mBAAmB;AAAA,IACtC,eAAe,UAAU;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,EAChB,CAAC;AAED,QAAM,eAAe,WAAW;AAChC,QAAM,aAAa,MAAM,GAAG,OAAO,YAAY;AAAA,IAC7C,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,eAAe,UAAU;AAAA,IACzB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM;AAAA,IAChB,UAAU,MAAM,OAAO;AAAA,IACvB,eAAe,UAAU,iBAAiB;AAAA,IAC1C,aAAa,OAAO;AAAA,IACpB,KAAK,uBAAuB,YAAY;AAAA,IACxC,iBAAiB;AAAA,MACf,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,UAAU,MAAM;AAAA,IAClB;AAAA,EACF,CAAC;AAED,QAAM,GAAG,QAAQ,UAAU;AAC3B,QAAM,MAAM,GAAG,MAAM;AAErB,SAAO;AACT;AAEA,eAAsB,0BACpB,YACiB;AACjB,QAAM,kBAAkB,WAAW,iBAAiB;AACpD,QAAM,eAAe;AAAA,IACnB,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACA,MAAI;AACF,WAAO,MAAM,GAAG,SAAS,YAAY;AAAA,EACvC,SAAS,OAAO;AACd,QAAI,OAAO,oBAAoB,YAAY,gBAAgB,SAAS,GAAG;AACrE,aAAO,OAAO,KAAK,iBAAiB,QAAQ;AAAA,IAC9C;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,gCACpB,YACmB;AACnB,QAAM,kBAAkB,WAAW,iBAAiB;AAEpD,MAAI;AACF,UAAM,eAAe;AAAA,MACnB,WAAW;AAAA,MACX,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AACA,UAAM,GAAG,OAAO,YAAY;AAC5B,WAAO,iBAAiB,YAAY;AAAA,EACtC,SAAS,OAAO;AACd,QAAI,OAAO,oBAAoB,YAAY,gBAAgB,SAAS,GAAG;AACrE,aAAO,SAAS,KAAK,CAAC,OAAO,KAAK,iBAAiB,QAAQ,CAAC,CAAC;AAAA,IAC/D;AACA,UAAM;AAAA,EACR;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const id = "id";
|
|
2
|
+
export const operation_key = "operation_key";
|
|
3
|
+
export const provider_key = "provider_key";
|
|
4
|
+
export const claim_token = "claim_token";
|
|
5
|
+
export const claimed_at = "claimed_at";
|
|
6
|
+
export const gateway_transaction_id = "gateway_transaction_id";
|
|
7
|
+
export const organization_id = "organization_id";
|
|
8
|
+
export const tenant_id = "tenant_id";
|
|
9
|
+
export const created_at = "created_at";
|
|
10
|
+
export const updated_at = "updated_at";
|
|
@@ -267,6 +267,7 @@ export const M = {
|
|
|
267
267
|
},
|
|
268
268
|
"payment_gateways": {
|
|
269
269
|
"gateway_transaction": "payment_gateways:gateway_transaction",
|
|
270
|
+
"gateway_session_initialization": "payment_gateways:gateway_session_initialization",
|
|
270
271
|
"webhook_processed_event": "payment_gateways:webhook_processed_event"
|
|
271
272
|
},
|
|
272
273
|
"shipping_carriers": {
|
|
@@ -1225,6 +1225,18 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
1225
1225
|
"updated_at": "updated_at",
|
|
1226
1226
|
"value": "value"
|
|
1227
1227
|
},
|
|
1228
|
+
"gateway_session_initialization": {
|
|
1229
|
+
"id": "id",
|
|
1230
|
+
"operation_key": "operation_key",
|
|
1231
|
+
"provider_key": "provider_key",
|
|
1232
|
+
"claim_token": "claim_token",
|
|
1233
|
+
"claimed_at": "claimed_at",
|
|
1234
|
+
"gateway_transaction_id": "gateway_transaction_id",
|
|
1235
|
+
"organization_id": "organization_id",
|
|
1236
|
+
"tenant_id": "tenant_id",
|
|
1237
|
+
"created_at": "created_at",
|
|
1238
|
+
"updated_at": "updated_at"
|
|
1239
|
+
},
|
|
1228
1240
|
"gateway_transaction": {
|
|
1229
1241
|
"id": "id",
|
|
1230
1242
|
"payment_id": "payment_id",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6551.1.1511bfa890",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -253,16 +253,16 @@
|
|
|
253
253
|
"zod": "^4.4.3"
|
|
254
254
|
},
|
|
255
255
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6551.1.1511bfa890",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6551.1.1511bfa890",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6551.1.1511bfa890",
|
|
259
259
|
"react": "^19.0.0",
|
|
260
260
|
"react-dom": "^19.0.0"
|
|
261
261
|
},
|
|
262
262
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
263
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6551.1.1511bfa890",
|
|
264
|
+
"@open-mercato/shared": "0.6.6-develop.6551.1.1511bfa890",
|
|
265
|
+
"@open-mercato/ui": "0.6.6-develop.6551.1.1511bfa890",
|
|
266
266
|
"@testing-library/dom": "^10.4.1",
|
|
267
267
|
"@testing-library/jest-dom": "^6.9.1",
|
|
268
268
|
"@testing-library/react": "^16.3.1",
|
|
@@ -299,10 +299,15 @@ const crud = makeCrudRoute<
|
|
|
299
299
|
|
|
300
300
|
const allowedIds = ctx.organizationScope?.allowedIds ?? null
|
|
301
301
|
const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? auth.orgId ?? null
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx)
|
|
303
|
+
if (
|
|
304
|
+
!isOrganizationAccessAllowed({
|
|
305
|
+
isSuperAdmin,
|
|
306
|
+
allowedOrganizationIds: allowedIds,
|
|
307
|
+
targetOrganizationId: organizationId,
|
|
308
|
+
})
|
|
309
|
+
) {
|
|
310
|
+
throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })
|
|
306
311
|
}
|
|
307
312
|
scopedCtx.__apiKeyOrganizationId = organizationId ?? null
|
|
308
313
|
|
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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>`,
|