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:
@@ -5,6 +5,7 @@ import { magicLink } from "better-auth/plugins/magic-link";
|
|||||||
import type { dbClient } from "@kan/db/client";
|
import type { dbClient } from "@kan/db/client";
|
||||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
import * as subscriptionRepo from "@kan/db/repository/subscription.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 * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
import { generateUID } from "@kan/shared/utils";
|
import { generateUID } from "@kan/shared/utils";
|
||||||
import { sendEmail } from "@kan/email";
|
import { sendEmail } from "@kan/email";
|
||||||
@@ -133,10 +134,10 @@ export function createPlugins(db: dbClient) {
|
|||||||
|
|
||||||
if (workspace?.id) {
|
if (workspace?.id) {
|
||||||
await memberRepo.pauseAllMembers(db, workspace.id);
|
await memberRepo.pauseAllMembers(db, workspace.id);
|
||||||
|
|
||||||
// Reset slug to publicId, or generate a UID if publicId is taken
|
// Reset slug to publicId, or generate a UID if publicId is taken
|
||||||
let newSlug = workspace.publicId;
|
let newSlug = workspace.publicId;
|
||||||
|
|
||||||
if (workspace.slug !== workspace.publicId) {
|
if (workspace.slug !== workspace.publicId) {
|
||||||
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
|
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
|
||||||
db,
|
db,
|
||||||
@@ -146,7 +147,7 @@ export function createPlugins(db: dbClient) {
|
|||||||
newSlug = generateUID();
|
newSlug = generateUID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await workspaceRepo.update(db, subscription.referenceId, {
|
await workspaceRepo.update(db, subscription.referenceId, {
|
||||||
plan: "free",
|
plan: "free",
|
||||||
slug: newSlug,
|
slug: newSlug,
|
||||||
@@ -171,19 +172,69 @@ export function createPlugins(db: dbClient) {
|
|||||||
magicLink({
|
magicLink({
|
||||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||||
sendMagicLink: async ({ email, url }) => {
|
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(
|
await sendEmail(
|
||||||
email,
|
email,
|
||||||
"Invitation to join workspace",
|
workspaceName
|
||||||
|
? `Invitation to join the workspace ${workspaceName}`
|
||||||
|
: "Invitation to join workspace",
|
||||||
"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,
|
magicLoginUrl: url,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
|
|
||||||
magicLoginUrl: url,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ import * as React from "react";
|
|||||||
|
|
||||||
export const JoinWorkspaceTemplate = ({
|
export const JoinWorkspaceTemplate = ({
|
||||||
magicLoginUrl,
|
magicLoginUrl,
|
||||||
|
inviterName,
|
||||||
|
workspaceName,
|
||||||
}: {
|
}: {
|
||||||
magicLoginUrl?: string;
|
magicLoginUrl?: string;
|
||||||
|
inviterName?: string;
|
||||||
|
workspaceName?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Preview>Log in with this magic link</Preview>
|
<Preview>Join {workspaceName ?? "workspace"} on kan.bn</Preview>
|
||||||
<Body style={{ backgroundColor: "white" }}>
|
<Body style={{ backgroundColor: "white" }}>
|
||||||
<Container
|
<Container
|
||||||
style={{
|
style={{
|
||||||
@@ -38,12 +42,16 @@ export const JoinWorkspaceTemplate = ({
|
|||||||
color: "#232323",
|
color: "#232323",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
kan.bn
|
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && "Kan"}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Heading
|
<Heading
|
||||||
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
|
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>
|
</Heading>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -52,7 +60,7 @@ export const JoinWorkspaceTemplate = ({
|
|||||||
color: "#232323",
|
color: "#232323",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Click the button below to instantly login to your account.
|
Click the button below to add this workspace to your account.
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -71,7 +79,7 @@ export const JoinWorkspaceTemplate = ({
|
|||||||
color: "white",
|
color: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Login to your account
|
Join {workspaceName ?? "workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -80,25 +88,29 @@ export const JoinWorkspaceTemplate = ({
|
|||||||
color: "#7e7e7e",
|
color: "#7e7e7e",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
If you didn't try to login, you can safely ignore this email.
|
If you don't want to join this workspace, 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.
|
|
||||||
</Text>
|
</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>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
Reference in New Issue
Block a user