Join Workspace Email improvements (#371)

* fix failing on percent-encoding invitation urls

* feat: add the workspace and inviter names

* fix: respect the NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY environment variable
This commit is contained in:
Matt
2026-02-08 17:34:17 -04:00
committed by GitHub
parent 19798ddbc9
commit 38d0e9495c
2 changed files with 95 additions and 32 deletions

View File

@@ -5,6 +5,7 @@ import { magicLink } from "better-auth/plugins/magic-link";
import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils";
import { sendEmail } from "@kan/email";
@@ -133,10 +134,10 @@ export function createPlugins(db: dbClient) {
if (workspace?.id) {
await memberRepo.pauseAllMembers(db, workspace.id);
// Reset slug to publicId, or generate a UID if publicId is taken
let newSlug = workspace.publicId;
if (workspace.slug !== workspace.publicId) {
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
db,
@@ -146,7 +147,7 @@ export function createPlugins(db: dbClient) {
newSlug = generateUID();
}
}
await workspaceRepo.update(db, subscription.referenceId, {
plan: "free",
slug: newSlug,
@@ -171,19 +172,69 @@ export function createPlugins(db: dbClient) {
magicLink({
expiresIn: 60 * 60 * 24 * 7, // 7 days
sendMagicLink: async ({ email, url }) => {
if (url.includes("type=invite")) {
const decodedUrl = decodeURIComponent(url);
console.log("Sending magic link to:", email, "URL:", url);
console.log(
"Magic link contains invite:",
decodedUrl.includes("type=invite"),
);
if (decodedUrl.includes("type=invite")) {
let inviterName = "";
let workspaceName = "";
try {
const urlObj = new URL(url);
const callbackUrl = urlObj.searchParams.get("callbackURL");
if (callbackUrl) {
const callbackParams = new URL(
callbackUrl,
process.env.NEXT_PUBLIC_BASE_URL,
).searchParams;
const memberPublicId = callbackParams.get("memberPublicId");
if (memberPublicId) {
const member = await memberRepo.getByPublicId(
db,
memberPublicId,
);
if (member) {
const [workspace, inviter] = await Promise.all([
workspaceRepo.getById(db, member.workspaceId),
userRepo.getById(db, member.createdBy),
]);
if (workspace) workspaceName = workspace.name;
if (inviter) inviterName = inviter.name ?? "";
}
}
}
} catch (error) {
console.error("Failed to fetch invite details:", error);
}
await sendEmail(
email,
"Invitation to join workspace",
workspaceName
? `Invitation to join the workspace ${workspaceName}`
: "Invitation to join workspace",
"JOIN_WORKSPACE",
{
magicLoginUrl: url,
inviterName,
workspaceName,
},
);
} else {
await sendEmail(
email,
process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY === "true"
? "Sign in to your account"
: "Sign in to Kan",
"MAGIC_LINK",
{
magicLoginUrl: url,
},
);
} else {
await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
magicLoginUrl: url,
});
}
},
}),

View File

@@ -13,12 +13,16 @@ import * as React from "react";
export const JoinWorkspaceTemplate = ({
magicLoginUrl,
inviterName,
workspaceName,
}: {
magicLoginUrl?: string;
inviterName?: string;
workspaceName?: string;
}) => (
<Html>
<Head />
<Preview>Log in with this magic link</Preview>
<Preview>Join {workspaceName ?? "workspace"} on kan.bn</Preview>
<Body style={{ backgroundColor: "white" }}>
<Container
style={{
@@ -38,12 +42,16 @@ export const JoinWorkspaceTemplate = ({
color: "#232323",
}}
>
kan.bn
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && "Kan"}
</Heading>
<Heading
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
>
Login to your Kan account
{inviterName
? `${inviterName} invited you to join ${
workspaceName ?? "a workspace"
}`
: `You've been invited to join ${workspaceName ?? "a workspace"}`}
</Heading>
<Text
style={{
@@ -52,7 +60,7 @@ export const JoinWorkspaceTemplate = ({
color: "#232323",
}}
>
Click the button below to instantly login to your account.
Click the button below to add this workspace to your account.
</Text>
<Button
target="_blank"
@@ -71,7 +79,7 @@ export const JoinWorkspaceTemplate = ({
color: "white",
}}
>
Login to your account
Join {workspaceName ?? "workspace"}
</Button>
<Text
style={{
@@ -80,25 +88,29 @@ export const JoinWorkspaceTemplate = ({
color: "#7e7e7e",
}}
>
If you didn&apos;t try to login, you can safely ignore this email.
</Text>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
If you don&apos;t want to join this workspace, you can safely ignore this email.
</Text>
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && (
<>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text>
</>
)}
</Container>
</Body>
</Html>