@open-mercato/core 0.7.1-develop.7104.1.1dd769bf1a → 0.7.1-develop.7105.1.0d2f4c6651

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.
Files changed (20) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +6 -3
  3. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +2 -2
  4. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js +3 -1
  5. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js.map +2 -2
  6. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +4 -4
  7. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
  8. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +3 -1
  9. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  10. package/dist/modules/customer_accounts/lib/portalOrgSlug.js +22 -0
  11. package/dist/modules/customer_accounts/lib/portalOrgSlug.js.map +7 -0
  12. package/dist/modules/customer_accounts/lib/portalUrl.js +5 -2
  13. package/dist/modules/customer_accounts/lib/portalUrl.js.map +2 -2
  14. package/package.json +7 -7
  15. package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +15 -9
  16. package/src/modules/customer_accounts/backend/customer_accounts/settings/page.tsx +3 -1
  17. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +15 -12
  18. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +3 -1
  19. package/src/modules/customer_accounts/lib/portalOrgSlug.ts +40 -0
  20. package/src/modules/customer_accounts/lib/portalUrl.ts +14 -3
@@ -1,4 +1,4 @@
1
- [build:core] found 4362 entry points
1
+ [build:core] found 4363 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 218 entry points
4
4
  [build:core:generated] built successfully
@@ -11,10 +11,13 @@ const DEMO_CREDENTIALS = [
11
11
  { email: "bob.smith@example.com", password: "Password123!", role: "Buyer" },
12
12
  { email: "carol.white@example.com", password: "Password123!", role: "Viewer" }
13
13
  ];
14
- function CustomerAccountsSettingsPageClient({ portalOrigin }) {
14
+ function CustomerAccountsSettingsPageClient({ portalOrigin, portalOrgSlug = null }) {
15
15
  const t = useT();
16
16
  const portalUrl = useMemo(() => buildPortalUrlPattern(portalOrigin), [portalOrigin]);
17
- const portalRootUrl = useMemo(() => buildPortalRootUrl(portalOrigin), [portalOrigin]);
17
+ const portalRootUrl = useMemo(
18
+ () => portalOrgSlug ? buildPortalRootUrl(portalOrigin, portalOrgSlug) : null,
19
+ [portalOrigin, portalOrgSlug]
20
+ );
18
21
  return /* @__PURE__ */ jsxs(Fragment, { children: [
19
22
  /* @__PURE__ */ jsx("div", { className: "px-3 py-3 md:px-6 md:py-4", children: /* @__PURE__ */ jsx(
20
23
  FormHeader,
@@ -32,7 +35,7 @@ function CustomerAccountsSettingsPageClient({ portalOrigin }) {
32
35
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customer_accounts.settings.portal_access.url_label", "Portal URL pattern:") }),
33
36
  /* @__PURE__ */ jsx("code", { className: "mt-1 block rounded bg-muted px-3 py-2 text-sm font-mono", children: portalUrl })
34
37
  ] }),
35
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", asChild: true, children: /* @__PURE__ */ jsx("a", { href: portalRootUrl, target: "_blank", rel: "noopener noreferrer", children: t("customer_accounts.settings.portal_access.open_portal", "Open Portal") }) }) }),
38
+ portalRootUrl ? /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", asChild: true, children: /* @__PURE__ */ jsx("a", { href: portalRootUrl, target: "_blank", rel: "noopener noreferrer", children: t("customer_accounts.settings.portal_access.open_portal", "Open Portal") }) }) }) : null,
36
39
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
37
40
  "customer_accounts.settings.portal_access.slug_note",
38
41
  "The organization slug is auto-generated from the organization name. You can view and manage organizations in Directory \u2192 Organizations."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport { useMemo } from 'react'\nimport Link from 'next/link'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'\n\nconst DEMO_CREDENTIALS = [\n { email: 'alice.johnson@example.com', password: 'Password123!', role: 'Portal Admin' },\n { email: 'bob.smith@example.com', password: 'Password123!', role: 'Buyer' },\n { email: 'carol.white@example.com', password: 'Password123!', role: 'Viewer' },\n] as const\n\nexport type CustomerAccountsSettingsPageClientProps = {\n portalOrigin: string\n}\n\nexport function CustomerAccountsSettingsPageClient({ portalOrigin }: CustomerAccountsSettingsPageClientProps) {\n const t = useT()\n\n const portalUrl = useMemo(() => buildPortalUrlPattern(portalOrigin), [portalOrigin])\n const portalRootUrl = useMemo(() => buildPortalRootUrl(portalOrigin), [portalOrigin])\n\n return (\n <>\n <div className=\"px-3 py-3 md:px-6 md:py-4\">\n <FormHeader\n mode=\"detail\"\n title={t('customer_accounts.settings.title', 'Portal Settings')}\n backHref=\"/backend/customer_accounts/users\"\n backLabel={t('customer_accounts.settings.back', 'Customer Users')}\n />\n </div>\n\n <div className=\"max-w-2xl space-y-6\">\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.portal_access.title', 'Portal Access')}\n </h3>\n <div>\n <p className=\"text-sm text-muted-foreground\">\n {t('customer_accounts.settings.portal_access.url_label', 'Portal URL pattern:')}\n </p>\n <code className=\"mt-1 block rounded bg-muted px-3 py-2 text-sm font-mono\">\n {portalUrl}\n </code>\n </div>\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" asChild>\n <a href={portalRootUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n {t('customer_accounts.settings.portal_access.open_portal', 'Open Portal')}\n </a>\n </Button>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.settings.portal_access.slug_note',\n 'The organization slug is auto-generated from the organization name. You can view and manage organizations in Directory \\u2192 Organizations.',\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n <Link href=\"/backend/directory/organizations\" className=\"text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.portal_access.manage_orgs', 'Manage Organizations')}\n </Link>\n </p>\n </div>\n\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.demo_credentials.title', 'Demo Credentials')}\n </h3>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b text-left\">\n <th className=\"pb-2 pr-4 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.email', 'Email')}\n </th>\n <th className=\"pb-2 pr-4 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.password', 'Password')}\n </th>\n <th className=\"pb-2 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.role', 'Role')}\n </th>\n </tr>\n </thead>\n <tbody>\n {DEMO_CREDENTIALS.map((cred) => (\n <tr key={cred.email} className=\"border-b last:border-0\">\n <td className=\"py-2 pr-4 font-mono text-xs\">{cred.email}</td>\n <td className=\"py-2 pr-4 font-mono text-xs\">{cred.password}</td>\n <td className=\"py-2 text-xs\">{cred.role}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.settings.demo_credentials.note',\n 'These credentials are only available if example data was seeded during setup.',\n )}\n </p>\n </div>\n\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.quick_links.title', 'Quick Links')}\n </h3>\n <ul className=\"space-y-2\">\n <li>\n <Link href=\"/backend/customer_accounts/users\" className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.quick_links.manage_users', 'Manage customer users')}\n </Link>\n </li>\n <li>\n <Link href=\"/backend/customer_accounts/roles\" className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.quick_links.manage_roles', 'Manage customer roles')}\n </Link>\n </li>\n <li>\n <a\n href=\"https://docs.open-mercato.com/framework/modules/customer-portal\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\"\n >\n {t('customer_accounts.settings.quick_links.portal_docs', 'Portal documentation')}\n </a>\n </li>\n </ul>\n </div>\n </div>\n </>\n )\n}\n"],
5
- "mappings": ";AA0BI,mBAEI,KAaE,YAfN;AAxBJ,SAAS,eAAe;AACxB,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,oBAAoB,6BAA6B;AAE1D,MAAM,mBAAmB;AAAA,EACvB,EAAE,OAAO,6BAA6B,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACrF,EAAE,OAAO,yBAAyB,UAAU,gBAAgB,MAAM,QAAQ;AAAA,EAC1E,EAAE,OAAO,2BAA2B,UAAU,gBAAgB,MAAM,SAAS;AAC/E;AAMO,SAAS,mCAAmC,EAAE,aAAa,GAA4C;AAC5G,QAAM,IAAI,KAAK;AAEf,QAAM,YAAY,QAAQ,MAAM,sBAAsB,YAAY,GAAG,CAAC,YAAY,CAAC;AACnF,QAAM,gBAAgB,QAAQ,MAAM,mBAAmB,YAAY,GAAG,CAAC,YAAY,CAAC;AAEpF,SACE,iCACE;AAAA,wBAAC,SAAI,WAAU,6BACb;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,EAAE,oCAAoC,iBAAiB;AAAA,QAC9D,UAAS;AAAA,QACT,WAAW,EAAE,mCAAmC,gBAAgB;AAAA;AAAA,IAClE,GACF;AAAA,IAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,2BAAC,SAAI,WAAU,mCACb;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,kDAAkD,eAAe,GACtE;AAAA,QACA,qBAAC,SACC;AAAA,8BAAC,OAAE,WAAU,iCACV,YAAE,sDAAsD,qBAAqB,GAChF;AAAA,UACA,oBAAC,UAAK,WAAU,2DACb,qBACH;AAAA,WACF;AAAA,QACA,oBAAC,SACC,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAO,MACvD,8BAAC,OAAE,MAAM,eAAe,QAAO,UAAS,KAAI,uBACzC,YAAE,wDAAwD,aAAa,GAC1E,GACF,GACF;AAAA,QACA,oBAAC,OAAE,WAAU,iCACV;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,QACA,oBAAC,OAAE,WAAU,iCACX,8BAAC,QAAK,MAAK,oCAAmC,WAAU,mEACrD,YAAE,wDAAwD,sBAAsB,GACnF,GACF;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,mCACb;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,qDAAqD,kBAAkB,GAC5E;AAAA,QACA,oBAAC,SAAI,WAAU,mBACb,+BAAC,WAAM,WAAU,kBACf;AAAA,8BAAC,WACC,+BAAC,QAAG,WAAU,sBACZ;AAAA,gCAAC,QAAG,WAAU,+CACX,YAAE,qDAAqD,OAAO,GACjE;AAAA,YACA,oBAAC,QAAG,WAAU,+CACX,YAAE,wDAAwD,UAAU,GACvE;AAAA,YACA,oBAAC,QAAG,WAAU,0CACX,YAAE,oDAAoD,MAAM,GAC/D;AAAA,aACF,GACF;AAAA,UACA,oBAAC,WACE,2BAAiB,IAAI,CAAC,SACrB,qBAAC,QAAoB,WAAU,0BAC7B;AAAA,gCAAC,QAAG,WAAU,+BAA+B,eAAK,OAAM;AAAA,YACxD,oBAAC,QAAG,WAAU,+BAA+B,eAAK,UAAS;AAAA,YAC3D,oBAAC,QAAG,WAAU,gBAAgB,eAAK,MAAK;AAAA,eAHjC,KAAK,KAId,CACD,GACH;AAAA,WACF,GACF;AAAA,QACA,oBAAC,OAAE,WAAU,iCACV;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,mCACb;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,gDAAgD,aAAa,GAClE;AAAA,QACA,qBAAC,QAAG,WAAU,aACZ;AAAA,8BAAC,QACC,8BAAC,QAAK,MAAK,oCAAmC,WAAU,2EACrD,YAAE,uDAAuD,uBAAuB,GACnF,GACF;AAAA,UACA,oBAAC,QACC,8BAAC,QAAK,MAAK,oCAAmC,WAAU,2EACrD,YAAE,uDAAuD,uBAAuB,GACnF,GACF;AAAA,UACA,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,QAAO;AAAA,cACP,KAAI;AAAA,cACJ,WAAU;AAAA,cAET,YAAE,sDAAsD,sBAAsB;AAAA;AAAA,UACjF,GACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport { useMemo } from 'react'\nimport Link from 'next/link'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'\n\nconst DEMO_CREDENTIALS = [\n { email: 'alice.johnson@example.com', password: 'Password123!', role: 'Portal Admin' },\n { email: 'bob.smith@example.com', password: 'Password123!', role: 'Buyer' },\n { email: 'carol.white@example.com', password: 'Password123!', role: 'Viewer' },\n] as const\n\nexport type CustomerAccountsSettingsPageClientProps = {\n portalOrigin: string\n portalOrgSlug?: string | null\n}\n\nexport function CustomerAccountsSettingsPageClient({ portalOrigin, portalOrgSlug = null }: CustomerAccountsSettingsPageClientProps) {\n const t = useT()\n\n const portalUrl = useMemo(() => buildPortalUrlPattern(portalOrigin), [portalOrigin])\n const portalRootUrl = useMemo(\n () => (portalOrgSlug ? buildPortalRootUrl(portalOrigin, portalOrgSlug) : null),\n [portalOrigin, portalOrgSlug],\n )\n\n return (\n <>\n <div className=\"px-3 py-3 md:px-6 md:py-4\">\n <FormHeader\n mode=\"detail\"\n title={t('customer_accounts.settings.title', 'Portal Settings')}\n backHref=\"/backend/customer_accounts/users\"\n backLabel={t('customer_accounts.settings.back', 'Customer Users')}\n />\n </div>\n\n <div className=\"max-w-2xl space-y-6\">\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.portal_access.title', 'Portal Access')}\n </h3>\n <div>\n <p className=\"text-sm text-muted-foreground\">\n {t('customer_accounts.settings.portal_access.url_label', 'Portal URL pattern:')}\n </p>\n <code className=\"mt-1 block rounded bg-muted px-3 py-2 text-sm font-mono\">\n {portalUrl}\n </code>\n </div>\n {portalRootUrl ? (\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" asChild>\n <a href={portalRootUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n {t('customer_accounts.settings.portal_access.open_portal', 'Open Portal')}\n </a>\n </Button>\n </div>\n ) : null}\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.settings.portal_access.slug_note',\n 'The organization slug is auto-generated from the organization name. You can view and manage organizations in Directory \\u2192 Organizations.',\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n <Link href=\"/backend/directory/organizations\" className=\"text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.portal_access.manage_orgs', 'Manage Organizations')}\n </Link>\n </p>\n </div>\n\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.demo_credentials.title', 'Demo Credentials')}\n </h3>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b text-left\">\n <th className=\"pb-2 pr-4 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.email', 'Email')}\n </th>\n <th className=\"pb-2 pr-4 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.password', 'Password')}\n </th>\n <th className=\"pb-2 font-medium text-muted-foreground\">\n {t('customer_accounts.settings.demo_credentials.role', 'Role')}\n </th>\n </tr>\n </thead>\n <tbody>\n {DEMO_CREDENTIALS.map((cred) => (\n <tr key={cred.email} className=\"border-b last:border-0\">\n <td className=\"py-2 pr-4 font-mono text-xs\">{cred.email}</td>\n <td className=\"py-2 pr-4 font-mono text-xs\">{cred.password}</td>\n <td className=\"py-2 text-xs\">{cred.role}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.settings.demo_credentials.note',\n 'These credentials are only available if example data was seeded during setup.',\n )}\n </p>\n </div>\n\n <div className=\"rounded-lg border p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold\">\n {t('customer_accounts.settings.quick_links.title', 'Quick Links')}\n </h3>\n <ul className=\"space-y-2\">\n <li>\n <Link href=\"/backend/customer_accounts/users\" className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.quick_links.manage_users', 'Manage customer users')}\n </Link>\n </li>\n <li>\n <Link href=\"/backend/customer_accounts/roles\" className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\">\n {t('customer_accounts.settings.quick_links.manage_roles', 'Manage customer roles')}\n </Link>\n </li>\n <li>\n <a\n href=\"https://docs.open-mercato.com/framework/modules/customer-portal\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-primary underline underline-offset-4 hover:text-primary/80\"\n >\n {t('customer_accounts.settings.quick_links.portal_docs', 'Portal documentation')}\n </a>\n </li>\n </ul>\n </div>\n </div>\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,10 +2,12 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { headers } from "next/headers";
3
3
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
4
4
  import { resolvePortalRequestOrigin } from "../../../lib/portalUrl.js";
5
+ import { resolveCurrentOrgPortalSlug } from "../../../lib/portalOrgSlug.js";
5
6
  import { CustomerAccountsSettingsPageClient } from "./CustomerAccountsSettingsPageClient.js";
6
7
  async function CustomerAccountsSettingsPage() {
7
8
  const portalOrigin = resolvePortalRequestOrigin(await headers());
8
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { className: "space-y-6", children: /* @__PURE__ */ jsx(CustomerAccountsSettingsPageClient, { portalOrigin }) }) });
9
+ const portalOrgSlug = await resolveCurrentOrgPortalSlug();
10
+ return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { className: "space-y-6", children: /* @__PURE__ */ jsx(CustomerAccountsSettingsPageClient, { portalOrigin, portalOrgSlug }) }) });
9
11
  }
10
12
  export {
11
13
  CustomerAccountsSettingsPage as default
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/settings/page.tsx"],
4
- "sourcesContent": ["import { headers } from 'next/headers'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { resolvePortalRequestOrigin } from '../../../lib/portalUrl'\nimport { CustomerAccountsSettingsPageClient } from './CustomerAccountsSettingsPageClient'\n\nexport default async function CustomerAccountsSettingsPage() {\n const portalOrigin = resolvePortalRequestOrigin(await headers())\n return (\n <Page>\n {/* space-y-6 preserves the gap the page header previously got as a direct <Page> child. */}\n <PageBody className=\"space-y-6\">\n <CustomerAccountsSettingsPageClient portalOrigin={portalOrigin} />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "AAWQ;AAXR,SAAS,eAAe;AACxB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,kCAAkC;AAC3C,SAAS,0CAA0C;AAEnD,eAAO,+BAAsD;AAC3D,QAAM,eAAe,2BAA2B,MAAM,QAAQ,CAAC;AAC/D,SACE,oBAAC,QAEC,8BAAC,YAAS,WAAU,aAClB,8BAAC,sCAAmC,cAA4B,GAClE,GACF;AAEJ;",
4
+ "sourcesContent": ["import { headers } from 'next/headers'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { resolvePortalRequestOrigin } from '../../../lib/portalUrl'\nimport { resolveCurrentOrgPortalSlug } from '../../../lib/portalOrgSlug'\nimport { CustomerAccountsSettingsPageClient } from './CustomerAccountsSettingsPageClient'\n\nexport default async function CustomerAccountsSettingsPage() {\n const portalOrigin = resolvePortalRequestOrigin(await headers())\n const portalOrgSlug = await resolveCurrentOrgPortalSlug()\n return (\n <Page>\n {/* space-y-6 preserves the gap the page header previously got as a direct <Page> child. */}\n <PageBody className=\"space-y-6\">\n <CustomerAccountsSettingsPageClient portalOrigin={portalOrigin} portalOrgSlug={portalOrgSlug} />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "AAaQ;AAbR,SAAS,eAAe;AACxB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,kCAAkC;AAC3C,SAAS,mCAAmC;AAC5C,SAAS,0CAA0C;AAEnD,eAAO,+BAAsD;AAC3D,QAAM,eAAe,2BAA2B,MAAM,QAAQ,CAAC;AAC/D,QAAM,gBAAgB,MAAM,4BAA4B;AACxD,SACE,oBAAC,QAEC,8BAAC,YAAS,WAAU,aAClB,8BAAC,sCAAmC,cAA4B,eAA8B,GAChG,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -183,7 +183,7 @@ function CreateUserDialog({
183
183
  ] })
184
184
  ] }) });
185
185
  }
186
- function PortalUsersPageClient({ portalOrigin }) {
186
+ function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }) {
187
187
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
188
188
  const t = useT();
189
189
  const router = useRouter();
@@ -430,19 +430,19 @@ function PortalUsersPageClient({ portalOrigin }) {
430
430
  ] })
431
431
  }
432
432
  ),
433
- /* @__PURE__ */ jsx(
433
+ portalOrgSlug ? /* @__PURE__ */ jsx(
434
434
  Button,
435
435
  {
436
436
  type: "button",
437
437
  variant: "outline",
438
438
  size: "sm",
439
439
  asChild: true,
440
- children: /* @__PURE__ */ jsxs("a", { href: buildPortalRootUrl(portalOrigin), target: "_blank", rel: "noopener noreferrer", children: [
440
+ children: /* @__PURE__ */ jsxs("a", { href: buildPortalRootUrl(portalOrigin, portalOrgSlug), target: "_blank", rel: "noopener noreferrer", children: [
441
441
  /* @__PURE__ */ jsx(Globe, { className: "size-4" }),
442
442
  t("customer_accounts.admin.portalInfo.open", "Open Portal")
443
443
  ] })
444
444
  }
445
- )
445
+ ) : null
446
446
  ] })
447
447
  ] }) }),
448
448
  /* @__PURE__ */ jsx(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/customer_accounts/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <Button\n key={role.id}\n type=\"button\"\n size=\"2xs\"\n variant={isSelected ? 'secondary' : 'outline'}\n aria-pressed={isSelected}\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className=\"rounded-full\"\n >\n {role.label}\n </Button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport type PortalUsersPageClientProps = {\n portalOrigin: string\n}\n\nexport function PortalUsersPageClient({ portalOrigin }: PortalUsersPageClientProps) {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\n const items = Array.isArray(payload?.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload?.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <>\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: buildPortalUrlPattern(portalOrigin),\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={buildPortalRootUrl(portalOrigin)} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: extensionPoints.hosts.usersTable.tableId }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/customer_accounts/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <Button\n key={role.id}\n type=\"button\"\n size=\"2xs\"\n variant={isSelected ? 'secondary' : 'outline'}\n aria-pressed={isSelected}\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className=\"rounded-full\"\n >\n {role.label}\n </Button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport type PortalUsersPageClientProps = {\n portalOrigin: string\n portalOrgSlug?: string | null\n}\n\nexport function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: PortalUsersPageClientProps) {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\n const items = Array.isArray(payload?.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload?.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <>\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: buildPortalUrlPattern(portalOrigin),\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n {portalOrgSlug ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={buildPortalRootUrl(portalOrigin, portalOrgSlug)} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n ) : null}\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: extensionPoints.hosts.usersTable.tableId }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,10 +2,12 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { headers } from "next/headers";
3
3
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
4
4
  import { resolvePortalRequestOrigin } from "../../../lib/portalUrl.js";
5
+ import { resolveCurrentOrgPortalSlug } from "../../../lib/portalOrgSlug.js";
5
6
  import { PortalUsersPageClient } from "./PortalUsersPageClient.js";
6
7
  async function CustomerAccountsPage() {
7
8
  const portalOrigin = resolvePortalRequestOrigin(await headers());
8
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { className: "space-y-4", children: /* @__PURE__ */ jsx(PortalUsersPageClient, { portalOrigin }) }) });
9
+ const portalOrgSlug = await resolveCurrentOrgPortalSlug();
10
+ return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { className: "space-y-4", children: /* @__PURE__ */ jsx(PortalUsersPageClient, { portalOrigin, portalOrgSlug }) }) });
9
11
  }
10
12
  export {
11
13
  CustomerAccountsPage as default
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/users/page.tsx"],
4
- "sourcesContent": ["import { headers } from 'next/headers'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { resolvePortalRequestOrigin } from '../../../lib/portalUrl'\nimport { PortalUsersPageClient } from './PortalUsersPageClient'\n\nexport default async function CustomerAccountsPage() {\n const portalOrigin = resolvePortalRequestOrigin(await headers())\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <PortalUsersPageClient portalOrigin={portalOrigin} />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "AAUQ;AAVR,SAAS,eAAe;AACxB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AAEtC,eAAO,uBAA8C;AACnD,QAAM,eAAe,2BAA2B,MAAM,QAAQ,CAAC;AAC/D,SACE,oBAAC,QACC,8BAAC,YAAS,WAAU,aAClB,8BAAC,yBAAsB,cAA4B,GACrD,GACF;AAEJ;",
4
+ "sourcesContent": ["import { headers } from 'next/headers'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { resolvePortalRequestOrigin } from '../../../lib/portalUrl'\nimport { resolveCurrentOrgPortalSlug } from '../../../lib/portalOrgSlug'\nimport { PortalUsersPageClient } from './PortalUsersPageClient'\n\nexport default async function CustomerAccountsPage() {\n const portalOrigin = resolvePortalRequestOrigin(await headers())\n const portalOrgSlug = await resolveCurrentOrgPortalSlug()\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <PortalUsersPageClient portalOrigin={portalOrigin} portalOrgSlug={portalOrgSlug} />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "AAYQ;AAZR,SAAS,eAAe;AACxB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,kCAAkC;AAC3C,SAAS,mCAAmC;AAC5C,SAAS,6BAA6B;AAEtC,eAAO,uBAA8C;AACnD,QAAM,eAAe,2BAA2B,MAAM,QAAQ,CAAC;AAC/D,QAAM,gBAAgB,MAAM,4BAA4B;AACxD,SACE,oBAAC,QACC,8BAAC,YAAS,WAAU,aAClB,8BAAC,yBAAsB,cAA4B,eAA8B,GACnF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,22 @@
1
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
+ import { getAuthFromCookies } from "@open-mercato/shared/lib/auth/server";
3
+ import { findOrganizationInTenant } from "./organizationLookup.js";
4
+ async function resolveCurrentOrgPortalSlug() {
5
+ try {
6
+ const auth = await getAuthFromCookies();
7
+ const organizationId = auth?.orgId;
8
+ const tenantId = auth?.tenantId;
9
+ if (!organizationId || !tenantId) return null;
10
+ const container = await createRequestContainer();
11
+ const em = container.resolve("em");
12
+ const organization = await findOrganizationInTenant(em, organizationId, tenantId);
13
+ const slug = organization?.slug?.trim();
14
+ return slug && slug.length > 0 ? slug : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ export {
20
+ resolveCurrentOrgPortalSlug
21
+ };
22
+ //# sourceMappingURL=portalOrgSlug.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customer_accounts/lib/portalOrgSlug.ts"],
4
+ "sourcesContent": ["// Server-only resolution of the signed-in admin's organization slug, used to\n// build the \"Open Portal\" link on the customer-accounts admin pages.\n//\n// Deliberately kept out of `portalUrl.ts`: that module is imported by client\n// components, and pulling the ORM in there would drag MikroORM into the browser\n// bundle. Keep every EntityManager import on this side of the boundary.\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'\nimport { findOrganizationInTenant } from './organizationLookup'\n\ntype EntityManagerResolver = {\n resolve(name: 'em'): EntityManager\n}\n\n/**\n * Resolve the slug of the organization the current admin session is scoped to,\n * or null when there is no session, no active organization, or the organization\n * has no slug yet. Callers render the portal link only for a non-null result \u2014\n * a slugless portal URL cannot resolve (#5668).\n */\nexport async function resolveCurrentOrgPortalSlug(): Promise<string | null> {\n try {\n const auth = await getAuthFromCookies()\n const organizationId = auth?.orgId\n const tenantId = auth?.tenantId\n if (!organizationId || !tenantId) return null\n\n const container = await createRequestContainer()\n const em = (container as Awaited<ReturnType<typeof createRequestContainer>> & EntityManagerResolver).resolve('em')\n const organization = await findOrganizationInTenant(em, organizationId, tenantId)\n const slug = organization?.slug?.trim()\n return slug && slug.length > 0 ? slug : null\n } catch {\n // The portal link is a convenience action \u2014 a lookup failure must not take\n // the whole admin page down, so degrade to hiding the button.\n return null\n }\n}\n"],
5
+ "mappings": "AAQA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,gCAAgC;AAYzC,eAAsB,8BAAsD;AAC1E,MAAI;AACF,UAAM,OAAO,MAAM,mBAAmB;AACtC,UAAM,iBAAiB,MAAM;AAC7B,UAAM,WAAW,MAAM;AACvB,QAAI,CAAC,kBAAkB,CAAC,SAAU,QAAO;AAEzC,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAM,UAAyF,QAAQ,IAAI;AACjH,UAAM,eAAe,MAAM,yBAAyB,IAAI,gBAAgB,QAAQ;AAChF,UAAM,OAAO,cAAc,MAAM,KAAK;AACtC,WAAO,QAAQ,KAAK,SAAS,IAAI,OAAO;AAAA,EAC1C,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -57,8 +57,11 @@ function normalizeOrigin(origin) {
57
57
  function buildPortalUrlPattern(origin) {
58
58
  return `${normalizeOrigin(origin)}/${PORTAL_ORG_SLUG_PLACEHOLDER}/portal`;
59
59
  }
60
- function buildPortalRootUrl(origin) {
61
- return `${normalizeOrigin(origin)}/portal`;
60
+ function buildPortalRootUrl(origin, orgSlug) {
61
+ const base = normalizeOrigin(origin);
62
+ const slug = typeof orgSlug === "string" ? orgSlug.trim() : "";
63
+ if (!slug) return `${base}/portal`;
64
+ return `${base}/${encodeURIComponent(slug)}/portal`;
62
65
  }
63
66
  export {
64
67
  PORTAL_ORG_SLUG_PLACEHOLDER,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/lib/portalUrl.ts"],
4
- "sourcesContent": ["// Resolve the browser-visible origin on the server so the admin portal-address\n// hints are already correct in the first server-rendered paint. Deriving the\n// origin from `window.location.origin` during render made the server HTML and\n// the hydrated client output disagree, which React reported as a hydration\n// error and repaired with a visible flicker of the portal address (#5457).\n//\n// `resolvePortalRequestOrigin` is deliberately named apart from the shared\n// `resolveRequestOrigin` in `@open-mercato/shared/lib/url`: that one takes a\n// `Request` and reads the protocol off `req.url`, which a server component does\n// not have \u2014 it only has `headers()`. This one also validates the untrusted host\n// header before the value reaches rendered text or a link href, and infers the\n// protocol when the proxy stays quiet, so the two are not interchangeable.\n\nexport type RequestHeaderReader = {\n get(name: string): string | null | undefined\n}\n\nconst FORWARDED_HOST_HEADER = 'x-forwarded-host'\nconst HOST_HEADER = 'host'\nconst FORWARDED_PROTO_HEADER = 'x-forwarded-proto'\n\n// Host headers are client-controlled, so only a plain `host[:port]` or bracketed\n// IPv6 literal is accepted before it reaches rendered text and link hrefs.\nconst HOST_PATTERN = /^(?:[a-z0-9.-]+|\\[[0-9a-f:.]+\\])(?::\\d{1,5})?$/i\nconst PROTOCOL_PATTERN = /^https?$/i\n// Bracketed, because HOST_PATTERN only accepts a colon-bearing host in that form.\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])\n\nexport const PORTAL_ORG_SLUG_PLACEHOLDER = '[org-slug]'\n\nfunction firstHeaderValue(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n // Proxy chains append values, so the client-facing hop is the first entry.\n const value = raw.split(',')[0]?.trim() ?? ''\n return value.length > 0 ? value : null\n}\n\nfunction readHost(headers: RequestHeaderReader): string | null {\n const forwarded = firstHeaderValue(headers.get(FORWARDED_HOST_HEADER))\n const host = forwarded ?? firstHeaderValue(headers.get(HOST_HEADER))\n if (!host) return null\n return HOST_PATTERN.test(host) ? host.toLowerCase() : null\n}\n\nfunction readProtocol(headers: RequestHeaderReader, host: string): string {\n const forwarded = firstHeaderValue(headers.get(FORWARDED_PROTO_HEADER))\n if (forwarded && PROTOCOL_PATTERN.test(forwarded)) return forwarded.toLowerCase()\n // An operator who configured APP_URL has named the canonical origin explicitly;\n // when it points at this very host, trust it over any inference below. Without\n // this, a plain-HTTP deployment behind a proxy that omits x-forwarded-proto\n // would render https:// links that do not work.\n const configured = configuredOrigin()\n if (configured) {\n try {\n const parsed = new URL(configured)\n if (parsed.host.toLowerCase() === host) return parsed.protocol.replace(':', '')\n } catch {\n // configuredOrigin() already parsed this successfully; ignore defensively.\n }\n }\n // Otherwise a terminating proxy that omits the header is serving a public\n // hostname over TLS far more often than not; only loopback hosts default to http.\n const hostname = host.replace(/:\\d{1,5}$/, '')\n return LOOPBACK_HOSTS.has(hostname) ? 'http' : 'https'\n}\n\nfunction configuredOrigin(): string {\n const candidates = [process.env.APP_URL, process.env.NEXT_PUBLIC_APP_URL]\n for (const candidate of candidates) {\n const value = candidate?.trim()\n if (!value) continue\n try {\n const parsed = new URL(value)\n if (PROTOCOL_PATTERN.test(parsed.protocol.replace(':', ''))) return parsed.origin\n } catch {\n // A malformed APP_URL must not break the page \u2014 fall through to the next candidate.\n }\n }\n return ''\n}\n\n/**\n * Resolve the origin the current request was made to, mirroring the value\n * `window.location.origin` reports in the browser. Falls back to the configured\n * app URL, then to an empty string so callers render a root-relative path that\n * is identical on the server and the client.\n *\n * Named apart from `resolveRequestOrigin` in `@open-mercato/shared/lib/url` \u2014\n * see the note at the top of this file for why the two cannot be merged.\n */\nexport function resolvePortalRequestOrigin(headers: RequestHeaderReader | null | undefined): string {\n if (headers && typeof headers.get === 'function') {\n const host = readHost(headers)\n if (host) return `${readProtocol(headers, host)}://${host}`\n }\n return configuredOrigin()\n}\n\nfunction normalizeOrigin(origin: string | null | undefined): string {\n if (typeof origin !== 'string') return ''\n return origin.trim().replace(/\\/+$/, '')\n}\n\n/** Display pattern for a tenant portal address, e.g. `https://app.example.com/[org-slug]/portal`. */\nexport function buildPortalUrlPattern(origin: string | null | undefined): string {\n return `${normalizeOrigin(origin)}/${PORTAL_ORG_SLUG_PLACEHOLDER}/portal`\n}\n\n/** Portal entry point used by the \"Open Portal\" action on admin pages. */\nexport function buildPortalRootUrl(origin: string | null | undefined): string {\n return `${normalizeOrigin(origin)}/portal`\n}\n"],
5
- "mappings": "AAiBA,MAAM,wBAAwB;AAC9B,MAAM,cAAc;AACpB,MAAM,yBAAyB;AAI/B,MAAM,eAAe;AACrB,MAAM,mBAAmB;AAEzB,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAE3D,MAAM,8BAA8B;AAE3C,SAAS,iBAAiB,KAA+C;AACvE,MAAI,OAAO,QAAQ,SAAU,QAAO;AAEpC,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAAK;AAC3C,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAEA,SAAS,SAAS,SAA6C;AAC7D,QAAM,YAAY,iBAAiB,QAAQ,IAAI,qBAAqB,CAAC;AACrE,QAAM,OAAO,aAAa,iBAAiB,QAAQ,IAAI,WAAW,CAAC;AACnE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,aAAa,KAAK,IAAI,IAAI,KAAK,YAAY,IAAI;AACxD;AAEA,SAAS,aAAa,SAA8B,MAAsB;AACxE,QAAM,YAAY,iBAAiB,QAAQ,IAAI,sBAAsB,CAAC;AACtE,MAAI,aAAa,iBAAiB,KAAK,SAAS,EAAG,QAAO,UAAU,YAAY;AAKhF,QAAM,aAAa,iBAAiB;AACpC,MAAI,YAAY;AACd,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,UAAU;AACjC,UAAI,OAAO,KAAK,YAAY,MAAM,KAAM,QAAO,OAAO,SAAS,QAAQ,KAAK,EAAE;AAAA,IAChF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,WAAW,KAAK,QAAQ,aAAa,EAAE;AAC7C,SAAO,eAAe,IAAI,QAAQ,IAAI,SAAS;AACjD;AAEA,SAAS,mBAA2B;AAClC,QAAM,aAAa,CAAC,QAAQ,IAAI,SAAS,QAAQ,IAAI,mBAAmB;AACxE,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,WAAW,KAAK;AAC9B,QAAI,CAAC,MAAO;AACZ,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAI,iBAAiB,KAAK,OAAO,SAAS,QAAQ,KAAK,EAAE,CAAC,EAAG,QAAO,OAAO;AAAA,IAC7E,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAWO,SAAS,2BAA2B,SAAyD;AAClG,MAAI,WAAW,OAAO,QAAQ,QAAQ,YAAY;AAChD,UAAM,OAAO,SAAS,OAAO;AAC7B,QAAI,KAAM,QAAO,GAAG,aAAa,SAAS,IAAI,CAAC,MAAM,IAAI;AAAA,EAC3D;AACA,SAAO,iBAAiB;AAC1B;AAEA,SAAS,gBAAgB,QAA2C;AAClE,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,SAAO,OAAO,KAAK,EAAE,QAAQ,QAAQ,EAAE;AACzC;AAGO,SAAS,sBAAsB,QAA2C;AAC/E,SAAO,GAAG,gBAAgB,MAAM,CAAC,IAAI,2BAA2B;AAClE;AAGO,SAAS,mBAAmB,QAA2C;AAC5E,SAAO,GAAG,gBAAgB,MAAM,CAAC;AACnC;",
4
+ "sourcesContent": ["// Resolve the browser-visible origin on the server so the admin portal-address\n// hints are already correct in the first server-rendered paint. Deriving the\n// origin from `window.location.origin` during render made the server HTML and\n// the hydrated client output disagree, which React reported as a hydration\n// error and repaired with a visible flicker of the portal address (#5457).\n//\n// `resolvePortalRequestOrigin` is deliberately named apart from the shared\n// `resolveRequestOrigin` in `@open-mercato/shared/lib/url`: that one takes a\n// `Request` and reads the protocol off `req.url`, which a server component does\n// not have \u2014 it only has `headers()`. This one also validates the untrusted host\n// header before the value reaches rendered text or a link href, and infers the\n// protocol when the proxy stays quiet, so the two are not interchangeable.\n\nexport type RequestHeaderReader = {\n get(name: string): string | null | undefined\n}\n\nconst FORWARDED_HOST_HEADER = 'x-forwarded-host'\nconst HOST_HEADER = 'host'\nconst FORWARDED_PROTO_HEADER = 'x-forwarded-proto'\n\n// Host headers are client-controlled, so only a plain `host[:port]` or bracketed\n// IPv6 literal is accepted before it reaches rendered text and link hrefs.\nconst HOST_PATTERN = /^(?:[a-z0-9.-]+|\\[[0-9a-f:.]+\\])(?::\\d{1,5})?$/i\nconst PROTOCOL_PATTERN = /^https?$/i\n// Bracketed, because HOST_PATTERN only accepts a colon-bearing host in that form.\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])\n\nexport const PORTAL_ORG_SLUG_PLACEHOLDER = '[org-slug]'\n\nfunction firstHeaderValue(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n // Proxy chains append values, so the client-facing hop is the first entry.\n const value = raw.split(',')[0]?.trim() ?? ''\n return value.length > 0 ? value : null\n}\n\nfunction readHost(headers: RequestHeaderReader): string | null {\n const forwarded = firstHeaderValue(headers.get(FORWARDED_HOST_HEADER))\n const host = forwarded ?? firstHeaderValue(headers.get(HOST_HEADER))\n if (!host) return null\n return HOST_PATTERN.test(host) ? host.toLowerCase() : null\n}\n\nfunction readProtocol(headers: RequestHeaderReader, host: string): string {\n const forwarded = firstHeaderValue(headers.get(FORWARDED_PROTO_HEADER))\n if (forwarded && PROTOCOL_PATTERN.test(forwarded)) return forwarded.toLowerCase()\n // An operator who configured APP_URL has named the canonical origin explicitly;\n // when it points at this very host, trust it over any inference below. Without\n // this, a plain-HTTP deployment behind a proxy that omits x-forwarded-proto\n // would render https:// links that do not work.\n const configured = configuredOrigin()\n if (configured) {\n try {\n const parsed = new URL(configured)\n if (parsed.host.toLowerCase() === host) return parsed.protocol.replace(':', '')\n } catch {\n // configuredOrigin() already parsed this successfully; ignore defensively.\n }\n }\n // Otherwise a terminating proxy that omits the header is serving a public\n // hostname over TLS far more often than not; only loopback hosts default to http.\n const hostname = host.replace(/:\\d{1,5}$/, '')\n return LOOPBACK_HOSTS.has(hostname) ? 'http' : 'https'\n}\n\nfunction configuredOrigin(): string {\n const candidates = [process.env.APP_URL, process.env.NEXT_PUBLIC_APP_URL]\n for (const candidate of candidates) {\n const value = candidate?.trim()\n if (!value) continue\n try {\n const parsed = new URL(value)\n if (PROTOCOL_PATTERN.test(parsed.protocol.replace(':', ''))) return parsed.origin\n } catch {\n // A malformed APP_URL must not break the page \u2014 fall through to the next candidate.\n }\n }\n return ''\n}\n\n/**\n * Resolve the origin the current request was made to, mirroring the value\n * `window.location.origin` reports in the browser. Falls back to the configured\n * app URL, then to an empty string so callers render a root-relative path that\n * is identical on the server and the client.\n *\n * Named apart from `resolveRequestOrigin` in `@open-mercato/shared/lib/url` \u2014\n * see the note at the top of this file for why the two cannot be merged.\n */\nexport function resolvePortalRequestOrigin(headers: RequestHeaderReader | null | undefined): string {\n if (headers && typeof headers.get === 'function') {\n const host = readHost(headers)\n if (host) return `${readProtocol(headers, host)}://${host}`\n }\n return configuredOrigin()\n}\n\nfunction normalizeOrigin(origin: string | null | undefined): string {\n if (typeof origin !== 'string') return ''\n return origin.trim().replace(/\\/+$/, '')\n}\n\n/** Display pattern for a tenant portal address, e.g. `https://app.example.com/[org-slug]/portal`. */\nexport function buildPortalUrlPattern(origin: string | null | undefined): string {\n return `${normalizeOrigin(origin)}/${PORTAL_ORG_SLUG_PLACEHOLDER}/portal`\n}\n\n/**\n * Portal entry point used by the \"Open Portal\" action on admin pages.\n *\n * Portal pages are only ever mounted at `frontend/[orgSlug]/portal/**`, so a URL\n * without the organization segment cannot resolve and renders a 404 (#5668).\n * The slug stays optional to keep this signature backward compatible; callers\n * that cannot resolve one should hide the action rather than link to the\n * slugless fallback.\n */\nexport function buildPortalRootUrl(origin: string | null | undefined, orgSlug?: string | null): string {\n const base = normalizeOrigin(origin)\n const slug = typeof orgSlug === 'string' ? orgSlug.trim() : ''\n if (!slug) return `${base}/portal`\n return `${base}/${encodeURIComponent(slug)}/portal`\n}\n"],
5
+ "mappings": "AAiBA,MAAM,wBAAwB;AAC9B,MAAM,cAAc;AACpB,MAAM,yBAAyB;AAI/B,MAAM,eAAe;AACrB,MAAM,mBAAmB;AAEzB,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAE3D,MAAM,8BAA8B;AAE3C,SAAS,iBAAiB,KAA+C;AACvE,MAAI,OAAO,QAAQ,SAAU,QAAO;AAEpC,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAAK;AAC3C,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAEA,SAAS,SAAS,SAA6C;AAC7D,QAAM,YAAY,iBAAiB,QAAQ,IAAI,qBAAqB,CAAC;AACrE,QAAM,OAAO,aAAa,iBAAiB,QAAQ,IAAI,WAAW,CAAC;AACnE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,aAAa,KAAK,IAAI,IAAI,KAAK,YAAY,IAAI;AACxD;AAEA,SAAS,aAAa,SAA8B,MAAsB;AACxE,QAAM,YAAY,iBAAiB,QAAQ,IAAI,sBAAsB,CAAC;AACtE,MAAI,aAAa,iBAAiB,KAAK,SAAS,EAAG,QAAO,UAAU,YAAY;AAKhF,QAAM,aAAa,iBAAiB;AACpC,MAAI,YAAY;AACd,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,UAAU;AACjC,UAAI,OAAO,KAAK,YAAY,MAAM,KAAM,QAAO,OAAO,SAAS,QAAQ,KAAK,EAAE;AAAA,IAChF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,WAAW,KAAK,QAAQ,aAAa,EAAE;AAC7C,SAAO,eAAe,IAAI,QAAQ,IAAI,SAAS;AACjD;AAEA,SAAS,mBAA2B;AAClC,QAAM,aAAa,CAAC,QAAQ,IAAI,SAAS,QAAQ,IAAI,mBAAmB;AACxE,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,WAAW,KAAK;AAC9B,QAAI,CAAC,MAAO;AACZ,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAI,iBAAiB,KAAK,OAAO,SAAS,QAAQ,KAAK,EAAE,CAAC,EAAG,QAAO,OAAO;AAAA,IAC7E,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAWO,SAAS,2BAA2B,SAAyD;AAClG,MAAI,WAAW,OAAO,QAAQ,QAAQ,YAAY;AAChD,UAAM,OAAO,SAAS,OAAO;AAC7B,QAAI,KAAM,QAAO,GAAG,aAAa,SAAS,IAAI,CAAC,MAAM,IAAI;AAAA,EAC3D;AACA,SAAO,iBAAiB;AAC1B;AAEA,SAAS,gBAAgB,QAA2C;AAClE,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,SAAO,OAAO,KAAK,EAAE,QAAQ,QAAQ,EAAE;AACzC;AAGO,SAAS,sBAAsB,QAA2C;AAC/E,SAAO,GAAG,gBAAgB,MAAM,CAAC,IAAI,2BAA2B;AAClE;AAWO,SAAS,mBAAmB,QAAmC,SAAiC;AACrG,QAAM,OAAO,gBAAgB,MAAM;AACnC,QAAM,OAAO,OAAO,YAAY,WAAW,QAAQ,KAAK,IAAI;AAC5D,MAAI,CAAC,KAAM,QAAO,GAAG,IAAI;AACzB,SAAO,GAAG,IAAI,IAAI,mBAAmB,IAAI,CAAC;AAC5C;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.7.1-develop.7104.1.1dd769bf1a",
3
+ "version": "0.7.1-develop.7105.1.0d2f4c6651",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -252,16 +252,16 @@
252
252
  "zod": "^4.4.3"
253
253
  },
254
254
  "peerDependencies": {
255
- "@open-mercato/ai-assistant": "0.7.1-develop.7104.1.1dd769bf1a",
256
- "@open-mercato/shared": "0.7.1-develop.7104.1.1dd769bf1a",
257
- "@open-mercato/ui": "0.7.1-develop.7104.1.1dd769bf1a",
255
+ "@open-mercato/ai-assistant": "0.7.1-develop.7105.1.0d2f4c6651",
256
+ "@open-mercato/shared": "0.7.1-develop.7105.1.0d2f4c6651",
257
+ "@open-mercato/ui": "0.7.1-develop.7105.1.0d2f4c6651",
258
258
  "react": "^19.0.0",
259
259
  "react-dom": "^19.0.0"
260
260
  },
261
261
  "devDependencies": {
262
- "@open-mercato/ai-assistant": "0.7.1-develop.7104.1.1dd769bf1a",
263
- "@open-mercato/shared": "0.7.1-develop.7104.1.1dd769bf1a",
264
- "@open-mercato/ui": "0.7.1-develop.7104.1.1dd769bf1a",
262
+ "@open-mercato/ai-assistant": "0.7.1-develop.7105.1.0d2f4c6651",
263
+ "@open-mercato/shared": "0.7.1-develop.7105.1.0d2f4c6651",
264
+ "@open-mercato/ui": "0.7.1-develop.7105.1.0d2f4c6651",
265
265
  "@testing-library/dom": "^10.4.1",
266
266
  "@testing-library/jest-dom": "^7.0.0",
267
267
  "@testing-library/react": "^16.3.1",
@@ -15,13 +15,17 @@ const DEMO_CREDENTIALS = [
15
15
 
16
16
  export type CustomerAccountsSettingsPageClientProps = {
17
17
  portalOrigin: string
18
+ portalOrgSlug?: string | null
18
19
  }
19
20
 
20
- export function CustomerAccountsSettingsPageClient({ portalOrigin }: CustomerAccountsSettingsPageClientProps) {
21
+ export function CustomerAccountsSettingsPageClient({ portalOrigin, portalOrgSlug = null }: CustomerAccountsSettingsPageClientProps) {
21
22
  const t = useT()
22
23
 
23
24
  const portalUrl = useMemo(() => buildPortalUrlPattern(portalOrigin), [portalOrigin])
24
- const portalRootUrl = useMemo(() => buildPortalRootUrl(portalOrigin), [portalOrigin])
25
+ const portalRootUrl = useMemo(
26
+ () => (portalOrgSlug ? buildPortalRootUrl(portalOrigin, portalOrgSlug) : null),
27
+ [portalOrigin, portalOrgSlug],
28
+ )
25
29
 
26
30
  return (
27
31
  <>
@@ -47,13 +51,15 @@ export function CustomerAccountsSettingsPageClient({ portalOrigin }: CustomerAcc
47
51
  {portalUrl}
48
52
  </code>
49
53
  </div>
50
- <div>
51
- <Button type="button" variant="outline" size="sm" asChild>
52
- <a href={portalRootUrl} target="_blank" rel="noopener noreferrer">
53
- {t('customer_accounts.settings.portal_access.open_portal', 'Open Portal')}
54
- </a>
55
- </Button>
56
- </div>
54
+ {portalRootUrl ? (
55
+ <div>
56
+ <Button type="button" variant="outline" size="sm" asChild>
57
+ <a href={portalRootUrl} target="_blank" rel="noopener noreferrer">
58
+ {t('customer_accounts.settings.portal_access.open_portal', 'Open Portal')}
59
+ </a>
60
+ </Button>
61
+ </div>
62
+ ) : null}
57
63
  <p className="text-xs text-muted-foreground">
58
64
  {t(
59
65
  'customer_accounts.settings.portal_access.slug_note',
@@ -1,15 +1,17 @@
1
1
  import { headers } from 'next/headers'
2
2
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
3
3
  import { resolvePortalRequestOrigin } from '../../../lib/portalUrl'
4
+ import { resolveCurrentOrgPortalSlug } from '../../../lib/portalOrgSlug'
4
5
  import { CustomerAccountsSettingsPageClient } from './CustomerAccountsSettingsPageClient'
5
6
 
6
7
  export default async function CustomerAccountsSettingsPage() {
7
8
  const portalOrigin = resolvePortalRequestOrigin(await headers())
9
+ const portalOrgSlug = await resolveCurrentOrgPortalSlug()
8
10
  return (
9
11
  <Page>
10
12
  {/* space-y-6 preserves the gap the page header previously got as a direct <Page> child. */}
11
13
  <PageBody className="space-y-6">
12
- <CustomerAccountsSettingsPageClient portalOrigin={portalOrigin} />
14
+ <CustomerAccountsSettingsPageClient portalOrigin={portalOrigin} portalOrgSlug={portalOrgSlug} />
13
15
  </PageBody>
14
16
  </Page>
15
17
  )
@@ -229,9 +229,10 @@ function CreateUserDialog({
229
229
 
230
230
  export type PortalUsersPageClientProps = {
231
231
  portalOrigin: string
232
+ portalOrgSlug?: string | null
232
233
  }
233
234
 
234
- export function PortalUsersPageClient({ portalOrigin }: PortalUsersPageClientProps) {
235
+ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: PortalUsersPageClientProps) {
235
236
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
236
237
  const t = useT()
237
238
  const router = useRouter()
@@ -521,17 +522,19 @@ export function PortalUsersPageClient({ portalOrigin }: PortalUsersPageClientPro
521
522
  {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}
522
523
  </Link>
523
524
  </Button>
524
- <Button
525
- type="button"
526
- variant="outline"
527
- size="sm"
528
- asChild
529
- >
530
- <a href={buildPortalRootUrl(portalOrigin)} target="_blank" rel="noopener noreferrer">
531
- <Globe className="size-4" />
532
- {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}
533
- </a>
534
- </Button>
525
+ {portalOrgSlug ? (
526
+ <Button
527
+ type="button"
528
+ variant="outline"
529
+ size="sm"
530
+ asChild
531
+ >
532
+ <a href={buildPortalRootUrl(portalOrigin, portalOrgSlug)} target="_blank" rel="noopener noreferrer">
533
+ <Globe className="size-4" />
534
+ {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}
535
+ </a>
536
+ </Button>
537
+ ) : null}
535
538
  </div>
536
539
  </div>
537
540
  </div>
@@ -1,14 +1,16 @@
1
1
  import { headers } from 'next/headers'
2
2
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
3
3
  import { resolvePortalRequestOrigin } from '../../../lib/portalUrl'
4
+ import { resolveCurrentOrgPortalSlug } from '../../../lib/portalOrgSlug'
4
5
  import { PortalUsersPageClient } from './PortalUsersPageClient'
5
6
 
6
7
  export default async function CustomerAccountsPage() {
7
8
  const portalOrigin = resolvePortalRequestOrigin(await headers())
9
+ const portalOrgSlug = await resolveCurrentOrgPortalSlug()
8
10
  return (
9
11
  <Page>
10
12
  <PageBody className="space-y-4">
11
- <PortalUsersPageClient portalOrigin={portalOrigin} />
13
+ <PortalUsersPageClient portalOrigin={portalOrigin} portalOrgSlug={portalOrgSlug} />
12
14
  </PageBody>
13
15
  </Page>
14
16
  )
@@ -0,0 +1,40 @@
1
+ // Server-only resolution of the signed-in admin's organization slug, used to
2
+ // build the "Open Portal" link on the customer-accounts admin pages.
3
+ //
4
+ // Deliberately kept out of `portalUrl.ts`: that module is imported by client
5
+ // components, and pulling the ORM in there would drag MikroORM into the browser
6
+ // bundle. Keep every EntityManager import on this side of the boundary.
7
+
8
+ import type { EntityManager } from '@mikro-orm/postgresql'
9
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
10
+ import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
11
+ import { findOrganizationInTenant } from './organizationLookup'
12
+
13
+ type EntityManagerResolver = {
14
+ resolve(name: 'em'): EntityManager
15
+ }
16
+
17
+ /**
18
+ * Resolve the slug of the organization the current admin session is scoped to,
19
+ * or null when there is no session, no active organization, or the organization
20
+ * has no slug yet. Callers render the portal link only for a non-null result —
21
+ * a slugless portal URL cannot resolve (#5668).
22
+ */
23
+ export async function resolveCurrentOrgPortalSlug(): Promise<string | null> {
24
+ try {
25
+ const auth = await getAuthFromCookies()
26
+ const organizationId = auth?.orgId
27
+ const tenantId = auth?.tenantId
28
+ if (!organizationId || !tenantId) return null
29
+
30
+ const container = await createRequestContainer()
31
+ const em = (container as Awaited<ReturnType<typeof createRequestContainer>> & EntityManagerResolver).resolve('em')
32
+ const organization = await findOrganizationInTenant(em, organizationId, tenantId)
33
+ const slug = organization?.slug?.trim()
34
+ return slug && slug.length > 0 ? slug : null
35
+ } catch {
36
+ // The portal link is a convenience action — a lookup failure must not take
37
+ // the whole admin page down, so degrade to hiding the button.
38
+ return null
39
+ }
40
+ }
@@ -106,7 +106,18 @@ export function buildPortalUrlPattern(origin: string | null | undefined): string
106
106
  return `${normalizeOrigin(origin)}/${PORTAL_ORG_SLUG_PLACEHOLDER}/portal`
107
107
  }
108
108
 
109
- /** Portal entry point used by the "Open Portal" action on admin pages. */
110
- export function buildPortalRootUrl(origin: string | null | undefined): string {
111
- return `${normalizeOrigin(origin)}/portal`
109
+ /**
110
+ * Portal entry point used by the "Open Portal" action on admin pages.
111
+ *
112
+ * Portal pages are only ever mounted at `frontend/[orgSlug]/portal/**`, so a URL
113
+ * without the organization segment cannot resolve and renders a 404 (#5668).
114
+ * The slug stays optional to keep this signature backward compatible; callers
115
+ * that cannot resolve one should hide the action rather than link to the
116
+ * slugless fallback.
117
+ */
118
+ export function buildPortalRootUrl(origin: string | null | undefined, orgSlug?: string | null): string {
119
+ const base = normalizeOrigin(origin)
120
+ const slug = typeof orgSlug === 'string' ? orgSlug.trim() : ''
121
+ if (!slug) return `${base}/portal`
122
+ return `${base}/${encodeURIComponent(slug)}/portal`
112
123
  }