From 140311d7419fa2192e5149df8f589c3c3733a399 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Thu, 10 Jul 2025 20:50:19 +0000 Subject: feat: Support forget and reset password --- packages/trpc/email.ts | 112 +++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 41 deletions(-) (limited to 'packages/trpc/email.ts') diff --git a/packages/trpc/email.ts b/packages/trpc/email.ts index ded23ed8..1c0b8800 100644 --- a/packages/trpc/email.ts +++ b/packages/trpc/email.ts @@ -1,9 +1,8 @@ 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 { passwordResetTokens, verificationTokens } from "@karakeep/db/schema"; import serverConfig from "@karakeep/shared/config"; export async function sendVerificationEmail(email: string, name: string) { @@ -70,45 +69,6 @@ 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; -} - export async function sendInviteEmail( email: string, token: string, @@ -169,3 +129,73 @@ If you weren't expecting this invitation, you can safely ignore this email. await transporter.sendMail(mailOptions); } + +export async function sendPasswordResetEmail( + email: string, + name: string, + userId: string, +) { + if (!serverConfig.email.smtp) { + throw new Error("SMTP is not configured"); + } + + const token = randomBytes(32).toString("hex"); + const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour + + // Store password reset token + await db.insert(passwordResetTokens).values({ + userId, + 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 resetUrl = `${serverConfig.publicUrl}/reset-password?token=${encodeURIComponent(token)}`; + + const mailOptions = { + from: serverConfig.email.smtp.from, + to: email, + subject: "Reset your password", + html: ` +
+

Password Reset Request

+

Hi ${name},

+

You requested to reset your password for your Karakeep account. Click the link below to reset your password:

+

+ + Reset Password + +

+

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

+

${resetUrl}

+

This link will expire in 1 hour.

+

If you didn't request a password reset, please ignore this email. Your password will remain unchanged.

+
+ `, + text: ` +Hi ${name}, + +You requested to reset your password for your Karakeep account. Visit this link to reset your password: +${resetUrl} + +This link will expire in 1 hour. + +If you didn't request a password reset, please ignore this email. Your password will remain unchanged. + `, + }; + + await transporter.sendMail(mailOptions); + return token; +} -- cgit v1.2.3-70-g09d2