Learn how to create a forgotten password flow in a web application using Next Js, React Js, Node Js and MongoDB.

Honorato Dev
6 min readMay 12, 2024

--

A “forgot password” flow in a web application allows users to reset their passwords if forgotten. Here’s a basic overview:

  • Password Reset Page: Users access the password reset page, typically via a link on the login page, where they enter their email address associated with their account.
  • Email Verification: The application verifies the email against its database to ensure it’s associated with a valid account.
  • Password Reset Email: If the email is valid, the application sends a unique password reset link via email, usually with an expiration time for security purposes.
  • Password Reset: Users click the link received in the email, which directs them to a page where they can input a new password and confirm it.
  • Confirmation: After successfully resetting the password, the application typically displays a confirmation message and redirects the user back to the login page.

Each project architecture requires a specific implementation, which is why this post only covers what a flow would be like in a Next JS project in a functional way.

Step-1: It all starts with a button calling a redirect link to forget-password route on the login page

./login.tsx

<div className="mb-4 text-sm font-sans ">
Forgot-password? &nbsp;
<Link
className=""
href="/forgot-password"
>
Click here
</Link>
</div>

Step-2: In this step we will create a form where the user’s email will be entered by calling an API through a function.

./forget-password




import React, { useState } from 'react';
import axios from 'axios';
import { useForm } from 'react-hook-form';


interface FormType {
email: string;
password?: string;
}

const ForgotPasswordScreen = () => {
const [loading, setLoading] = useState(false);

const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormType>();

const submitHandler = async ({ email }: FormType) => {
setLoading(true);
try {
await axios
.post('/api/auth/forgot-password', { email: email })
.then((res: any) => {
const response = res.data;

if (response.status == 200) {
console.log(response.message);
setLoading(false);

} else if (response.status == 400) {
} else if (response.status == 500) {
console.log(response.message);
setLoading(false);
}
});
} catch (err) {
setLoading(false);
console.log('The error is', err);
}
};
return (
<Layout title="Recover password">
<div className="flex justify-center">
<div className="w-[500px] p-5 rounded-sm shadow-lg bg-white bg-opacity-70">
<h1 className="text-xl lg:text-2xl font-bold">Forgot password ?</h1>
<p className="flex text-sm lg:text-base">
Don&#39;t worry it happens all the time. Write your email
below and we will send you a recovery email.
</p>
<p className="text-xs mt-2">
<span>(OBS: </span>Check your spam box)
</p>
<form onSubmit={handleSubmit(submitHandler)}>
<div className="mb-3 mt-4">
<label htmlFor="email">Email</label>
<input
type="email"
className="w-full mt-1"
placeholder="exemplo@email.com"
id="email"
autoFocus
{...register('email', {
required: 'Please enter a valid email ',
pattern: {
value: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/i,
message: 'Please use a valid email format ',
},
})}
/>
{errors.email && (
<div className="text-red-600">{errors.email.message}</div>
)}
</div>
<div className="mt-5">
<button
className="w-full bg-gray-500 hover:bg-gray-700 p-2 rounded-sm text-white"
disabled={loading}
>
{loading ? 'Processing' : 'Send'}
</button>
</div>
</form>
</div>
</div>
</Layout>
);
};

export default ForgotPasswordScreen;

Step-3: Now we will create the Node API

/api/auth/forgot-password/index



import db from "@/utils/db";
import User from "@/models/User";
import Env from "@/config/env";
import Cryptr from "cryptr";
import cryptoRandomString from "crypto-random-string";
import ForgotPasswordEmail from "@/emails/ForgotPasswordEmail";
import { sendEmail } from "@/config/mail";

import { render } from "@react-email/render";




export default async function POST(req:any, res:any) {
// eslint-disable-next-line no-undef
const payload:ForgotPasswordPayload = await req.body;
if(req.method !== 'POST') {
return;
}
// const { email }:any = await request.body;

// * Check user email first
await db.connect();
const user = await User.findOne({ email: payload.email });
if (user == null) {
res.status(422).json({message: 'Nonexistent user!'},console.log('out'));
await db.disconnect();
return;
}



// * Generate random string
const randomStr = cryptoRandomString({
length: 64,
type: "alphanumeric",
});

user.password_reset_token = randomStr;
await user.save();

// * Encrypt user email
const crypt = new Cryptr(Env.SECRET_KEY);
const encryptedEmail = crypt.encrypt(user.email);

const url = `${Env.APP_URL}/reset-password/${encryptedEmail}?signature=${randomStr}&mail=${encryptedEmail}`;
console.log(url)

try {
const html = render(
ForgotPasswordEmail({
params: {
name: user.name,
email: user.email,
url: url,
},
})
);

// * Send email to user
await sendEmail(payload.email, "Reset Password", html);
return res.json({
status: 200,
message: "Email successfully sent. Please check your email.",
});
} catch (error) {
console.log("the error is", error);
return res.json({
status: 500,
message: "Something went wrong, please try again!",
});
}
}

Step-4: At this point we call the API where the nodemailer library is located so that the email is sent to the user.

npm install nodemailer
./config/mail



import nodemailer from "nodemailer";
import Env from '@/config/env'

export const transporter = nodemailer.createTransport({
host: Env.SMTP_HOST,
port: Number(Env.SMTP_PORT),
secure: false,
auth: {
user: Env.SMTP_USER,
pass: Env.SMTP_PASSWORD,
},
});

//TO send the email

export const sendEmail = async (
to: string,
subject: string,
html: string
): Promise<string | null> => {
const info = await transporter.sendMail({
from: Env.EMAIL_FROM,
to: to,
subject: subject,
html: html,
});

return info ?.messageId;
};

Step-05: Install react-email and configure your best layout

npm i react-email
./emails/ForgotPasswordEmail.tsx



import React from "react";
import { Button } from "@react-email/button";
import { Html } from "@react-email/html";
import { Heading } from "@react-email/heading";
import { Text } from "@react-email/text";
import { Hr } from "@react-email/hr";


export default function ForgotPasswordEmail({
params,
}:{
params: { name: string; url: string; email:string; };
}) {
return (
<Html lang="pt-br">
<Heading as="h2"> Olá {params.name} </Heading>
<Text> We received a request to change passwords. If it wasn't you, ignore this email {''}

</Text>
<Button

href={params.url}
style={{ background: "#000", color: "#FFFFFF", padding: "10px 20px" }}
>
Click aqui
</Button>
<Hr />

<Heading as="h3">Cumprimentos</Heading>
<Text>Albatroz</Text>

</Html>
);
}

Step-6: Create reset-password API.

./api/auth/reset-password



import User from "@/models/User";
//import { NextRequest, NextResponse } from "next/server";
import Cryptr from "cryptr";
import Env from "@/config/env";
import db from "@/utils/db";
import bcrypt from "bcryptjs";



export default async function POST(req: any, res:any) {

console.log('eu aqui oh')

// eslint-disable-next-line no-undef
const payload: ResetPasswordPayload = await req.body;
// eslint-disable-next-line no-undef
//const payload: any = await req.body;

// TODO: You have to add validation here to check both passwords are same

console.log('payload: ',payload)
// * Decrypt string
const crypter = new Cryptr(Env.SECRET_KEY);
console.log('eu aqui oh3')

const email = crypter.decrypt(payload.email);


await db.connect();
const user = await User.findOne({
email: email,
password_reset_token: payload.signature,
});

if (user == null || user == undefined) {
return res.json({
status: 400,
message: "Something went wrong. Please try again .",
});

}

const salt = bcrypt.genSaltSync(10);
user.password = bcrypt.hashSync(payload.password, salt);
user.password_reset_token = '';
await user.save();

return res.json({
status: 200,
message: "Password updated successfully. please log in with new password",
});
}

Step-7: Create reset-passoword page:

./forgot-password.tsx




import React, { useState } from "react";
import axios from "axios";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { useRouter } from "next/router";

const ResetPassword = () => {
const searchParam = useSearchParams();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');

const router = useRouter();
const [loading, setLoading] = useState(false);
const submit = (event: React.FormEvent) => {
event.preventDefault();
setLoading(true);

// Reset errors
setPasswordError('');
setConfirmPasswordError('');

// Check if password is empty
if (!password) {
setPasswordError('Digite a nova senha');
setLoading(false);
return;
}

// Check if password meets certain criteria (e.g., length)
if (password.length < 8) {
setPasswordError('A senha deve ter pelo menos 8 caracteres');
setLoading(false);
return;
}

// Check if confirmPassword is empty
if (!confirmPassword) {
setConfirmPasswordError('Por favor, confirme sua senha');
setLoading(false);
return;
}

// Check if passwords match
if (password !== confirmPassword) {
setConfirmPasswordError('As senhas não correspondem');
setLoading(false);
return;
}


axios
.post("/api/auth/reset-password", {
email: searchParam.get("mail"),
signature: searchParam.get("signature"),
password: password,
password_confirmation: confirmPassword,
})
.then((res) => {
const response = res.data;
if (response.status == 400) {
console.log('Error')
router.push('/');
} else if (response.status == 200) {
console.log('Success')
setLoading(false);
}
})
.catch((err) => {
setLoading(false);
console.log("err..", err);
});
};
return (
<>

<div className="h-screen w-screen flex justify-center items-center">
<div className="w-[500px] p-5 rounded-sm shadow-lg bg-white bg-opacity-70">
<h1 className="text-2xl font-bold">Change password?</h1>

<form onSubmit={submit}>
<div className="mt-5">
<label className="block mb-1">Password</label>
<input
type="password"
value={password}
placeholder="Digite a nova senha"
className="w-full h-10 p-2 border rounded-md outline-red-400"
onChange={(e) => setPassword(e.target.value)}
/>
<span style={{ color: 'red' }}>{passwordError}</span>
</div>
<div className="mt-5">
<label className="block mb-1">Confirm password</label>
<input
type="password"
value={confirmPassword}
placeholder="Confirme a nova senha"
className="w-full h-10 p-2 border rounded-md outline-red-400"
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<span style={{ color: 'red' }}>{confirmPasswordError}</span>
</div>
<div className="mt-5">
<button
className="w-full bg-black p-2 rounded-lg text-white"
disabled={loading}
>
{loading ? "Processing.." : "Confirm"}
</button>
</div>
<div className="mt-5 text-center">
<Link href="/login" className="text-indigo-500 font-semibold">
{" "}
Back
</Link>
</div>
</form>
</div>
</div>
</>
);
}

export default ResetPassword

Step-8: Set the env.ts file:

./config/env.ts


class Env {
static SMTP_HOST: string = process.env.SMTP_HOST!;
static SMTP_PORT: string = process.env.SMTP_PORT!;
static SMTP_USER: string = process.env.SMTP_USER!;
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD!;
static SMTP_SECURE: string = process.env.SMTP_SECURE!;
static EMAIL_FROM: string = process.env.EMAIL_FROM!;
static SECRET_KEY: string = process.env.NEXTAUTH_SECRET!;
static APP_URL: string = process.env.APP_URL!;
}

export default Env

--

--

Honorato Dev

Web Developer. If everyone helps each other, we'll get there together!