import { SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_USER, } from "@/config/email/smtp.config"; import { SITE_ORIGIN } from "@/config/site.config"; import { db } from "@/db"; import { accounts, sessions, users, verificationTokens } from "@/db/schema"; import { recordSignIn } from "@/db/users"; import { renderSignInEmail } from "@/lib/signin-email"; import { confirmLinkFor, normalizeEmail, sentTooRecently, SIGN_IN_LINK_MAX_AGE_SECONDS, } from "@/lib/signin-link"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { and, eq, gt, max } from "drizzle-orm"; import NextAuth from "next-auth"; import Nodemailer from "next-auth/providers/nodemailer"; import { createTransport } from "nodemailer"; export const SIGN_IN_PATH = "/signin"; export function checkInboxPath(email: string): string { return `${SIGN_IN_PATH}?${new URLSearchParams({ sent: email })}`; } const server = { host: SMTP_HOST, port: SMTP_PORT, secure: false, auth: { user: SMTP_USER, pass: SMTP_PASSWORD }, }; async function latestTokenExpiry(identifier: string): Promise { const [row] = await db .select({ expires: max(verificationTokens.expires) }) .from(verificationTokens) .where( and( eq(verificationTokens.identifier, identifier), gt(verificationTokens.expires, new Date()), ), ); return row?.expires ?? null; } export const { handlers, auth, signIn, signOut } = NextAuth({ secret: process.env.AUTH_SECRET, trustHost: true, adapter: DrizzleAdapter(db, { usersTable: users, accountsTable: accounts, sessionsTable: sessions, verificationTokensTable: verificationTokens, }), session: { strategy: "database" }, pages: { signIn: SIGN_IN_PATH, error: SIGN_IN_PATH, verifyRequest: SIGN_IN_PATH, }, providers: [ Nodemailer({ id: "email", name: "Email", server, from: SMTP_FROM, maxAge: SIGN_IN_LINK_MAX_AGE_SECONDS, normalizeIdentifier: normalizeEmail, async sendVerificationRequest({ identifier, url }) { const pinned = process.env.VERCEL_ENV === "production" ? SITE_ORIGIN : null; const mail = renderSignInEmail(confirmLinkFor(url, pinned)); await createTransport(server).sendMail({ from: SMTP_FROM, to: identifier, ...mail, }); }, }), ], callbacks: { // Runs before Auth.js stores a token, for every way of asking for a link. // A string answer skips the send and goes to that page instead. async signIn({ user, email }) { if (email?.verificationRequest && user.email) { if (sentTooRecently(await latestTokenExpiry(user.email), new Date())) { return checkInboxPath(user.email); } } return true; }, }, events: { async signIn({ user }) { if (user.email) await recordSignIn(user.email); }, }, });