@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682
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 +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +10 -9
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/tenantAccess.js +7 -0
- package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/directory/api/tenants/route.js +8 -2
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/commands/tenants.js +6 -0
- package/dist/modules/directory/commands/tenants.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/backendChrome.tsx +15 -9
- package/src/modules/auth/lib/tenantAccess.ts +8 -0
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/directory/api/tenants/route.ts +9 -1
- package/src/modules/directory/commands/tenants.ts +6 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Head, Preview, Body, Container, Text, Section, Link } from "@react-email/components";
|
|
3
|
+
function CustomerInvitationEmail({ inviteUrl, copy }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
5
|
+
/* @__PURE__ */ jsx(Head, {}),
|
|
6
|
+
/* @__PURE__ */ jsx(Preview, { children: copy.preview }),
|
|
7
|
+
/* @__PURE__ */ jsx(Body, { style: body, children: /* @__PURE__ */ jsx(Container, { style: container, children: /* @__PURE__ */ jsxs(Section, { children: [
|
|
8
|
+
/* @__PURE__ */ jsx(Text, { style: title, children: copy.title }),
|
|
9
|
+
/* @__PURE__ */ jsx(Text, { style: paragraph, children: copy.body }),
|
|
10
|
+
/* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Link, { href: inviteUrl, style: button, children: copy.cta }) }),
|
|
11
|
+
/* @__PURE__ */ jsx(Text, { style: hint, children: copy.hint })
|
|
12
|
+
] }) }) })
|
|
13
|
+
] });
|
|
14
|
+
}
|
|
15
|
+
const body = { backgroundColor: "#f9fafb", margin: 0, padding: "24px 0" };
|
|
16
|
+
const container = { backgroundColor: "#ffffff", borderRadius: 12, padding: 24, margin: "0 auto", maxWidth: 520, boxShadow: "0 1px 4px rgba(0,0,0,0.06)" };
|
|
17
|
+
const title = { fontSize: 20, fontWeight: 600, color: "#111827", margin: "0 0 12px" };
|
|
18
|
+
const paragraph = { fontSize: 14, color: "#4b5563", lineHeight: "20px", margin: "0 0 16px" };
|
|
19
|
+
const button = { display: "inline-block", backgroundColor: "#111827", color: "#ffffff", padding: "10px 14px", borderRadius: 8, textDecoration: "none", fontSize: 14 };
|
|
20
|
+
const hint = { fontSize: 12, color: "#6b7280", marginTop: 16 };
|
|
21
|
+
var CustomerInvitationEmail_default = CustomerInvitationEmail;
|
|
22
|
+
export {
|
|
23
|
+
CustomerInvitationEmail,
|
|
24
|
+
CustomerInvitationEmail_default as default
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=CustomerInvitationEmail.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx"],
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { Html, Head, Preview, Body, Container, Text, Section, Link } from '@react-email/components'\n\nexport type CustomerInvitationEmailCopy = {\n preview: string\n title: string\n body: string\n cta: string\n hint: string\n}\n\nexport type CustomerInvitationEmailProps = {\n inviteUrl: string\n copy: CustomerInvitationEmailCopy\n}\n\nexport function CustomerInvitationEmail({ inviteUrl, copy }: CustomerInvitationEmailProps) {\n return (\n <Html>\n <Head />\n <Preview>{copy.preview}</Preview>\n <Body style={body}>\n <Container style={container}>\n <Section>\n <Text style={title}>{copy.title}</Text>\n <Text style={paragraph}>{copy.body}</Text>\n <Text>\n <Link href={inviteUrl} style={button}>{copy.cta}</Link>\n </Text>\n <Text style={hint}>{copy.hint}</Text>\n </Section>\n </Container>\n </Body>\n </Html>\n )\n}\n\nconst body: React.CSSProperties = { backgroundColor: '#f9fafb', margin: 0, padding: '24px 0' }\nconst container: React.CSSProperties = { backgroundColor: '#ffffff', borderRadius: 12, padding: 24, margin: '0 auto', maxWidth: 520, boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }\nconst title: React.CSSProperties = { fontSize: 20, fontWeight: 600, color: '#111827', margin: '0 0 12px' }\nconst paragraph: React.CSSProperties = { fontSize: 14, color: '#4b5563', lineHeight: '20px', margin: '0 0 16px' }\nconst button: React.CSSProperties = { display: 'inline-block', backgroundColor: '#111827', color: '#ffffff', padding: '10px 14px', borderRadius: 8, textDecoration: 'none', fontSize: 14 }\nconst hint: React.CSSProperties = { fontSize: 12, color: '#6b7280', marginTop: 16 }\n\nexport default CustomerInvitationEmail\n"],
|
|
5
|
+
"mappings": "AAmBM,cAII,YAJJ;AAlBN,SAAS,MAAM,MAAM,SAAS,MAAM,WAAW,MAAM,SAAS,YAAY;AAenE,SAAS,wBAAwB,EAAE,WAAW,KAAK,GAAiC;AACzF,SACE,qBAAC,QACC;AAAA,wBAAC,QAAK;AAAA,IACN,oBAAC,WAAS,eAAK,SAAQ;AAAA,IACvB,oBAAC,QAAK,OAAO,MACX,8BAAC,aAAU,OAAO,WAChB,+BAAC,WACC;AAAA,0BAAC,QAAK,OAAO,OAAQ,eAAK,OAAM;AAAA,MAChC,oBAAC,QAAK,OAAO,WAAY,eAAK,MAAK;AAAA,MACnC,oBAAC,QACC,8BAAC,QAAK,MAAM,WAAW,OAAO,QAAS,eAAK,KAAI,GAClD;AAAA,MACA,oBAAC,QAAK,OAAO,MAAO,eAAK,MAAK;AAAA,OAChC,GACF,GACF;AAAA,KACF;AAEJ;AAEA,MAAM,OAA4B,EAAE,iBAAiB,WAAW,QAAQ,GAAG,SAAS,SAAS;AAC7F,MAAM,YAAiC,EAAE,iBAAiB,WAAW,cAAc,IAAI,SAAS,IAAI,QAAQ,UAAU,UAAU,KAAK,WAAW,6BAA6B;AAC7K,MAAM,QAA6B,EAAE,UAAU,IAAI,YAAY,KAAK,OAAO,WAAW,QAAQ,WAAW;AACzG,MAAM,YAAiC,EAAE,UAAU,IAAI,OAAO,WAAW,YAAY,QAAQ,QAAQ,WAAW;AAChH,MAAM,SAA8B,EAAE,SAAS,gBAAgB,iBAAiB,WAAW,OAAO,WAAW,SAAS,aAAa,cAAc,GAAG,gBAAgB,QAAQ,UAAU,GAAG;AACzL,MAAM,OAA4B,EAAE,UAAU,IAAI,OAAO,WAAW,WAAW,GAAG;AAElF,IAAO,kCAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
2
|
async function isOwnedCompanyEntity(em, customerEntityId, scope) {
|
|
3
|
+
return isOwnedEntityOfKind(em, customerEntityId, "company", scope);
|
|
4
|
+
}
|
|
5
|
+
async function isOwnedPersonEntity(em, personEntityId, scope) {
|
|
6
|
+
return isOwnedEntityOfKind(em, personEntityId, "person", scope);
|
|
7
|
+
}
|
|
8
|
+
async function resolveOwnedCompanyForPerson(em, personEntityId, scope) {
|
|
9
|
+
const { CustomerPersonProfile } = await import("@open-mercato/core/modules/customers/data/entities");
|
|
10
|
+
const profile = await em.findOne(CustomerPersonProfile, {
|
|
11
|
+
entity: personEntityId,
|
|
12
|
+
tenantId: scope.tenantId,
|
|
13
|
+
organizationId: scope.organizationId
|
|
14
|
+
});
|
|
15
|
+
const candidate = readCompanyEntityId(profile);
|
|
16
|
+
if (!candidate) return null;
|
|
17
|
+
return await isOwnedCompanyEntity(em, candidate, scope) ? candidate : null;
|
|
18
|
+
}
|
|
19
|
+
function readCompanyEntityId(profile) {
|
|
20
|
+
const company = profile?.company;
|
|
21
|
+
if (!company) return null;
|
|
22
|
+
if (typeof company === "string") return company;
|
|
23
|
+
const id = company.id;
|
|
24
|
+
return typeof id === "string" ? id : null;
|
|
25
|
+
}
|
|
26
|
+
async function isOwnedEntityOfKind(em, entityId, kind, scope) {
|
|
3
27
|
const { CustomerEntity } = await import("@open-mercato/core/modules/customers/data/entities");
|
|
4
28
|
const entity = await findOneWithDecryption(
|
|
5
29
|
em,
|
|
6
30
|
CustomerEntity,
|
|
7
31
|
{
|
|
8
|
-
id:
|
|
32
|
+
id: entityId,
|
|
9
33
|
tenantId: scope.tenantId,
|
|
10
34
|
organizationId: scope.organizationId,
|
|
11
|
-
kind
|
|
35
|
+
kind,
|
|
12
36
|
deletedAt: null
|
|
13
37
|
},
|
|
14
38
|
void 0,
|
|
@@ -17,6 +41,8 @@ async function isOwnedCompanyEntity(em, customerEntityId, scope) {
|
|
|
17
41
|
return !!entity;
|
|
18
42
|
}
|
|
19
43
|
export {
|
|
20
|
-
isOwnedCompanyEntity
|
|
44
|
+
isOwnedCompanyEntity,
|
|
45
|
+
isOwnedPersonEntity,
|
|
46
|
+
resolveOwnedCompanyForPerson
|
|
21
47
|
};
|
|
22
48
|
//# sourceMappingURL=customerEntityOwnership.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerEntityOwnership.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport type CustomerEntityScope = {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n}\n\n/**\n * Confirms that a caller-supplied `customerEntityId` (the CRM company FK linked\n * onto a customer user) exists, is a company, is not soft-deleted, and belongs\n * to the caller's tenant and organization.\n *\n * Without this check an admin in org A could pass a company UUID from org B in\n * the same tenant and cross-link a customer user into another org's portal\n * context (#2693). The lookup is org-scoped to match how `CustomerEntity` rows\n * are created and listed.\n */\nexport async function isOwnedCompanyEntity(\n em: EntityManager,\n customerEntityId: string,\n scope: CustomerEntityScope,\n): Promise<boolean> {\n const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')\n const entity = await findOneWithDecryption(\n em,\n CustomerEntity,\n {\n id:
|
|
5
|
-
"mappings": "AACA,SAAS,6BAA6B;AAiBtC,eAAsB,qBACpB,IACA,kBACA,OACkB;AAClB,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,oDAAoD;AAC5F,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport type CustomerEntityScope = {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n}\n\n/**\n * Confirms that a caller-supplied `customerEntityId` (the CRM company FK linked\n * onto a customer user) exists, is a company, is not soft-deleted, and belongs\n * to the caller's tenant and organization.\n *\n * Without this check an admin in org A could pass a company UUID from org B in\n * the same tenant and cross-link a customer user into another org's portal\n * context (#2693). The lookup is org-scoped to match how `CustomerEntity` rows\n * are created and listed.\n */\nexport async function isOwnedCompanyEntity(\n em: EntityManager,\n customerEntityId: string,\n scope: CustomerEntityScope,\n): Promise<boolean> {\n return isOwnedEntityOfKind(em, customerEntityId, 'company', scope)\n}\n\n/**\n * Confirms that a caller-supplied `personEntityId` (the CRM person FK linked\n * onto a customer user) exists, is a person, is not soft-deleted, and belongs\n * to the caller's tenant and organization.\n *\n * This is the symmetric guard to {@link isOwnedCompanyEntity}: the person FK is\n * copied onto the customer user on accept and short-circuits CRM auto-linking,\n * so an unvalidated id would permanently cross-link a portal user to another\n * org's person (the #4362/#2693 class of bug, on the person side).\n */\nexport async function isOwnedPersonEntity(\n em: EntityManager,\n personEntityId: string,\n scope: CustomerEntityScope,\n): Promise<boolean> {\n return isOwnedEntityOfKind(em, personEntityId, 'person', scope)\n}\n\n/**\n * Resolves the CRM company a person belongs to, but only when both the profile\n * lookup and the company itself stay inside the given tenant/organization scope.\n * Returns null when the person has no company or that company is out of scope.\n *\n * Both the invite route and the CRM auto-link subscriber need this answer, and\n * both write the result into `customerEntityId` \u2014 the portal company scope key \u2014\n * so they must agree on what counts as an in-scope company.\n */\nexport async function resolveOwnedCompanyForPerson(\n em: EntityManager,\n personEntityId: string,\n scope: CustomerEntityScope,\n): Promise<string | null> {\n const { CustomerPersonProfile } = await import('@open-mercato/core/modules/customers/data/entities')\n const profile = await em.findOne(CustomerPersonProfile as any, {\n entity: personEntityId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as any)\n const candidate = readCompanyEntityId(profile)\n if (!candidate) return null\n return (await isOwnedCompanyEntity(em, candidate, scope)) ? candidate : null\n}\n\n/**\n * `CustomerPersonProfile.company` is a relation on `company_entity_id`, not a\n * scalar \u2014 MikroORM hands back either the raw id or an entity reference, never a\n * `companyEntityId` property. Reading the wrong one silently yields undefined and\n * turns every company recovery into a clear.\n */\nfunction readCompanyEntityId(profile: unknown): string | null {\n const company = (profile as { company?: unknown } | null | undefined)?.company\n if (!company) return null\n if (typeof company === 'string') return company\n const id = (company as { id?: unknown }).id\n return typeof id === 'string' ? id : null\n}\n\nasync function isOwnedEntityOfKind(\n em: EntityManager,\n entityId: string,\n kind: 'company' | 'person',\n scope: CustomerEntityScope,\n): Promise<boolean> {\n const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')\n const entity = await findOneWithDecryption(\n em,\n CustomerEntity,\n {\n id: entityId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n return !!entity\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,6BAA6B;AAiBtC,eAAsB,qBACpB,IACA,kBACA,OACkB;AAClB,SAAO,oBAAoB,IAAI,kBAAkB,WAAW,KAAK;AACnE;AAYA,eAAsB,oBACpB,IACA,gBACA,OACkB;AAClB,SAAO,oBAAoB,IAAI,gBAAgB,UAAU,KAAK;AAChE;AAWA,eAAsB,6BACpB,IACA,gBACA,OACwB;AACxB,QAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,oDAAoD;AACnG,QAAM,UAAU,MAAM,GAAG,QAAQ,uBAA8B;AAAA,IAC7D,QAAQ;AAAA,IACR,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAQ;AACR,QAAM,YAAY,oBAAoB,OAAO;AAC7C,MAAI,CAAC,UAAW,QAAO;AACvB,SAAQ,MAAM,qBAAqB,IAAI,WAAW,KAAK,IAAK,YAAY;AAC1E;AAQA,SAAS,oBAAoB,SAAiC;AAC5D,QAAM,UAAW,SAAsD;AACvE,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,QAAM,KAAM,QAA6B;AACzC,SAAO,OAAO,OAAO,WAAW,KAAK;AACvC;AAEA,eAAe,oBACb,IACA,UACA,MACA,OACkB;AAClB,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,oDAAoD;AAC5F,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB;AAAA,MACA,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACnE;AACA,SAAO,CAAC,CAAC;AACX;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { Organization } from "@open-mercato/core/modules/directory/data/entities";
|
|
2
3
|
function platformBaseUrl() {
|
|
3
4
|
const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim();
|
|
4
5
|
if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\/+$/, "");
|
|
@@ -13,6 +14,23 @@ function resolveContainer(container) {
|
|
|
13
14
|
if (container) return Promise.resolve(container);
|
|
14
15
|
return createRequestContainer();
|
|
15
16
|
}
|
|
17
|
+
async function resolveOrgSlug(container, orgId) {
|
|
18
|
+
try {
|
|
19
|
+
const orgService = container.resolve("orgService");
|
|
20
|
+
if (orgService && typeof orgService.findById === "function") {
|
|
21
|
+
const org = await orgService.findById(orgId);
|
|
22
|
+
return org?.slug ?? null;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const em = container.resolve("em");
|
|
28
|
+
const org = await em.findOne(Organization, { id: orgId, deletedAt: null });
|
|
29
|
+
return org?.slug ?? null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
async function urlForCustomerOrg(orgId, path, options) {
|
|
17
35
|
const safePath = path.startsWith("/") ? path : `/${path}`;
|
|
18
36
|
const container = await resolveContainer(options?.container);
|
|
@@ -28,16 +46,7 @@ async function urlForCustomerOrg(orgId, path, options) {
|
|
|
28
46
|
if (active && active.hostname) {
|
|
29
47
|
return `https://${active.hostname}${safePath}`;
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const orgService = container.resolve("orgService");
|
|
34
|
-
if (orgService && typeof orgService.findById === "function") {
|
|
35
|
-
const org = await orgService.findById(orgId);
|
|
36
|
-
orgSlug = org?.slug ?? null;
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
orgSlug = null;
|
|
40
|
-
}
|
|
49
|
+
const orgSlug = await resolveOrgSlug(container, orgId);
|
|
41
50
|
const base = platformBaseUrl();
|
|
42
51
|
if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`;
|
|
43
52
|
return `${base}${safePath}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerUrl.ts"],
|
|
4
|
-
"sourcesContent": ["// Build customer-portal URLs that honor the active custom domain when present.\n// All customer-facing email senders (signup welcome, magic link, password reset,\n// notification digests) MUST construct links via this helper instead of\n// hard-coding PLATFORM_PORTAL_BASE_URL \u2014 see customer_accounts/AGENTS.md.\n\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\n\ntype DomainMappingService = {\n resolveActiveByOrg(orgId: string): Promise<{ hostname: string } | null>\n}\n\ntype OrgService = {\n findById(orgId: string): Promise<{ id: string; slug: string | null } | null>\n}\n\nfunction platformBaseUrl(): string {\n const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim()\n if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\\/+$/, '')\n // Outbound emails (welcome, magic link, password reset) embed this URL \u2014\n // localhost in production silently sends broken links to real customers.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'PLATFORM_PORTAL_BASE_URL is required in production. Configure it before sending customer-portal emails.',\n )\n }\n return 'http://localhost:3000'\n}\n\nfunction resolveContainer(container?: AppContainer): Promise<AppContainer> {\n if (container) return Promise.resolve(container)\n return createRequestContainer()\n}\n\nexport async function urlForCustomerOrg(\n orgId: string,\n path: string,\n options?: { container?: AppContainer },\n): Promise<string> {\n const safePath = path.startsWith('/') ? path : `/${path}`\n const container = await resolveContainer(options?.container)\n\n let active: { hostname: string } | null = null\n try {\n const service = container.resolve('domainMappingService') as DomainMappingService\n if (service && typeof service.resolveActiveByOrg === 'function') {\n active = await service.resolveActiveByOrg(orgId)\n }\n } catch {\n // domainMappingService not registered yet (fresh installs, tests) \u2014 fall through to platform URL.\n active = null\n }\n\n if (active && active.hostname) {\n return `https://${active.hostname}${safePath}`\n }\n\n
|
|
5
|
-
"mappings": "AAMA,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["// Build customer-portal URLs that honor the active custom domain when present.\n// All customer-facing email senders (signup welcome, magic link, password reset,\n// notification digests) MUST construct links via this helper instead of\n// hard-coding PLATFORM_PORTAL_BASE_URL \u2014 see customer_accounts/AGENTS.md.\n\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\ntype DomainMappingService = {\n resolveActiveByOrg(orgId: string): Promise<{ hostname: string } | null>\n}\n\ntype OrgService = {\n findById(orgId: string): Promise<{ id: string; slug: string | null } | null>\n}\n\ntype EntityManagerResolver = {\n resolve(name: 'em'): EntityManager\n}\n\nfunction platformBaseUrl(): string {\n const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim()\n if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\\/+$/, '')\n // Outbound emails (welcome, magic link, password reset) embed this URL \u2014\n // localhost in production silently sends broken links to real customers.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'PLATFORM_PORTAL_BASE_URL is required in production. Configure it before sending customer-portal emails.',\n )\n }\n return 'http://localhost:3000'\n}\n\nfunction resolveContainer(container?: AppContainer): Promise<AppContainer> {\n if (container) return Promise.resolve(container)\n return createRequestContainer()\n}\n\nasync function resolveOrgSlug(container: AppContainer, orgId: string): Promise<string | null> {\n try {\n const orgService = container.resolve('orgService') as OrgService | undefined\n if (orgService && typeof orgService.findById === 'function') {\n const org = await orgService.findById(orgId)\n return org?.slug ?? null\n }\n } catch {\n // orgService is optional in standalone/runtime contexts \u2014 fall through to EntityManager.\n }\n\n try {\n const em = (container as AppContainer & EntityManagerResolver).resolve('em')\n const org = await em.findOne(Organization, { id: orgId, deletedAt: null })\n return org?.slug ?? null\n } catch {\n return null\n }\n}\n\nexport async function urlForCustomerOrg(\n orgId: string,\n path: string,\n options?: { container?: AppContainer },\n): Promise<string> {\n const safePath = path.startsWith('/') ? path : `/${path}`\n const container = await resolveContainer(options?.container)\n\n let active: { hostname: string } | null = null\n try {\n const service = container.resolve('domainMappingService') as DomainMappingService\n if (service && typeof service.resolveActiveByOrg === 'function') {\n active = await service.resolveActiveByOrg(orgId)\n }\n } catch {\n // domainMappingService not registered yet (fresh installs, tests) \u2014 fall through to platform URL.\n active = null\n }\n\n if (active && active.hostname) {\n return `https://${active.hostname}${safePath}`\n }\n\n const orgSlug = await resolveOrgSlug(container, orgId)\n const base = platformBaseUrl()\n if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`\n return `${base}${safePath}`\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAe7B,SAAS,kBAA0B;AACjC,QAAM,UAAU,QAAQ,IAAI,0BAA0B,KAAK;AAC3D,MAAI,WAAW,QAAQ,SAAS,EAAG,QAAO,QAAQ,QAAQ,QAAQ,EAAE;AAGpE,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,WAAiD;AACzE,MAAI,UAAW,QAAO,QAAQ,QAAQ,SAAS;AAC/C,SAAO,uBAAuB;AAChC;AAEA,eAAe,eAAe,WAAyB,OAAuC;AAC5F,MAAI;AACF,UAAM,aAAa,UAAU,QAAQ,YAAY;AACjD,QAAI,cAAc,OAAO,WAAW,aAAa,YAAY;AAC3D,YAAM,MAAM,MAAM,WAAW,SAAS,KAAK;AAC3C,aAAO,KAAK,QAAQ;AAAA,IACtB;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAM,KAAM,UAAmD,QAAQ,IAAI;AAC3E,UAAM,MAAM,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,OAAO,WAAW,KAAK,CAAC;AACzE,WAAO,KAAK,QAAQ;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,kBACpB,OACA,MACA,SACiB;AACjB,QAAM,WAAW,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AACvD,QAAM,YAAY,MAAM,iBAAiB,SAAS,SAAS;AAE3D,MAAI,SAAsC;AAC1C,MAAI;AACF,UAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,QAAI,WAAW,OAAO,QAAQ,uBAAuB,YAAY;AAC/D,eAAS,MAAM,QAAQ,mBAAmB,KAAK;AAAA,IACjD;AAAA,EACF,QAAQ;AAEN,aAAS;AAAA,EACX;AAEA,MAAI,UAAU,OAAO,UAAU;AAC7B,WAAO,WAAW,OAAO,QAAQ,GAAG,QAAQ;AAAA,EAC9C;AAEA,QAAM,UAAU,MAAM,eAAe,WAAW,KAAK;AACrD,QAAM,OAAO,gBAAgB;AAC7B,MAAI,QAAS,QAAO,GAAG,IAAI,IAAI,OAAO,UAAU,QAAQ;AACxD,SAAO,GAAG,IAAI,GAAG,QAAQ;AAC3B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
2
|
+
import { sendEmail } from "@open-mercato/shared/lib/email/send";
|
|
3
|
+
import CustomerInvitationEmail from "@open-mercato/core/modules/customer_accounts/emails/CustomerInvitationEmail";
|
|
4
|
+
import { urlForCustomerOrg } from "@open-mercato/core/modules/customer_accounts/lib/customerUrl";
|
|
5
|
+
async function sendCustomerInvitationEmail(input) {
|
|
6
|
+
const { translate } = await resolveTranslations();
|
|
7
|
+
const inviteUrl = await urlForCustomerOrg(
|
|
8
|
+
input.organizationId,
|
|
9
|
+
`/invite?token=${encodeURIComponent(input.rawToken)}`,
|
|
10
|
+
{ container: input.container }
|
|
11
|
+
);
|
|
12
|
+
const subject = translate("customer_accounts.invitation.email.subject", "You have been invited to the customer portal");
|
|
13
|
+
const copy = {
|
|
14
|
+
preview: translate("customer_accounts.invitation.email.preview", "Accept your invitation to finish setting up portal access."),
|
|
15
|
+
title: translate("customer_accounts.invitation.email.title", "You have been invited"),
|
|
16
|
+
body: translate(
|
|
17
|
+
"customer_accounts.invitation.email.body",
|
|
18
|
+
"Use this secure invitation link to create your password and access the customer portal."
|
|
19
|
+
),
|
|
20
|
+
cta: translate("customer_accounts.invitation.email.cta", "Accept invitation"),
|
|
21
|
+
hint: translate(
|
|
22
|
+
"customer_accounts.invitation.email.hint",
|
|
23
|
+
"This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email."
|
|
24
|
+
)
|
|
25
|
+
};
|
|
26
|
+
await sendEmail({
|
|
27
|
+
to: input.email,
|
|
28
|
+
subject,
|
|
29
|
+
react: CustomerInvitationEmail({ inviteUrl, copy })
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
sendCustomerInvitationEmail
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=invitationEmail.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/invitationEmail.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport CustomerInvitationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerInvitationEmail'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\n\nexport type CustomerInvitationEmailInput = {\n container: AppContainer\n organizationId: string\n email: string\n rawToken: string\n}\n\nexport async function sendCustomerInvitationEmail(input: CustomerInvitationEmailInput): Promise<void> {\n const { translate } = await resolveTranslations()\n const inviteUrl = await urlForCustomerOrg(\n input.organizationId,\n `/invite?token=${encodeURIComponent(input.rawToken)}`,\n { container: input.container },\n )\n\n const subject = translate('customer_accounts.invitation.email.subject', 'You have been invited to the customer portal')\n const copy = {\n preview: translate('customer_accounts.invitation.email.preview', 'Accept your invitation to finish setting up portal access.'),\n title: translate('customer_accounts.invitation.email.title', 'You have been invited'),\n body: translate(\n 'customer_accounts.invitation.email.body',\n 'Use this secure invitation link to create your password and access the customer portal.',\n ),\n cta: translate('customer_accounts.invitation.email.cta', 'Accept invitation'),\n hint: translate(\n 'customer_accounts.invitation.email.hint',\n 'This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: input.email,\n subject,\n react: CustomerInvitationEmail({ inviteUrl, copy }),\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAC1B,OAAO,6BAA6B;AACpC,SAAS,yBAAyB;AASlC,eAAsB,4BAA4B,OAAoD;AACpG,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,YAAY,MAAM;AAAA,IACtB,MAAM;AAAA,IACN,iBAAiB,mBAAmB,MAAM,QAAQ,CAAC;AAAA,IACnD,EAAE,WAAW,MAAM,UAAU;AAAA,EAC/B;AAEA,QAAM,UAAU,UAAU,8CAA8C,8CAA8C;AACtH,QAAM,OAAO;AAAA,IACX,SAAS,UAAU,8CAA8C,4DAA4D;AAAA,IAC7H,OAAO,UAAU,4CAA4C,uBAAuB;AAAA,IACpF,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA,KAAK,UAAU,0CAA0C,mBAAmB;AAAA,IAC5E,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,IAAI,MAAM;AAAA,IACV;AAAA,IACA,OAAO,wBAAwB,EAAE,WAAW,KAAK,CAAC;AAAA,EACpD,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260723120000_customer_accounts extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`alter table "customer_user_invitations" add column "person_entity_id" uuid null;`);
|
|
5
|
+
}
|
|
6
|
+
async down() {
|
|
7
|
+
this.addSql(`alter table "customer_user_invitations" drop column "person_entity_id";`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
Migration20260723120000_customer_accounts
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=Migration20260723120000_customer_accounts.js.map
|
package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260723120000_customer_accounts extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"customer_user_invitations\" add column \"person_entity_id\" uuid null;`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`alter table \"customer_user_invitations\" drop column \"person_entity_id\";`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,kDAAkD,UAAU;AAAA,EAEvE,MAAe,KAAoB;AACjC,SAAK,OAAO,kFAAkF;AAAA,EAChG;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,yEAAyE;AAAA,EACvF;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -35,16 +35,28 @@ class CustomerInvitationService {
|
|
|
35
35
|
{ tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
36
36
|
);
|
|
37
37
|
if (existing) {
|
|
38
|
+
const rollbackState = {
|
|
39
|
+
email: existing.email,
|
|
40
|
+
token: existing.token,
|
|
41
|
+
customerEntityId: existing.customerEntityId ?? null,
|
|
42
|
+
personEntityId: existing.personEntityId ?? null,
|
|
43
|
+
roleIdsJson: [...existing.roleIdsJson ?? []],
|
|
44
|
+
invitedByUserId: existing.invitedByUserId ?? null,
|
|
45
|
+
invitedByCustomerUserId: existing.invitedByCustomerUserId ?? null,
|
|
46
|
+
displayName: existing.displayName ?? null,
|
|
47
|
+
expiresAt: new Date(existing.expiresAt)
|
|
48
|
+
};
|
|
38
49
|
existing.email = normalizedEmail;
|
|
39
50
|
existing.token = tokenHashed;
|
|
40
51
|
existing.customerEntityId = options.customerEntityId || null;
|
|
52
|
+
existing.personEntityId = options.personEntityId || null;
|
|
41
53
|
existing.roleIdsJson = options.roleIds;
|
|
42
54
|
existing.invitedByUserId = options.invitedByUserId || null;
|
|
43
55
|
existing.invitedByCustomerUserId = options.invitedByCustomerUserId || null;
|
|
44
56
|
existing.displayName = options.displayName || null;
|
|
45
57
|
existing.expiresAt = expiresAt;
|
|
46
58
|
await this.em.flush();
|
|
47
|
-
return { invitation: existing, rawToken: token };
|
|
59
|
+
return { invitation: existing, rawToken: token, reused: true, rollbackState };
|
|
48
60
|
}
|
|
49
61
|
const invitation = this.em.create(CustomerUserInvitation, {
|
|
50
62
|
tenantId: scope.tenantId,
|
|
@@ -53,6 +65,7 @@ class CustomerInvitationService {
|
|
|
53
65
|
emailHash,
|
|
54
66
|
token: tokenHashed,
|
|
55
67
|
customerEntityId: options.customerEntityId || null,
|
|
68
|
+
personEntityId: options.personEntityId || null,
|
|
56
69
|
roleIdsJson: options.roleIds,
|
|
57
70
|
invitedByUserId: options.invitedByUserId || null,
|
|
58
71
|
invitedByCustomerUserId: options.invitedByCustomerUserId || null,
|
|
@@ -61,7 +74,23 @@ class CustomerInvitationService {
|
|
|
61
74
|
createdAt: /* @__PURE__ */ new Date()
|
|
62
75
|
});
|
|
63
76
|
await this.em.persist(invitation).flush();
|
|
64
|
-
return { invitation, rawToken: token };
|
|
77
|
+
return { invitation, rawToken: token, reused: false, rollbackState: null };
|
|
78
|
+
}
|
|
79
|
+
async rollbackInvitation(invitation, rollbackState) {
|
|
80
|
+
if (!rollbackState) {
|
|
81
|
+
await this.em.remove(invitation).flush();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
invitation.email = rollbackState.email;
|
|
85
|
+
invitation.token = rollbackState.token;
|
|
86
|
+
invitation.customerEntityId = rollbackState.customerEntityId;
|
|
87
|
+
invitation.personEntityId = rollbackState.personEntityId;
|
|
88
|
+
invitation.roleIdsJson = [...rollbackState.roleIdsJson];
|
|
89
|
+
invitation.invitedByUserId = rollbackState.invitedByUserId;
|
|
90
|
+
invitation.invitedByCustomerUserId = rollbackState.invitedByCustomerUserId;
|
|
91
|
+
invitation.displayName = rollbackState.displayName;
|
|
92
|
+
invitation.expiresAt = new Date(rollbackState.expiresAt);
|
|
93
|
+
await this.em.flush();
|
|
65
94
|
}
|
|
66
95
|
async findByToken(token) {
|
|
67
96
|
const tokenHashed = hashToken(token);
|
|
@@ -89,6 +118,7 @@ class CustomerInvitationService {
|
|
|
89
118
|
tenantId: invitation.tenantId,
|
|
90
119
|
organizationId: invitation.organizationId,
|
|
91
120
|
customerEntityId: invitation.customerEntityId || null,
|
|
121
|
+
personEntityId: invitation.personEntityId || null,
|
|
92
122
|
isActive: true,
|
|
93
123
|
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
94
124
|
// Invitation implicitly verifies email
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/services/customerInvitationService.ts"],
|
|
4
|
-
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash } from 'bcryptjs'\nimport {\n CustomerUser,\n CustomerUserInvitation,\n CustomerUserRole,\n CustomerRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\nimport { hashForLookup } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst INVITATION_TTL_MS = 72 * 60 * 60 * 1000 // 72 hours\n\nexport class CustomerInvitationService {\n constructor(private em: EntityManager) {}\n\n async createInvitation(\n email: string,\n scope: { tenantId: string; organizationId: string },\n options: {\n customerEntityId?: string | null\n roleIds: string[]\n invitedByUserId?: string | null\n invitedByCustomerUserId?: string | null\n displayName?: string | null\n },\n ): Promise<{
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB,iBAAiB;AAC/C,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,0BAA0B;AAE1D,MAAM,cAAc;AACpB,MAAM,oBAAoB,KAAK,KAAK,KAAK;
|
|
4
|
+
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash } from 'bcryptjs'\nimport {\n CustomerUser,\n CustomerUserInvitation,\n CustomerUserRole,\n CustomerRole,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\nimport { hashForLookup } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst INVITATION_TTL_MS = 72 * 60 * 60 * 1000 // 72 hours\n\nexport type CustomerInvitationRollbackState = {\n email: string\n token: string\n customerEntityId: string | null\n personEntityId: string | null\n roleIdsJson: string[]\n invitedByUserId: string | null\n invitedByCustomerUserId: string | null\n displayName: string | null\n expiresAt: Date\n}\n\nexport class CustomerInvitationService {\n constructor(private em: EntityManager) {}\n\n async createInvitation(\n email: string,\n scope: { tenantId: string; organizationId: string },\n options: {\n customerEntityId?: string | null\n personEntityId?: string | null\n roleIds: string[]\n invitedByUserId?: string | null\n invitedByCustomerUserId?: string | null\n displayName?: string | null\n },\n ): Promise<{\n invitation: CustomerUserInvitation\n rawToken: string\n reused: boolean\n rollbackState: CustomerInvitationRollbackState | null\n }> {\n const token = generateSecureToken()\n const emailHash = hashForLookup(email)\n const normalizedEmail = email.toLowerCase().trim()\n const expiresAt = new Date(Date.now() + INVITATION_TTL_MS)\n const tokenHashed = hashToken(token)\n\n // Dedupe: reuse an existing pending (not accepted, not cancelled, unexpired)\n // invitation for the same recipient instead of inserting a new row. This caps\n // row/token growth and keeps a single live token per concurrently-pending\n // (tenant, organization, email) tuple.\n const existing = await findOneWithDecryption(\n this.em,\n CustomerUserInvitation,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n emailHash,\n acceptedAt: null,\n cancelledAt: null,\n expiresAt: { $gt: new Date() },\n } as any,\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n if (existing) {\n const rollbackState: CustomerInvitationRollbackState = {\n email: existing.email,\n token: existing.token,\n customerEntityId: existing.customerEntityId ?? null,\n personEntityId: existing.personEntityId ?? null,\n roleIdsJson: [...(existing.roleIdsJson ?? [])],\n invitedByUserId: existing.invitedByUserId ?? null,\n invitedByCustomerUserId: existing.invitedByCustomerUserId ?? null,\n displayName: existing.displayName ?? null,\n expiresAt: new Date(existing.expiresAt),\n }\n existing.email = normalizedEmail\n existing.token = tokenHashed\n existing.customerEntityId = options.customerEntityId || null\n existing.personEntityId = options.personEntityId || null\n existing.roleIdsJson = options.roleIds\n existing.invitedByUserId = options.invitedByUserId || null\n existing.invitedByCustomerUserId = options.invitedByCustomerUserId || null\n existing.displayName = options.displayName || null\n existing.expiresAt = expiresAt\n await this.em.flush()\n return { invitation: existing, rawToken: token, reused: true, rollbackState }\n }\n\n const invitation = this.em.create(CustomerUserInvitation, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n email: normalizedEmail,\n emailHash,\n token: tokenHashed,\n customerEntityId: options.customerEntityId || null,\n personEntityId: options.personEntityId || null,\n roleIdsJson: options.roleIds,\n invitedByUserId: options.invitedByUserId || null,\n invitedByCustomerUserId: options.invitedByCustomerUserId || null,\n displayName: options.displayName || null,\n expiresAt,\n createdAt: new Date(),\n } as any) as CustomerUserInvitation\n await this.em.persist(invitation).flush()\n return { invitation, rawToken: token, reused: false, rollbackState: null }\n }\n\n async rollbackInvitation(\n invitation: CustomerUserInvitation,\n rollbackState: CustomerInvitationRollbackState | null,\n ): Promise<void> {\n if (!rollbackState) {\n await this.em.remove(invitation).flush()\n return\n }\n\n invitation.email = rollbackState.email\n invitation.token = rollbackState.token\n invitation.customerEntityId = rollbackState.customerEntityId\n invitation.personEntityId = rollbackState.personEntityId\n invitation.roleIdsJson = [...rollbackState.roleIdsJson]\n invitation.invitedByUserId = rollbackState.invitedByUserId\n invitation.invitedByCustomerUserId = rollbackState.invitedByCustomerUserId\n invitation.displayName = rollbackState.displayName\n invitation.expiresAt = new Date(rollbackState.expiresAt)\n await this.em.flush()\n }\n\n async findByToken(token: string): Promise<CustomerUserInvitation | null> {\n const tokenHashed = hashToken(token)\n const invitation = await findOneWithDecryption(\n this.em,\n CustomerUserInvitation,\n { token: tokenHashed } as any,\n )\n if (!invitation) return null\n if (invitation.acceptedAt) return null\n if (invitation.cancelledAt) return null\n if (invitation.expiresAt.getTime() < Date.now()) return null\n return invitation\n }\n\n async acceptInvitation(\n token: string,\n password: string,\n displayName: string,\n ): Promise<{ user: CustomerUser; invitation: CustomerUserInvitation } | null> {\n const invitation = await this.findByToken(token)\n if (!invitation) return null\n\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(invitation.email)\n\n // Create user\n const user = this.em.create(CustomerUser, {\n email: invitation.email,\n emailHash,\n passwordHash,\n displayName: displayName || invitation.displayName || invitation.email,\n tenantId: invitation.tenantId,\n organizationId: invitation.organizationId,\n customerEntityId: invitation.customerEntityId || null,\n personEntityId: invitation.personEntityId || null,\n isActive: true,\n emailVerifiedAt: new Date(), // Invitation implicitly verifies email\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any) as CustomerUser\n this.em.persist(user)\n\n // Assign roles\n const roleIds = Array.isArray(invitation.roleIdsJson) ? invitation.roleIdsJson : []\n const roles = roleIds.length > 0\n ? await findWithDecryption(\n this.em,\n CustomerRole,\n {\n id: { $in: roleIds } as any,\n tenantId: invitation.tenantId,\n organizationId: invitation.organizationId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId: invitation.tenantId, organizationId: invitation.organizationId },\n )\n : []\n for (const role of roles) {\n const userRole = this.em.create(CustomerUserRole, {\n user,\n role,\n createdAt: new Date(),\n } as any)\n this.em.persist(userRole)\n }\n\n // Mark invitation as accepted\n invitation.acceptedAt = new Date()\n\n await this.em.flush()\n return { user, invitation }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CustomerUser } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
2
2
|
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { resolveOwnedCompanyForPerson } from "@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership";
|
|
3
4
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
5
|
const logger = createLogger("customer_accounts").child({ component: "auto-link-crm" });
|
|
5
6
|
const metadata = {
|
|
@@ -17,14 +18,51 @@ async function handle(payload, ctx) {
|
|
|
17
18
|
try {
|
|
18
19
|
const user = await findOneWithDecryption(em, CustomerUser, { id: userId, tenantId, deletedAt: null }, void 0, { tenantId, organizationId });
|
|
19
20
|
if (!user) return;
|
|
20
|
-
|
|
21
|
+
const { CustomerEntity } = await import("@open-mercato/core/modules/customers/data/entities");
|
|
22
|
+
const ownScope = { tenantId, organizationId: user.organizationId };
|
|
23
|
+
if (user.customerEntityId) {
|
|
24
|
+
const linked = await findOneWithDecryption(
|
|
25
|
+
em,
|
|
26
|
+
CustomerEntity,
|
|
27
|
+
{ id: user.customerEntityId, tenantId, organizationId: user.organizationId, deletedAt: null },
|
|
28
|
+
void 0,
|
|
29
|
+
{ tenantId, organizationId }
|
|
30
|
+
);
|
|
31
|
+
if (!linked) {
|
|
32
|
+
await em.nativeUpdate(
|
|
33
|
+
CustomerUser,
|
|
34
|
+
{ id: userId, tenantId, organizationId: user.organizationId },
|
|
35
|
+
{ customerEntityId: null }
|
|
36
|
+
);
|
|
37
|
+
user.customerEntityId = null;
|
|
38
|
+
} else if (linked.kind === "person") {
|
|
39
|
+
const replacement = await resolveOwnedCompanyForPerson(em, user.customerEntityId, ownScope);
|
|
40
|
+
await em.nativeUpdate(
|
|
41
|
+
CustomerUser,
|
|
42
|
+
{ id: userId, tenantId, organizationId: user.organizationId },
|
|
43
|
+
{ customerEntityId: replacement }
|
|
44
|
+
);
|
|
45
|
+
user.customerEntityId = replacement;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (user.personEntityId) {
|
|
49
|
+
if (user.customerEntityId) return;
|
|
50
|
+
const recovered = await resolveOwnedCompanyForPerson(em, user.personEntityId, ownScope);
|
|
51
|
+
if (recovered) {
|
|
52
|
+
await em.nativeUpdate(
|
|
53
|
+
CustomerUser,
|
|
54
|
+
{ id: userId, tenantId, organizationId: user.organizationId },
|
|
55
|
+
{ customerEntityId: recovered }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
21
60
|
const email = (data?.email ? data.email : user.email)?.toLowerCase().trim();
|
|
22
61
|
if (!email) return;
|
|
23
|
-
const { CustomerEntity } = await import("@open-mercato/core/modules/customers/data/entities");
|
|
24
62
|
const personEntities = await findWithDecryption(
|
|
25
63
|
em,
|
|
26
64
|
CustomerEntity,
|
|
27
|
-
{ tenantId, kind: "person", deletedAt: null },
|
|
65
|
+
{ tenantId, organizationId: user.organizationId, kind: "person", deletedAt: null },
|
|
28
66
|
{ limit: 500 },
|
|
29
67
|
{ tenantId, organizationId }
|
|
30
68
|
);
|
|
@@ -32,14 +70,16 @@ async function handle(payload, ctx) {
|
|
|
32
70
|
(e) => e.primaryEmail && e.primaryEmail.toLowerCase().trim() === email
|
|
33
71
|
);
|
|
34
72
|
if (!matchingEntity) return;
|
|
35
|
-
const
|
|
36
|
-
const profile = await em.findOne(CustomerPersonProfile, { entity: matchingEntity.id });
|
|
37
|
-
const companyEntityId = profile?.companyEntityId;
|
|
73
|
+
const companyEntityId = await resolveOwnedCompanyForPerson(em, matchingEntity.id, ownScope);
|
|
38
74
|
const updates = { personEntityId: matchingEntity.id };
|
|
39
75
|
if (companyEntityId) {
|
|
40
76
|
updates.customerEntityId = companyEntityId;
|
|
41
77
|
}
|
|
42
|
-
await em.nativeUpdate(
|
|
78
|
+
await em.nativeUpdate(
|
|
79
|
+
CustomerUser,
|
|
80
|
+
{ id: userId, tenantId, organizationId: user.organizationId },
|
|
81
|
+
updates
|
|
82
|
+
);
|
|
43
83
|
} catch (err) {
|
|
44
84
|
logger.error("Failed to link customer user to CRM person", { err });
|
|
45
85
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/subscribers/autoLinkCrm.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'auto-link-crm' })\n\nexport const metadata = {\n event: 'customer_accounts.user.created',\n persistent: true,\n id: 'customer_accounts:auto-link-crm',\n}\n\nexport default async function handle(\n payload: unknown,\n ctx: { resolve: <T = unknown>(name: string) => T; eventName?: string },\n): Promise<void> {\n const data = payload as Record<string, unknown>\n const userId = data?.id as string\n const tenantId = data?.tenantId as string\n const organizationId = data?.organizationId as string | undefined\n if (!userId || !tenantId) return\n\n const em = ctx.resolve<EntityManager>('em')\n\n try {\n const user = await findOneWithDecryption(em, CustomerUser, { id: userId, tenantId, deletedAt: null }, undefined, { tenantId, organizationId })\n if (!user) return\n if (user.personEntityId) return\n\n const email = (data?.email ? (data.email as string) : user.email)?.toLowerCase().trim()\n if (!email) return\n\n
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,mBAAmB,EAAE,MAAM,EAAE,WAAW,gBAAgB,CAAC;AAE9E,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,OACL,SACA,KACe;AACf,QAAM,OAAO;AACb,QAAM,SAAS,MAAM;AACrB,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,MAAM;AAC7B,MAAI,CAAC,UAAU,CAAC,SAAU;AAE1B,QAAM,KAAK,IAAI,QAAuB,IAAI;AAE1C,MAAI;AACF,UAAM,OAAO,MAAM,sBAAsB,IAAI,cAAc,EAAE,IAAI,QAAQ,UAAU,WAAW,KAAK,GAAG,QAAW,EAAE,UAAU,eAAe,CAAC;AAC7I,QAAI,CAAC,KAAM;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveOwnedCompanyForPerson } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'auto-link-crm' })\n\nexport const metadata = {\n event: 'customer_accounts.user.created',\n persistent: true,\n id: 'customer_accounts:auto-link-crm',\n}\n\nexport default async function handle(\n payload: unknown,\n ctx: { resolve: <T = unknown>(name: string) => T; eventName?: string },\n): Promise<void> {\n const data = payload as Record<string, unknown>\n const userId = data?.id as string\n const tenantId = data?.tenantId as string\n const organizationId = data?.organizationId as string | undefined\n if (!userId || !tenantId) return\n\n const em = ctx.resolve<EntityManager>('em')\n\n try {\n const user = await findOneWithDecryption(em, CustomerUser, { id: userId, tenantId, deletedAt: null }, undefined, { tenantId, organizationId })\n if (!user) return\n\n const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')\n\n // Every lookup and write below normalizes against the user's OWN org, not just\n // the tenant: customerEntityId is the portal's company scope key (portal user\n // and invite routes filter on it), so adopting an entity from another org would\n // widen the user's portal access instead of repairing it.\n const ownScope = { tenantId, organizationId: user.organizationId }\n\n // Defensive normalization for NEW users only \u2014 this subscriber runs on\n // `customer_accounts.user.created`, which is never re-emitted for an existing\n // row. customerEntityId is the CRM company FK, and earlier invite flows (#4362)\n // could poison it with a person entity id, which then breaks every later user\n // edit (\"Company not found\"). If the linked entity is a person (not a company)\n // or sits outside the user's org, drop it \u2014 or recover the person's real company\n // from their profile. Correct company links are left untouched. Rows created\n // before this change keep their bad FK; see #4473 for the backfill.\n if (user.customerEntityId) {\n const linked = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: user.customerEntityId, tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId, organizationId },\n ) as any\n if (!linked) {\n // Not resolvable inside the user's own org \u2014 a cross-org (or deleted)\n // entity. Clear it rather than leave it: this FK is the portal scope key.\n await em.nativeUpdate(\n CustomerUser,\n { id: userId, tenantId, organizationId: user.organizationId },\n { customerEntityId: null },\n )\n user.customerEntityId = null\n } else if (linked.kind === 'person') {\n // Only adopt a recovered company that is itself in the user's org.\n const replacement = await resolveOwnedCompanyForPerson(em, user.customerEntityId, ownScope)\n await em.nativeUpdate(\n CustomerUser,\n { id: userId, tenantId, organizationId: user.organizationId },\n { customerEntityId: replacement },\n )\n user.customerEntityId = replacement\n }\n }\n\n // A user invited from a CRM person card arrives with personEntityId already\n // set, so the email-matching path below never runs for it. Derive the company\n // FK from that person's own profile instead of leaving it null forever \u2014 the\n // portal Users page, portal invitations, and the company detail \"Portal users\"\n // group all key off customerEntityId (#4362).\n if (user.personEntityId) {\n if (user.customerEntityId) return\n const recovered = await resolveOwnedCompanyForPerson(em, user.personEntityId, ownScope)\n if (recovered) {\n await em.nativeUpdate(\n CustomerUser,\n { id: userId, tenantId, organizationId: user.organizationId },\n { customerEntityId: recovered },\n )\n }\n return\n }\n\n const email = (data?.email ? (data.email as string) : user.email)?.toLowerCase().trim()\n if (!email) return\n\n // Scope the candidate lookup to the user's own org. The fifth argument is a\n // decryption scope, not a filter, so without organizationId in the `where`\n // this matches people across every org in the tenant and writes a cross-org\n // link that the normalization block above just cleared.\n const personEntities = await findWithDecryption(\n em,\n CustomerEntity,\n { tenantId, organizationId: user.organizationId, kind: 'person', deletedAt: null } as any,\n { limit: 500 } as any,\n { tenantId, organizationId },\n )\n\n const matchingEntity = personEntities.find(\n (e: any) => e.primaryEmail && e.primaryEmail.toLowerCase().trim() === email,\n ) as any\n\n if (!matchingEntity) return\n\n const companyEntityId = await resolveOwnedCompanyForPerson(em, matchingEntity.id, ownScope)\n\n const updates: Record<string, unknown> = { personEntityId: matchingEntity.id }\n if (companyEntityId) {\n updates.customerEntityId = companyEntityId\n }\n\n await em.nativeUpdate(\n CustomerUser,\n { id: userId, tenantId, organizationId: user.organizationId },\n updates,\n )\n } catch (err) {\n logger.error('Failed to link customer user to CRM person', { err })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -94,7 +94,7 @@ function InviteForm({ personEntityId, onSuccess }) {
|
|
|
94
94
|
try {
|
|
95
95
|
await runMutation({
|
|
96
96
|
context: { entityType: "customer_accounts:user" },
|
|
97
|
-
mutationPayload: {
|
|
97
|
+
mutationPayload: { personEntityId, roleIds: selectedRoleIds },
|
|
98
98
|
operation: async () => {
|
|
99
99
|
const call = await apiCall(
|
|
100
100
|
"/api/customer_accounts/admin/users-invite",
|
|
@@ -105,7 +105,7 @@ function InviteForm({ personEntityId, onSuccess }) {
|
|
|
105
105
|
email: trimmedEmail,
|
|
106
106
|
roleIds: selectedRoleIds,
|
|
107
107
|
displayName: displayName.trim() || void 0,
|
|
108
|
-
|
|
108
|
+
personEntityId
|
|
109
109
|
})
|
|
110
110
|
}
|
|
111
111
|
);
|