Project: Password Reset Token and Email Delivery Flow
In this project, we will build a password-reset pipeline. The system handles user reset requests, generates a single-use token, saves it with a 15-minute expiry in PostgreSQL/Prisma, and emails the reset link.
1. Reset Token Database Schema
Ensure your database contains a model to hold reset tokens:
// schema.prisma snippet
model PasswordResetToken {
id String @id @default(cuid())
email String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
}2. Inbound Reset Handler Component
Create the Server Action or API router that processes request inputs:
// app/actions/resetPassword.ts
"use server";
import { prisma } from "../lib/prisma";
import { resend } from "../lib/resend";
import crypto from "crypto";
export async function requestPasswordReset(formData: FormData) {
const email = formData.get("email") as string;
if (!email) {
return { error: "Please enter your registered email address" };
}
// 1. Check if the user exists in our system database
const userExists = await prisma.user.findUnique({
where: { email },
});
if (!userExists) {
// Prevent account enumeration by returning a success message regardless
return { success: true };
}
// 2. Generate secure token
const token = crypto.randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // Expires in 15 minutes
// Save token, dropping any existing tokens for this user email
await prisma.passwordResetToken.deleteMany({ where: { email } });
await prisma.passwordResetToken.create({
data: { email, token, expiresAt },
});
const resetUrl = `https://mycompany.com/reset-password?token=${token}`;
// 3. Dispatch the reset email
try {
await resend.emails.send({
from: "Security Team <security@mycompany.com>",
to: [email],
subject: "Reset your account password",
html: `
<h2>Password Reset Request</h2>
<p>You requested a password reset. Click the link below to set a new password. This link is valid for 15 minutes only:</p>
<a href="${resetUrl}">Reset Password Now</a>
`,
});
return { success: true };
} catch (err: any) {
console.error("Security email dispatch failed:", err.message);
return { error: "Failed to dispatch recovery email. Try again later." };
}
}3. Verifying and Clearing Token
When the user submits a new password from the frontend reset form:
// app/api/auth/reset/route.ts
import { prisma } from "../../../lib/prisma";
import bcrypt from "bcryptjs";
export async function POST(req: Request) {
const { token, newPassword } = await req.json();
// 1. Verify token exists and is valid
const tokenRecord = await prisma.passwordResetToken.findUnique({
where: { token },
});
if (!tokenRecord || tokenRecord.expiresAt < new Date()) {
return new Response("Invalid or expired token", { status: 400 });
}
// 2. Hash new password and update user record
const hashedPassword = await bcrypt.hash(newPassword, 12);
await prisma.user.update({
where: { email: tokenRecord.email },
data: { hashedPassword },
});
// 3. Delete token to prevent reuse
await prisma.passwordResetToken.delete({
where: { token },
});
return new Response("Password updated successfully", { status: 200 });
}Published on Last updated: