Add invite email (#209)

This commit is contained in:
Brendan Kellam 2025-02-24 17:50:31 -08:00 committed by GitHub
parent e1f7cd90ac
commit 72da582145
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 237 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

View file

@ -19,9 +19,12 @@ import { headers } from "next/headers"
import { getStripe } from "@/lib/stripe"
import { getUser } from "@/data/user";
import { Session } from "next-auth";
import { STRIPE_PRODUCT_ID, CONFIG_MAX_REPOS_NO_TOKEN } from "@/lib/environment";
import { STRIPE_PRODUCT_ID, CONFIG_MAX_REPOS_NO_TOKEN, EMAIL_FROM, SMTP_CONNECTION_URL } from "@/lib/environment";
import Stripe from "stripe";
import { OnboardingSteps } from "./lib/constants";
import { render } from "@react-email/components";
import InviteUserEmail from "./emails/inviteUserEmail";
import { createTransport } from "nodemailer";
const ajv = new Ajv({
validateFormats: false,
@ -553,18 +556,71 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
} satisfies ServiceError;
}
await prisma.$transaction(async (tx) => {
for (const email of emails) {
await tx.invite.create({
data: {
recipientEmail: email,
hostUserId: session.user.id,
orgId,
}
});
}
await prisma.invite.createMany({
data: emails.map((email) => ({
recipientEmail: email,
hostUserId: session.user.id,
orgId,
})),
skipDuplicates: true,
});
// Send invites to recipients
if (SMTP_CONNECTION_URL && EMAIL_FROM) {
const origin = (await headers()).get('origin')!;
await Promise.all(emails.map(async (email) => {
const invite = await prisma.invite.findUnique({
where: {
recipientEmail_orgId: {
recipientEmail: email,
orgId,
},
},
include: {
org: true,
}
});
if (!invite) {
return;
}
const recipient = await prisma.user.findUnique({
where: {
email,
},
});
const inviteLink = `${origin}/redeem?invite_id=${invite.id}`;
const transport = createTransport(SMTP_CONNECTION_URL);
const html = await render(InviteUserEmail({
baseUrl: 'https://sourcebot.app',
host: {
name: session.user.name ?? undefined,
email: session.user.email!,
avatarUrl: session.user.image ?? undefined,
},
recipient: {
name: recipient?.name ?? undefined,
},
orgName: invite.org.name,
orgImageUrl: invite.org.imageUrl ?? undefined,
inviteLink,
}));
const result = await transport.sendMail({
to: email,
from: EMAIL_FROM,
subject: `Join ${invite.org.name} on Sourcebot`,
html,
text: `Join ${invite.org.name} on Sourcebot by clicking here: ${inviteLink}`,
});
const failed = result.rejected.concat(result.pending).filter(Boolean);
if (failed.length > 0) {
console.error(`Failed to send invite email to ${email}: ${failed}`);
}
}));
}
return {
success: true,

View file

@ -23,7 +23,7 @@ import type { Provider } from "next-auth/providers";
import { verifyCredentialsRequestSchema, verifyCredentialsResponseSchema } from './lib/schemas';
import { createTransport } from 'nodemailer';
import { render } from '@react-email/render';
import MagicLinkEmail from './emails/magicLink';
import MagicLinkEmail from './emails/magicLinkEmail';
export const runtime = 'nodejs';

View file

@ -0,0 +1,20 @@
import {
Hr,
Link,
Section,
Text,
} from '@react-email/components';
export const EmailFooter = () => {
return (
<Section className="mt-[10px]">
<Hr className="border border-solid border-[#eaeaea] mx-0 w-full" />
<Text className="text-[#666666] text-[12px] leading-[24px]">
<Link href="https://sourcebot.dev" className="underline text-[#666666]" target="_blank">
Sourcebot.dev,
</Link>
&nbsp;blazingly fast code search.
</Text>
</Section>
)
}

View file

@ -0,0 +1,145 @@
import {
Body,
Button,
Column,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Row,
Section,
Tailwind,
Text,
} from '@react-email/components';
import { EmailFooter } from './emailFooter';
interface InviteUserEmailProps {
inviteLink: string;
baseUrl: string;
host: {
email: string;
name?: string;
avatarUrl?: string;
},
recipient: {
name?: string;
},
orgName: string;
orgImageUrl?: string;
}
export const InviteUserEmail = ({
baseUrl,
host,
recipient,
orgName,
orgImageUrl,
inviteLink,
}: InviteUserEmailProps) => {
const previewText = `Join ${host.name ?? host.email} on Sourcebot`;
return (
<Html>
<Head />
<Tailwind>
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Preview>{previewText}</Preview>
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={`${baseUrl}/sb_logo_light_large.png`}
width="auto"
height="60"
alt="Sourcebot Logo"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Join <strong>{orgName}</strong> on <strong>Sourcebot</strong>
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
{`Hello${recipient.name ? ` ${recipient.name.split(' ')[0]}` : ''},`}
</Text>
<Text className="text-black text-[14px] leading-[24px]">
<InvitedByText email={host.email} name={host.name} /> has invited you to the <strong>{orgName}</strong> organization on{' '}
<strong>Sourcebot</strong>.
</Text>
<Section>
<Row>
<Column align="right">
<Img
className="rounded-full"
src={host.avatarUrl ? host.avatarUrl : `${baseUrl}/placeholder_avatar.png`}
width="64"
height="64"
/>
</Column>
<Column align="center">
<Img
src={`${baseUrl}/arrow.png`}
width="12"
height="9"
alt="invited you to"
/>
</Column>
<Column align="left">
<Img
className="rounded-full"
src={orgImageUrl ? orgImageUrl : `${baseUrl}/placeholder_avatar.png`}
width="64"
height="64"
/>
</Column>
</Row>
</Section>
<Section className="text-center mt-[32px] mb-[32px]">
<Button
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
href={inviteLink}
>
Join the organization
</Button>
</Section>
<Text className="text-black text-[14px] leading-[24px]">
or copy and paste this URL into your browser:{' '}
<Link href={inviteLink} className="text-blue-600 no-underline">
{inviteLink}
</Link>
</Text>
<EmailFooter />
</Container>
</Body>
</Tailwind>
</Html>
);
};
const InvitedByText = ({ email, name }: { email: string, name?: string }) => {
const emailElement = <Link href={`mailto:${email}`} className="text-blue-600 no-underline">{email}</Link>;
if (name) {
const firstName = name.split(' ')[0];
return <span><strong>{firstName}</strong> ({emailElement})</span>;
}
return emailElement;
}
InviteUserEmail.PreviewProps = {
baseUrl: 'http://localhost:3000',
host: {
name: 'Alan Turing',
email: 'alan.turing@example.com',
// avatarUrl: `http://localhost:3000/arrow.png`,
},
recipient: {
// name: 'alanturing',
},
orgName: 'Enigma',
orgImageUrl: `http://localhost:3000/arrow.png`,
inviteLink: 'https://localhost:3000/redeem?invite_id=1234',
} satisfies InviteUserEmailProps;
export default InviteUserEmail;

View file

@ -1,7 +1,6 @@
import {
Body,
Container,
Hr,
Img,
Link,
Preview,
@ -9,7 +8,7 @@ import {
Tailwind,
Text,
} from '@react-email/components';
import { EmailFooter } from './emailFooter';
interface MagicLinkEmailProps {
magicLink: string,
@ -23,7 +22,7 @@ export const MagicLinkEmail = ({
<Tailwind>
<Preview>Log in to Sourcebot</Preview>
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="my-[40px] mx-auto p-[20px] max-w-[465px]">
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={`${baseUrl}/sb_logo_light_large.png`}
@ -34,7 +33,7 @@ export const MagicLinkEmail = ({
/>
</Section>
<Text className="text-black text-[14px] leading-[24px]">
Hey there,
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
You can log in to your Sourcebot account by clicking the link below.
@ -53,13 +52,7 @@ export const MagicLinkEmail = ({
<Text className="text-black text-[14px] leading-[24px]">
If you didn&apos;t try to login, you can safely ignore this email.
</Text>
<Hr className="border border-solid border-[#eaeaea] my-[10px] mx-0 w-full" />
<Text className="text-[#666666] text-[12px] leading-[24px]">
<Link href="https://sourcebot.dev" className="underline text-[#666666]" target="_blank">
Sourcebot.dev,
</Link>
&nbsp;blazingly fast code search.
</Text>
<EmailFooter />
</Container>
</Body>
</Tailwind>