Skip to content

Commit 940d18a

Browse files
authored
Merge pull request #4302 from Dokploy/fix/send-email-cloud-version
feat: implement invitation email functionality for organization creation
2 parents cdd77a0 + c41b69c commit 940d18a

6 files changed

Lines changed: 136 additions & 80 deletions

File tree

apps/dokploy/server/api/routers/organization.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from "@dokploy/server/db";
2-
import { IS_CLOUD } from "@dokploy/server/index";
2+
import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
33
import { TRPCError } from "@trpc/server";
44
import { and, desc, eq, exists } from "drizzle-orm";
55
import { nanoid } from "nanoid";
@@ -325,6 +325,24 @@ export const organizationRouter = createTRPCRouter({
325325
})
326326
.returning();
327327

328+
if (IS_CLOUD && created) {
329+
const host =
330+
process.env.NODE_ENV === "development"
331+
? "http://localhost:3000"
332+
: "https://app.dokploy.com";
333+
const inviteLink = `${host}/invitation?token=${created.id}`;
334+
335+
const org = await db.query.organization.findFirst({
336+
where: eq(organization.id, orgId),
337+
});
338+
339+
await sendInvitationEmail({
340+
email,
341+
inviteLink,
342+
organizationName: org?.name || "organization",
343+
});
344+
}
345+
328346
await audit(ctx, {
329347
action: "create",
330348
resourceType: "organization",

apps/dokploy/server/api/routers/user.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import {
99
getWebServerSettings,
1010
IS_CLOUD,
1111
removeUserById,
12+
renderInvitationEmail,
1213
sendEmailNotification,
1314
sendResendNotification,
1415
updateUser,
1516
} from "@dokploy/server";
1617
import { db } from "@dokploy/server/db";
17-
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
1818
import {
1919
account,
2020
apiAssignPermissions,
@@ -29,6 +29,7 @@ import {
2929
hasPermission,
3030
resolvePermissions,
3131
} from "@dokploy/server/services/permission";
32+
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
3233
import { TRPCError } from "@trpc/server";
3334
import * as bcrypt from "bcrypt";
3435
import { and, asc, eq, gt } from "drizzle-orm";
@@ -639,27 +640,26 @@ export const userRouter = createTRPCRouter({
639640
);
640641

641642
try {
642-
const htmlContent = `
643-
\t\t\t\t<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
644-
\t\t\t\t`;
643+
const toEmail = currentInvitation?.email || "";
644+
const orgName = organization?.name || "organization";
645+
const subject = `You've been invited to join ${orgName} on Dokploy`;
646+
const html = await renderInvitationEmail({
647+
email: toEmail,
648+
inviteLink,
649+
organizationName: orgName,
650+
});
645651

646652
if (email) {
647653
await sendEmailNotification(
648-
{
649-
...email,
650-
toAddresses: [currentInvitation?.email || ""],
651-
},
652-
"Invitation to join organization",
653-
htmlContent,
654+
{ ...email, toAddresses: [toEmail] },
655+
subject,
656+
html,
654657
);
655658
} else if (resend) {
656659
await sendResendNotification(
657-
{
658-
...resend,
659-
toAddresses: [currentInvitation?.email || ""],
660-
},
661-
"Invitation to join organization",
662-
htmlContent,
660+
{ ...resend, toAddresses: [toEmail] },
661+
subject,
662+
html,
663663
);
664664
}
665665
} catch (error) {

packages/server/src/emails/emails/invitation.tsx

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@ import {
1414
Text,
1515
} from "@react-email/components";
1616

17-
export type TemplateProps = {
18-
email: string;
19-
name: string;
20-
};
21-
22-
interface VercelInviteUserEmailProps {
17+
interface InvitationEmailProps {
2318
inviteLink: string;
2419
toEmail: string;
20+
organizationName: string;
2521
}
2622

2723
export const InvitationEmail = ({
2824
inviteLink,
2925
toEmail,
30-
}: VercelInviteUserEmailProps) => {
31-
const previewText = "Join to Dokploy";
26+
organizationName = "an organization",
27+
}: InvitationEmailProps) => {
28+
const previewText = `You've been invited to join ${organizationName} on Dokploy`;
3229
return (
3330
<Html>
3431
<Head />
@@ -44,50 +41,67 @@ export const InvitationEmail = ({
4441
},
4542
}}
4643
>
47-
<Body className="bg-white my-auto mx-auto font-sans px-2">
48-
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
49-
<Section className="mt-[32px]">
44+
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
45+
<Container className="my-[40px] mx-auto max-w-[520px]">
46+
{/* Header */}
47+
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
5048
<Img
51-
src={
52-
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
53-
}
54-
width="100"
55-
height="50"
49+
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
50+
width="190"
51+
height="120"
5652
alt="Dokploy"
5753
className="my-0 mx-auto"
5854
/>
5955
</Section>
60-
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
61-
Join to <strong>Dokploy</strong>
62-
</Heading>
63-
<Text className="text-black text-[14px] leading-[24px]">
64-
Hello,
65-
</Text>
66-
<Text className="text-black text-[14px] leading-[24px]">
67-
You have been invited to join <strong>Dokploy</strong>, a platform
68-
that helps for deploying your apps to the cloud.
69-
</Text>
70-
<Section className="text-center mt-[32px] mb-[32px]">
71-
<Button
72-
href={inviteLink}
73-
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
74-
>
75-
Join the team 🚀
76-
</Button>
56+
57+
{/* Body */}
58+
<Section className="bg-white px-[40px] py-[32px]">
59+
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
60+
You've been invited to join {organizationName}
61+
</Heading>
62+
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
63+
You have been invited to join{" "}
64+
<strong className="text-[#09090b]">{organizationName}</strong>{" "}
65+
on Dokploy, the platform for deploying your apps to the cloud.
66+
Click the button below to accept the invitation.
67+
</Text>
68+
69+
{/* CTA Button */}
70+
<Section className="text-center mb-[24px]">
71+
<Button
72+
href={inviteLink}
73+
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
74+
>
75+
Accept Invitation
76+
</Button>
77+
</Section>
78+
79+
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center mb-[16px]">
80+
If the button above doesn't work, copy and paste the following
81+
link into your browser:
82+
</Text>
83+
<Text className="text-[#71717a] text-[12px] leading-[18px] m-0 text-center break-all">
84+
{inviteLink}
85+
</Text>
86+
</Section>
87+
88+
{/* Footer */}
89+
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
90+
<Hr className="border border-solid border-[#e4e4e7] my-0 mb-[16px] mx-0 w-full" />
91+
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
92+
This invitation was intended for{" "}
93+
<span className="text-[#71717a]">{toEmail}</span>. This invite
94+
was sent from{" "}
95+
<Link
96+
href="https://dokploy.com"
97+
className="text-[#71717a] underline"
98+
>
99+
Dokploy Cloud
100+
</Link>
101+
. If you were not expecting this invitation, you can safely
102+
ignore this email.
103+
</Text>
77104
</Section>
78-
<Text className="text-black text-[14px] leading-[24px]">
79-
or copy and paste this URL into your browser:{" "}
80-
<Link href={inviteLink} className="text-blue-600 no-underline">
81-
https://dokploy.com
82-
</Link>
83-
</Text>
84-
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
85-
<Text className="text-[#666666] text-[12px] leading-[24px]">
86-
This invitation was intended for {toEmail}. This invite was sent
87-
from <strong className="text-black">dokploy.com</strong>. If you
88-
were not expecting this invitation, you can ignore this email. If
89-
you are concerned about your account's safety, please reply to
90-
</Text>
91105
</Container>
92106
</Body>
93107
</Tailwind>

packages/server/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup";
108108
export * from "./utils/notifications/dokploy-restart";
109109
export * from "./utils/notifications/server-threshold";
110110
export * from "./utils/notifications/utils";
111+
export * from "./verification/send-verification-email";
111112
export * from "./utils/process/execAsync";
112113
export * from "./utils/process/spawnAsync";
113114
export * from "./utils/providers/bitbucket";

packages/server/src/lib/auth.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -409,23 +409,6 @@ const { handler, api } = betterAuth({
409409
enabled: true,
410410
maximumRolesPerOrganization: 10,
411411
},
412-
async sendInvitationEmail(data, _request) {
413-
if (IS_CLOUD) {
414-
const host =
415-
process.env.NODE_ENV === "development"
416-
? "http://localhost:3000"
417-
: "https://app.dokploy.com";
418-
const inviteLink = `${host}/invitation?token=${data.id}`;
419-
420-
await sendEmail({
421-
email: data.email,
422-
subject: "Invitation to join organization",
423-
text: `
424-
<p>You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
425-
`,
426-
});
427-
}
428-
},
429412
}),
430413
...(IS_CLOUD
431414
? [

packages/server/src/verification/send-verification-email.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { renderAsync } from "@react-email/components";
2+
import InvitationEmail from "../emails/emails/invitation";
23
import VerifyEmailTemplate from "../emails/emails/verify-email";
34
import { sendEmailNotification } from "../utils/notifications/utils";
45

@@ -51,3 +52,42 @@ export const sendVerificationEmail = async ({
5152
text: html,
5253
});
5354
};
55+
56+
export const renderInvitationEmail = async ({
57+
email,
58+
inviteLink,
59+
organizationName,
60+
}: {
61+
email: string;
62+
inviteLink: string;
63+
organizationName: string;
64+
}) => {
65+
return renderAsync(
66+
InvitationEmail({
67+
inviteLink,
68+
toEmail: email,
69+
organizationName,
70+
}),
71+
);
72+
};
73+
74+
export const sendInvitationEmail = async ({
75+
email,
76+
inviteLink,
77+
organizationName,
78+
}: {
79+
email: string;
80+
inviteLink: string;
81+
organizationName: string;
82+
}) => {
83+
const html = await renderInvitationEmail({
84+
email,
85+
inviteLink,
86+
organizationName,
87+
});
88+
await sendEmail({
89+
email,
90+
subject: `You've been invited to join ${organizationName} on Dokploy`,
91+
text: html,
92+
});
93+
};

0 commit comments

Comments
 (0)