From b610f7aeffccbb6f841bcb73ec50a4b68733d152 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 24 Apr 2026 21:40:08 -0600 Subject: [PATCH 1/2] feat: implement invitation email functionality for organization creation - Added `sendInvitationEmail` function to send invitation emails when a new organization is created in the cloud environment. - Updated email template to enhance the invitation message and included a direct link for users to accept the invitation. - Refactored email sending logic in the user router to utilize the new invitation email rendering function. - Improved organization invitation email design for better user experience. --- .../server/api/routers/organization.ts | 20 +++- apps/dokploy/server/api/routers/user.ts | 32 +++--- .../server/src/emails/emails/invitation.tsx | 106 ++++++++++-------- packages/server/src/index.ts | 1 + packages/server/src/lib/auth.ts | 17 --- .../verification/send-verification-email.tsx | 36 ++++++ 6 files changed, 132 insertions(+), 80 deletions(-) diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 2f9da6d71..51c1fec5d 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -1,5 +1,5 @@ import { db } from "@dokploy/server/db"; -import { IS_CLOUD } from "@dokploy/server/index"; +import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, exists } from "drizzle-orm"; import { nanoid } from "nanoid"; @@ -325,6 +325,24 @@ export const organizationRouter = createTRPCRouter({ }) .returning(); + if (IS_CLOUD && created) { + const host = + process.env.NODE_ENV === "development" + ? "http://localhost:3000" + : "https://app.dokploy.com"; + const inviteLink = `${host}/invitation?token=${created.id}`; + + const org = await db.query.organization.findFirst({ + where: eq(organization.id, orgId), + }); + + await sendInvitationEmail({ + email, + inviteLink, + organizationName: org?.name || "organization", + }); + } + await audit(ctx, { action: "create", resourceType: "organization", diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 63578b099..93b7e6cf6 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -9,12 +9,12 @@ import { getWebServerSettings, IS_CLOUD, removeUserById, + renderInvitationEmail, sendEmailNotification, sendResendNotification, updateUser, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; -import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { account, apiAssignPermissions, @@ -29,6 +29,7 @@ import { hasPermission, resolvePermissions, } from "@dokploy/server/services/permission"; +import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { and, asc, eq, gt } from "drizzle-orm"; @@ -639,27 +640,26 @@ export const userRouter = createTRPCRouter({ ); try { - const htmlContent = ` -\t\t\t\t

You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: Accept Invitation

-\t\t\t\t`; + const toEmail = currentInvitation?.email || ""; + const orgName = organization?.name || "organization"; + const subject = `You've been invited to join ${orgName} on Dokploy`; + const html = await renderInvitationEmail({ + email: toEmail, + inviteLink, + organizationName: orgName, + }); if (email) { await sendEmailNotification( - { - ...email, - toAddresses: [currentInvitation?.email || ""], - }, - "Invitation to join organization", - htmlContent, + { ...email, toAddresses: [toEmail] }, + subject, + html, ); } else if (resend) { await sendResendNotification( - { - ...resend, - toAddresses: [currentInvitation?.email || ""], - }, - "Invitation to join organization", - htmlContent, + { ...resend, toAddresses: [toEmail] }, + subject, + html, ); } } catch (error) { diff --git a/packages/server/src/emails/emails/invitation.tsx b/packages/server/src/emails/emails/invitation.tsx index 833b77286..dd075aecf 100644 --- a/packages/server/src/emails/emails/invitation.tsx +++ b/packages/server/src/emails/emails/invitation.tsx @@ -14,21 +14,18 @@ import { Text, } from "@react-email/components"; -export type TemplateProps = { - email: string; - name: string; -}; - -interface VercelInviteUserEmailProps { +interface InvitationEmailProps { inviteLink: string; toEmail: string; + organizationName: string; } export const InvitationEmail = ({ inviteLink, toEmail, -}: VercelInviteUserEmailProps) => { - const previewText = "Join to Dokploy"; + organizationName = "an organization", +}: InvitationEmailProps) => { + const previewText = `You've been invited to join ${organizationName} on Dokploy`; return ( @@ -44,50 +41,67 @@ export const InvitationEmail = ({ }, }} > - - -
+ + + {/* Header */} +
Dokploy
- - Join to Dokploy - - - Hello, - - - You have been invited to join Dokploy, a platform - that helps for deploying your apps to the cloud. - -
- + + {/* Body */} +
+ + You've been invited to join {organizationName} + + + You have been invited to join{" "} + {organizationName}{" "} + on Dokploy, the platform for deploying your apps to the cloud. + Click the button below to accept the invitation. + + + {/* CTA Button */} +
+ +
+ + + If the button above doesn't work, copy and paste the following + link into your browser: + + + {inviteLink} + +
+ + {/* Footer */} +
+
+ + This invitation was intended for{" "} + {toEmail}. This invite + was sent from{" "} + + Dokploy Cloud + + . If you were not expecting this invitation, you can safely + ignore this email. +
- - or copy and paste this URL into your browser:{" "} - - https://dokploy.com - - -
- - This invitation was intended for {toEmail}. This invite was sent - from dokploy.com. If you - were not expecting this invitation, you can ignore this email. If - you are concerned about your account's safety, please reply to - diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e6fd0ba59..717c20246 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup"; export * from "./utils/notifications/dokploy-restart"; export * from "./utils/notifications/server-threshold"; export * from "./utils/notifications/utils"; +export * from "./verification/send-verification-email"; export * from "./utils/process/execAsync"; export * from "./utils/process/spawnAsync"; export * from "./utils/providers/bitbucket"; diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index afbc57881..069be48cc 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -409,23 +409,6 @@ const { handler, api } = betterAuth({ enabled: true, maximumRolesPerOrganization: 10, }, - async sendInvitationEmail(data, _request) { - if (IS_CLOUD) { - const host = - process.env.NODE_ENV === "development" - ? "http://localhost:3000" - : "https://app.dokploy.com"; - const inviteLink = `${host}/invitation?token=${data.id}`; - - await sendEmail({ - email: data.email, - subject: "Invitation to join organization", - text: ` -

You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: Accept Invitation

- `, - }); - } - }, }), ...(IS_CLOUD ? [ diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx index 098789a2b..fe9cd6ee6 100644 --- a/packages/server/src/verification/send-verification-email.tsx +++ b/packages/server/src/verification/send-verification-email.tsx @@ -1,4 +1,5 @@ import { renderAsync } from "@react-email/components"; +import InvitationEmail from "../emails/emails/invitation"; import VerifyEmailTemplate from "../emails/emails/verify-email"; import { sendEmailNotification } from "../utils/notifications/utils"; @@ -51,3 +52,38 @@ export const sendVerificationEmail = async ({ text: html, }); }; + +export const renderInvitationEmail = async ({ + email, + inviteLink, + organizationName, +}: { + email: string; + inviteLink: string; + organizationName: string; +}) => { + return renderAsync( + InvitationEmail({ + inviteLink, + toEmail: email, + organizationName, + }), + ); +}; + +export const sendInvitationEmail = async ({ + email, + inviteLink, + organizationName, +}: { + email: string; + inviteLink: string; + organizationName: string; +}) => { + const html = await renderInvitationEmail({ email, inviteLink, organizationName }); + await sendEmail({ + email, + subject: `You've been invited to join ${organizationName} on Dokploy`, + text: html, + }); +}; From c41b69c925d88dab8dffc228882cf3a56a7a185d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:40:50 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- .../server/src/verification/send-verification-email.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx index fe9cd6ee6..e6ae0250a 100644 --- a/packages/server/src/verification/send-verification-email.tsx +++ b/packages/server/src/verification/send-verification-email.tsx @@ -80,7 +80,11 @@ export const sendInvitationEmail = async ({ inviteLink: string; organizationName: string; }) => { - const html = await renderInvitationEmail({ email, inviteLink, organizationName }); + const html = await renderInvitationEmail({ + email, + inviteLink, + organizationName, + }); await sendEmail({ email, subject: `You've been invited to join ${organizationName} on Dokploy`,