From 93049e864ae6d281b60c23dee868bca3f585dd4a Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Thu, 10 Jul 2025 08:35:32 +0000 Subject: feat: Add support for email verification --- packages/trpc/email.ts | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 packages/trpc/email.ts (limited to 'packages/trpc/email.ts') diff --git a/packages/trpc/email.ts b/packages/trpc/email.ts new file mode 100644 index 00000000..2ca3e396 --- /dev/null +++ b/packages/trpc/email.ts @@ -0,0 +1,110 @@ +import { randomBytes } from "crypto"; +import { and, eq } from "drizzle-orm"; +import { createTransport } from "nodemailer"; + +import { db } from "@karakeep/db"; +import { verificationTokens } from "@karakeep/db/schema"; +import serverConfig from "@karakeep/shared/config"; + +export async function sendVerificationEmail(email: string, name: string) { + if (!serverConfig.email.smtp) { + throw new Error("SMTP is not configured"); + } + + const token = randomBytes(10).toString("hex"); + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours + + // Store verification token + await db.insert(verificationTokens).values({ + identifier: email, + token, + expires, + }); + + const transporter = createTransport({ + host: serverConfig.email.smtp.host, + port: serverConfig.email.smtp.port, + secure: serverConfig.email.smtp.secure, + auth: + serverConfig.email.smtp.user && serverConfig.email.smtp.password + ? { + user: serverConfig.email.smtp.user, + pass: serverConfig.email.smtp.password, + } + : undefined, + }); + + const verificationUrl = `${serverConfig.publicUrl}/verify-email?token=${encodeURIComponent(token)}&email=${encodeURIComponent(email)}`; + + const mailOptions = { + from: serverConfig.email.smtp.from, + to: email, + subject: "Verify your email address", + html: ` +
+

Welcome to Karakeep, ${name}!

+

Please verify your email address by clicking the link below:

+

+ + Verify Email Address + +

+

If the button doesn't work, you can copy and paste this link into your browser:

+

${verificationUrl}

+

This link will expire in 24 hours.

+

If you didn't create an account with us, please ignore this email.

+
+ `, + text: ` +Welcome to Karakeep, ${name}! + +Please verify your email address by visiting this link: +${verificationUrl} + +This link will expire in 24 hours. + +If you didn't create an account with us, please ignore this email. + `, + }; + + await transporter.sendMail(mailOptions); +} + +export async function verifyEmailToken( + email: string, + token: string, +): Promise { + const verificationToken = await db.query.verificationTokens.findFirst({ + where: (vt, { and, eq }) => + and(eq(vt.identifier, email), eq(vt.token, token)), + }); + + if (!verificationToken) { + return false; + } + + if (verificationToken.expires < new Date()) { + // Clean up expired token + await db + .delete(verificationTokens) + .where( + and( + eq(verificationTokens.identifier, email), + eq(verificationTokens.token, token), + ), + ); + return false; + } + + // Clean up used token + await db + .delete(verificationTokens) + .where( + and( + eq(verificationTokens.identifier, email), + eq(verificationTokens.token, token), + ), + ); + + return true; +} -- cgit v1.2.3-70-g09d2