From 1a2e2ff8f1217f2309956a64227470d5e886da9b Mon Sep 17 00:00:00 2001 From: rafavalls Date: Wed, 8 Apr 2026 17:28:18 -0300 Subject: [PATCH] fix(invite): fix invite email styling, org redirect, and no-access UX - Improve invite email HTML with proper inline styles and table layout - Use org slug in redirectTo so acceptance redirects to the correct org - Escape HTML entities in email to prevent XSS (org name, inviter name) - URL-encode invitationId and orgSlug in the accept URL - Show "Access denied" EmptyState instead of infinite splash when user lacks access to an org; clear stale lastOrgSlug before redirecting - Restore correct toast ordering in inbox accept flow (after all async) - Add org list fallback when setActive doesn't return slug (#4006) Co-Authored-By: Claude Sonnet 4.6 --- apps/mesh/src/auth/index.ts | 57 ++++++++++++++++--- .../web/components/sidebar/footer/inbox.tsx | 8 ++- apps/mesh/src/web/layouts/shell-layout.tsx | 20 ++++++- bun.lock | 15 +---- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/apps/mesh/src/auth/index.ts b/apps/mesh/src/auth/index.ts index 4cdeed645e..bc2c821816 100644 --- a/apps/mesh/src/auth/index.ts +++ b/apps/mesh/src/auth/index.ts @@ -130,17 +130,58 @@ if ( const sendEmail = createEmailSender(inviteProvider); sendInvitationEmail = async (data) => { - const inviterName = data.inviter.user?.name || data.inviter.user?.email; - const acceptUrl = `${getBaseUrl()}/auth/accept-invitation?invitationId=${data.invitation.id}&redirectTo=/`; + const esc = (s: string) => + s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + const inviterName = + data.inviter.user?.name || data.inviter.user?.email || ""; + const orgSlug = data.organization.slug ?? ""; + const acceptUrl = `${getBaseUrl()}/auth/accept-invitation?invitationId=${encodeURIComponent(data.invitation.id)}&redirectTo=/${encodeURIComponent(orgSlug)}`; await sendEmail({ to: data.email, - subject: `Invitation to join ${data.organization.name}`, - html: ` -

You've been invited!

-

${inviterName} has invited you to join ${data.organization.name}.

-

Click here to accept the invitation

- `, + subject: `You've been invited to join ${data.organization.name}`, + html: ` + + + + + + + +
+ + + + + + + + + + +
+

Invitation

+

You've been invited to join
${esc(data.organization.name)}

+
+

+ ${esc(inviterName)} has invited you to collaborate on ${esc(data.organization.name)}. +

+ Accept invitation +
+

+ Or copy this link into your browser:
+ ${esc(acceptUrl)} +

+
+
+ +`, }); }; } diff --git a/apps/mesh/src/web/components/sidebar/footer/inbox.tsx b/apps/mesh/src/web/components/sidebar/footer/inbox.tsx index 414b00cc29..9e5fa03bd5 100644 --- a/apps/mesh/src/web/components/sidebar/footer/inbox.tsx +++ b/apps/mesh/src/web/components/sidebar/footer/inbox.tsx @@ -53,11 +53,17 @@ function InvitationItem({ invitation }: { invitation: Invitation }) { toast.error(result.error.message); setIsAccepting(false); } else { + // Get org slug — prefer setActive response, fall back to org list lookup. + // setActive may not always return data if the session hasn't refreshed yet. const setActiveResult = await authClient.organization.setActive({ organizationId: invitation.organizationId, }); + let slug = setActiveResult?.data?.slug; + if (!slug) { + const { data: orgs } = await authClient.organization.list(); + slug = orgs?.find((o) => o.id === invitation.organizationId)?.slug; + } toast.success("Invitation accepted!"); - const slug = setActiveResult?.data?.slug; window.location.href = slug ? `/${slug}` : "/"; } } catch { diff --git a/apps/mesh/src/web/layouts/shell-layout.tsx b/apps/mesh/src/web/layouts/shell-layout.tsx index aae790ba32..64cd9ba65a 100644 --- a/apps/mesh/src/web/layouts/shell-layout.tsx +++ b/apps/mesh/src/web/layouts/shell-layout.tsx @@ -10,7 +10,6 @@ import { Chat, useChatTask } from "@/web/components/chat/index"; import { ChatPanel } from "@/web/components/chat/side-panel-chat"; import { TasksSidePanel } from "@/web/components/chat/side-panel-tasks"; import { ErrorBoundary } from "@/web/components/error-boundary"; -import { SplashScreen } from "@/web/components/splash-screen"; import { KeyboardShortcutsDialog } from "@/web/components/keyboard-shortcuts-dialog"; import { isMac, isModKey } from "@/web/lib/keyboard-shortcuts"; import { StudioSidebar, StudioSidebarMobile } from "@/web/components/sidebar"; @@ -915,7 +914,24 @@ function ShellLayoutContent() { const { data: ssoStatus } = useOrgSsoStatus(orgId); if (!activeOrg) { - return ; + return ( + { + localStorage.removeItem(LOCALSTORAGE_KEYS.lastOrgSlug()); + window.location.href = "/"; + }} + > + Go to your account + + } + /> + ); } if (ssoStatus?.ssoRequired && !ssoStatus.authenticated) { diff --git a/bun.lock b/bun.lock index 50b26c7ade..cecaff07e5 100644 --- a/bun.lock +++ b/bun.lock @@ -53,7 +53,7 @@ }, "apps/mesh": { "name": "decocms", - "version": "2.235.0", + "version": "2.248.8", "bin": { "deco": "./dist/server/cli.js", }, @@ -75,7 +75,6 @@ "@tanstack/react-virtual": "^3.13.21", "ai-sdk-provider-claude-code": "^3.4.4", "ai-sdk-provider-codex-cli": "^1.1.0", - "dompurify": "^3.3.1", "embedded-postgres": "^18.3.0-beta.16", "ink": "^6.8.0", "kysely": "^0.28.12", @@ -137,7 +136,6 @@ "@tiptap/starter-kit": "3.20.2", "@tiptap/suggestion": "3.20.2", "@types/bun": "^1.3.1", - "@types/dompurify": "^3.2.0", "@types/pg": "^8.15.6", "@types/react-syntax-highlighter": "^15.5.13", "@untitledui/icons": "^0.0.19", @@ -152,7 +150,6 @@ "croner": "^9.1.0", "date-fns": "^4.1.0", "degit": "^2.8.4", - "github-markdown-css": "^5.8.1", "hono": "^4.10.7", "input-otp": "^1.4.2", "jose": "^6.0.11", @@ -207,7 +204,7 @@ }, "packages/mcp-utils": { "name": "@decocms/mcp-utils", - "version": "1.0.0", + "version": "1.0.1", "devDependencies": { "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "@modelcontextprotocol/sdk": "1.27.1", @@ -1609,8 +1606,6 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -1957,7 +1952,7 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -2119,8 +2114,6 @@ "git-diff": ["git-diff@2.0.6", "", { "dependencies": { "chalk": "^2.3.2", "diff": "^3.5.0", "loglevel": "^1.6.1", "shelljs": "^0.8.1", "shelljs.exec": "^1.1.7" } }, "sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA=="], - "github-markdown-css": ["github-markdown-css@5.9.0", "", {}, "sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], @@ -3631,8 +3624,6 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], - "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], "p-queue/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],