Back to roadmaps resend Course

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: