Skip to content

Commit 17d8db5

Browse files
Switch to nodemailer / resend for transactional mail
1 parent a6fe972 commit 17d8db5

File tree

7 files changed

+48
-61
lines changed

7 files changed

+48
-61
lines changed

packages/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"next": "14.2.21",
108108
"next-auth": "^5.0.0-beta.25",
109109
"next-themes": "^0.3.0",
110+
"nodemailer": "^6.10.0",
110111
"posthog-js": "^1.161.5",
111112
"pretty-bytes": "^6.1.1",
112113
"psl": "^1.15.0",
@@ -128,6 +129,7 @@
128129
"devDependencies": {
129130
"@types/bcrypt": "^5.0.2",
130131
"@types/node": "^20",
132+
"@types/nodemailer": "^6.4.17",
131133
"@types/psl": "^1.1.3",
132134
"@types/react": "^18",
133135
"@types/react-dom": "^18",

packages/web/src/app/login/components/loginForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const LoginForm = ({ callbackUrl, error }: LoginFormProps) => {
4949
}
5050

5151
const onSignInWithMagicLink = (values: z.infer<typeof magicLinkSchema>) => {
52-
signIn("loops", { email: values.email, redirectTo: callbackUrl ?? "/" });
52+
signIn("nodemailer", { email: values.email, redirectTo: callbackUrl ?? "/" });
5353
}
5454

5555
const onSignInWithOauth = useCallback((provider: string) => {

packages/web/src/app/login/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { auth } from "@/auth";
12
import { LoginForm } from "./components/loginForm";
3+
import { redirect } from "next/navigation";
24

35
interface LoginProps {
46
searchParams: {
@@ -8,6 +10,11 @@ interface LoginProps {
810
}
911

1012
export default async function Login({ searchParams }: LoginProps) {
13+
const session = await auth();
14+
if (session) {
15+
return redirect("/");
16+
}
17+
1118
return (
1219
<div className="flex flex-col justify-center items-center h-screen">
1320
<LoginForm callbackUrl={searchParams.callbackUrl} error={searchParams.error} />

packages/web/src/auth.ts

Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@ import NextAuth, { DefaultSession } from "next-auth"
33
import GitHub from "next-auth/providers/github"
44
import Google from "next-auth/providers/google"
55
import Credentials from "next-auth/providers/credentials"
6+
import EmailProvider from "next-auth/providers/nodemailer";
67
import { PrismaAdapter } from "@auth/prisma-adapter"
78
import { prisma } from "@/prisma";
8-
import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, AUTH_LOOPS_KEY, AUTH_LOOPS_TRANSACTIONAL_ID, AUTH_SECRET, AUTH_URL } from "./lib/environment";
9+
import {
10+
AUTH_GITHUB_CLIENT_ID,
11+
AUTH_GITHUB_CLIENT_SECRET,
12+
AUTH_GOOGLE_CLIENT_ID,
13+
AUTH_GOOGLE_CLIENT_SECRET,
14+
AUTH_SECRET,
15+
AUTH_URL,
16+
EMAIL_FROM,
17+
SMTP_CONNECTION_URL
18+
} from "./lib/environment";
919
import { User } from '@sourcebot/db';
1020
import 'next-auth/jwt';
11-
import type { EmailConfig, EmailUserConfig, Provider } from "next-auth/providers";
21+
import type { Provider } from "next-auth/providers";
1222
import { verifyCredentialsRequestSchema, verifyCredentialsResponseSchema } from './lib/schemas';
1323

1424
export const runtime = 'nodejs';
@@ -27,37 +37,6 @@ declare module 'next-auth/jwt' {
2737
}
2838
}
2939

30-
function Loops(config: EmailUserConfig & { transactionalId?: string }): EmailConfig {
31-
return {
32-
id: "loops",
33-
type: "email",
34-
name: "Loops",
35-
36-
maxAge: 24 * 60 * 60,
37-
async sendVerificationRequest(params) {
38-
const { identifier: to, provider, url } = params;
39-
const res = await fetch("https://app.loops.so/api/v1/transactional", {
40-
method: "POST",
41-
headers: {
42-
Authorization: `Bearer ${provider.apiKey}`,
43-
"Content-Type": "application/json",
44-
},
45-
body: JSON.stringify({
46-
transactionalId: config.transactionalId,
47-
email: to,
48-
dataVariables: {
49-
url: url,
50-
},
51-
}),
52-
})
53-
if (!res.ok) {
54-
throw new Error("Loops Send Error: " + JSON.stringify(await res.json()))
55-
}
56-
},
57-
options: config,
58-
}
59-
}
60-
6140
const providers: Provider[] = [
6241
GitHub({
6342
clientId: AUTH_GITHUB_CLIENT_ID,
@@ -67,10 +46,6 @@ const providers: Provider[] = [
6746
clientId: AUTH_GOOGLE_CLIENT_ID,
6847
clientSecret: AUTH_GOOGLE_CLIENT_SECRET,
6948
}),
70-
Loops({
71-
apiKey: AUTH_LOOPS_KEY,
72-
transactionalId: AUTH_LOOPS_TRANSACTIONAL_ID,
73-
}),
7449
Credentials({
7550
credentials: {
7651
email: {},
@@ -103,22 +78,16 @@ const providers: Provider[] = [
10378
image: user.image,
10479
}
10580
}
106-
})
81+
}),
82+
...(SMTP_CONNECTION_URL && EMAIL_FROM ? [
83+
EmailProvider({
84+
server: SMTP_CONNECTION_URL,
85+
from: EMAIL_FROM,
86+
maxAge: 60 * 10,
87+
}),
88+
] : []),
10789
];
10890

109-
// @see: https://authjs.dev/guides/pages/signin
110-
export const providerMap = providers
111-
.map((provider) => {
112-
if (typeof provider === "function") {
113-
const providerData = provider()
114-
return { id: providerData.id, name: providerData.name }
115-
} else {
116-
return { id: provider.id, name: provider.name }
117-
}
118-
})
119-
.filter((provider) => provider.id !== "credentials");
120-
121-
12291
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false;
12392
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost";
12493

packages/web/src/lib/environment.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ export const AUTH_GITHUB_CLIENT_SECRET = getEnv(process.env.AUTH_GITHUB_CLIENT_S
1313
export const AUTH_GOOGLE_CLIENT_ID = getEnv(process.env.AUTH_GOOGLE_CLIENT_ID);
1414
export const AUTH_GOOGLE_CLIENT_SECRET = getEnv(process.env.AUTH_GOOGLE_CLIENT_SECRET);
1515
export const AUTH_URL = getEnv(process.env.AUTH_URL)!;
16-
export const AUTH_LOOPS_KEY = getEnv(process.env.AUTH_LOOPS_KEY);
17-
export const AUTH_LOOPS_TRANSACTIONAL_ID = getEnv(process.env.AUTH_LOOPS_TRANSACTIONAL_ID);
1816

1917
export const STRIPE_SECRET_KEY = getEnv(process.env.STRIPE_SECRET_KEY);
2018
export const STRIPE_PRODUCT_ID = getEnv(process.env.STRIPE_PRODUCT_ID);
2119
export const STRIPE_WEBHOOK_SECRET = getEnv(process.env.STRIPE_WEBHOOK_SECRET);
2220

2321
export const CONFIG_MAX_REPOS_NO_TOKEN = getEnvNumber(process.env.CONFIG_MAX_REPOS_NO_TOKEN, 500);
22+
23+
export const SMTP_CONNECTION_URL = getEnv(process.env.SMTP_CONNECTION_URL);
24+
export const EMAIL_FROM = getEnv(process.env.EMAIL_FROM);

packages/web/src/middleware.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { NextResponse } from "next/server";
2-
import { auth } from "./auth"
1+
import { NextRequest, NextResponse } from "next/server";
32

4-
export default auth((request) => {
3+
export async function middleware(request: NextRequest) {
54
const host = request.headers.get("host")!;
65

76
const searchParams = request.nextUrl.searchParams.toString();
@@ -13,15 +12,12 @@ export default auth((request) => {
1312
host === process.env.NEXT_PUBLIC_ROOT_DOMAIN ||
1413
host === 'localhost:3000'
1514
) {
16-
if (request.nextUrl.pathname === "/login" && request.auth) {
17-
return NextResponse.redirect(new URL("/", request.url));
18-
}
1915
return NextResponse.next();
2016
}
2117

2218
const subdomain = host.split(".")[0];
2319
return NextResponse.rewrite(new URL(`/${subdomain}${path}`, request.url));
24-
});
20+
};
2521

2622

2723
export const config = {

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,6 +2528,13 @@
25282528
dependencies:
25292529
undici-types "~6.19.2"
25302530

2531+
"@types/nodemailer@^6.4.17":
2532+
version "6.4.17"
2533+
resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz#5c82a42aee16a3dd6ea31446a1bd6a447f1ac1a4"
2534+
integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==
2535+
dependencies:
2536+
"@types/node" "*"
2537+
25312538
"@types/prop-types@*":
25322539
version "15.7.13"
25332540
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz"
@@ -5948,6 +5955,11 @@ [email protected]:
59485955
dependencies:
59495956
detect-libc "^2.0.1"
59505957

5958+
nodemailer@^6.10.0:
5959+
version "6.10.0"
5960+
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912"
5961+
integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==
5962+
59515963
nopt@^5.0.0:
59525964
version "5.0.0"
59535965
resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"

0 commit comments

Comments
 (0)