In this article, you’ll learn how to implement forget/reset passwords with Node.js, Prisma, PostgreSQL, Nodemailer, Redis, Docker-compose, and Pug.
CRUD API with Node.js and PostgreSQL Series:
- API Node.js, TypeScript, Prisma, PostgreSQL: Project Setup
- Node.js + Prisma + PostgreSQL: Access & Refresh Tokens
- CRUD API with Node.js and PostgreSQL: Send HTML Emails
- API with Node.js, Prisma & PostgreSQL: Forget/Reset Password
Forget/Reset Password with Node.js, Prisma, & PostgreSQL
HTTP METHOD | ROUTE | DESCRIPTION |
---|---|---|
POST | /api/auth/forgotpassword | To request a reset token |
PATCH | /api/auth/resetpassword/:resetToken | To reset the password |
To reset the password, the user will make a POST request with his email address to the Node.js server.
The server then validates the email, generates the reset token, and sends the password reset token to the user’s email.
Also, the server returns a success message to the frontend app indicating that the email has been sent.
The user then clicks on the “Reset Password” button upon receiving the reset token email.
The user is then taken to the password reset page where he is required to enter his new password before making a PATCH request to the server.
The server then validates the reset token and updates the user’s password in the database before returning a success message to the frontend app.
The frontend app receives the success message and redirects the user to the login page.
Update Prisma User Model
The forget/reset password mechanism requires the users
table to have some specific columns. To do so edit and add the following fields to the user Prisma schema.
provider
: This column stores a value if the user registered with a Google, GitHub, or Facebook OAuth provider. This step is needed for the upcoming series.passwordResetToken
: This column stores the hashed password reset token.passwordResetAt
: This column stores the timestamp within which the user has to change the password. I decided to give the user 10 minutes to change the password but you can change the time to suit your application.
Lastly, I added a unique constraint and an index on the passwordResetToken
column since we’ll be querying the database by it.
prisma/schema.prisma
model User{
@@map(name: "users")
id String @id @default(uuid())
name String
email String @unique
photo String? @default("default.png")
verified Boolean? @default(false)
password String
role RoleEnumType? @default(user)
verificationCode String? @db.Text @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
provider String?
passwordResetToken String?
passwordResetAt DateTime?
@@unique([email, verificationCode, passwordResetToken])
@@index([email, verificationCode,passwordResetToken])
}
enum RoleEnumType {
user
admin
}
Create Migration and Update the PostgreSQL Database
Once you’ve updated the user schema you need to create a new Prisma migration to reflect the changes before pushing it to the PostgreSQL database.
The PostgreSQL database docker container must be running for this to work.
package.json
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
Remember to change the migration name --name user-entity
to help you distinguish between the different migrations in the future.
Run the following command to create and push the Prisma migration to the PostgreSQL database.
yarn db:migrate && yarn db:push
Update the User Schema
In every backend application, it’s always recommended to validate the request body in a middleware before passing it to the controllers or handlers.
You need to validate the request body and send the appropriate error messages when any of the schema rules have been violated.
There’re many validation libraries like Joi, Zod, Yup, etc but I decided to use Zod since it’s a Typescript-first schema validation library with static type inference and I feel comfortable using it in my React and Node.js projects.
Add the following schemas to the user.schema.ts
file.
src/schemas/user.schema.ts
export const forgotPasswordSchema = object({
body: object({
email: string({
required_error: 'Email is required',
}).email('Email is invalid'),
}),
});
export const resetPasswordSchema = object({
params: object({
resetToken: string(),
}),
body: object({
password: string({
required_error: 'Password is required',
}).min(8, 'Password must be more than 8 characters'),
passwordConfirm: string({
required_error: 'Please confirm your password',
}),
}).refine((data) => data.password === data.passwordConfirm, {
message: 'Passwords do not match',
path: ['passwordConfirm'],
}),
});
export type ForgotPasswordInput = TypeOf<typeof forgotPasswordSchema>['body'];
export type ResetPasswordInput = TypeOf<typeof resetPasswordSchema>;
The user.schema.ts
file should now look like this:
src/schemas/user.schema.ts
import { object, string, TypeOf, z } from 'zod';
enum RoleEnumType {
ADMIN = 'admin',
USER = 'user',
}
export const registerUserSchema = object({
body: object({
name: string({
required_error: 'Name is required',
}),
email: string({
required_error: 'Email address is required',
}).email('Invalid email address'),
password: string({
required_error: 'Password is required',
})
.min(8, 'Password must be more than 8 characters')
.max(32, 'Password must be less than 32 characters'),
passwordConfirm: string({
required_error: 'Please confirm your password',
}),
role: z.optional(z.nativeEnum(RoleEnumType)),
}).refine((data) => data.password === data.passwordConfirm, {
path: ['passwordConfirm'],
message: 'Passwords do not match',
}),
});
export const loginUserSchema = object({
body: object({
email: string({
required_error: 'Email address is required',
}).email('Invalid email address'),
password: string({
required_error: 'Password is required',
}).min(8, 'Invalid email or password'),
}),
});
export const verifyEmailSchema = object({
params: object({
verificationCode: string(),
}),
});
export const updateUserSchema = object({
body: object({
name: string({}),
email: string({}).email('Invalid email address'),
password: string({})
.min(8, 'Password must be more than 8 characters')
.max(32, 'Password must be less than 32 characters'),
passwordConfirm: string({}),
role: z.optional(z.nativeEnum(RoleEnumType)),
})
.partial()
.refine((data) => data.password === data.passwordConfirm, {
path: ['passwordConfirm'],
message: 'Passwords do not match',
}),
});
export const forgotPasswordSchema = object({
body: object({
email: string({
required_error: 'Email is required',
}).email('Email is invalid'),
}),
});
export const resetPasswordSchema = object({
params: object({
resetToken: string(),
}),
body: object({
password: string({
required_error: 'Password is required',
}).min(8, 'Password must be more than 8 characters'),
passwordConfirm: string({
required_error: 'Please confirm your password',
}),
}).refine((data) => data.password === data.passwordConfirm, {
message: 'Passwords do not match',
path: ['passwordConfirm'],
}),
});
export type RegisterUserInput = Omit<
TypeOf<typeof registerUserSchema>['body'],
'passwordConfirm'
>;
export type LoginUserInput = TypeOf<typeof loginUserSchema>['body'];
export type VerifyEmailInput = TypeOf<typeof verifyEmailSchema>['params'];
export type UpdateUserInput = TypeOf<typeof updateUserSchema>['body'];
export type ForgotPasswordInput = TypeOf<typeof forgotPasswordSchema>['body'];
export type ResetPasswordInput = TypeOf<typeof resetPasswordSchema>;
Services to Query and Mutate the PostgreSQL Database
Here I defined some services that will be called by the controllers to query and mutate the database state.
In Node.js architecture, it’s recommended to separate the business and application logic.
Most of the business logic should be implemented in the models or services and you should end up with fat models or services and thin controllers.
src/services/user.service.ts
import { PrismaClient, Prisma, User } from '@prisma/client';
import { omit } from 'lodash';
import config from 'config';
import redisClient from '../utils/connectRedis';
import { signJwt } from '../utils/jwt';
export const excludedFields = [
"password",
"verified",
"verificationCode",
"passwordResetAt",
"passwordResetToken",
];
const prisma = new PrismaClient();
export const createUser = async (input: Prisma.UserCreateInput) => {
return (await prisma.user.create({
data: input,
})) as User;
};
export const findUser = async (
where: Partial<Prisma.UserCreateInput>,
select?: Prisma.UserSelect
) => {
return (await prisma.user.findFirst({
where,
select,
})) as User;
};
export const findUniqueUser = async (
where: Prisma.UserWhereUniqueInput,
select?: Prisma.UserSelect
) => {
return (await prisma.user.findUnique({
where,
select,
})) as User;
};
export const updateUser = async (
where: Partial<Prisma.UserWhereUniqueInput>,
data: Prisma.UserUpdateInput,
select?: Prisma.UserSelect
) => {
return (await prisma.user.update({ where, data, select })) as User;
};
export const signTokens = async (user: Prisma.UserCreateInput) => {
// 1. Create Session
redisClient.set(`${user.id}`, JSON.stringify(omit(user, excludedFields)), {
EX: config.get<number>('redisCacheExpiresIn') * 60,
});
// 2. Create Access and Refresh tokens
const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', {
expiresIn: `${config.get<number>('accessTokenExpiresIn')}m`,
});
const refresh_token = signJwt({ sub: user.id }, 'refreshTokenPrivateKey', {
expiresIn: `${config.get<number>('refreshTokenExpiresIn')}m`,
});
return { access_token, refresh_token };
};
Create a Utility Class to Send Emails
Below is the utility class to send the password reset token to the user’s email address.
Please read CRUD API with Node.js and PostgreSQL: Send HTML Emails for a detailed explanation.
import nodemailer from 'nodemailer';
import config from 'config';
import pug from 'pug';
import { convert } from 'html-to-text';
import { Prisma } from '@prisma/client';
const smtp = config.get<{
host: string;
port: number;
user: string;
pass: string;
}>('smtp');
export default class Email {
#firstName: string;
#to: string;
#from: string;
constructor(private user: Prisma.UserCreateInput, private url: string) {
this.#firstName = user.name.split(' ')[0];
this.#to = user.email;
this.#from = `Codevo <admin@admin.com>`;
}
private newTransport() {
// if (process.env.NODE_ENV === 'production') {
// }
return nodemailer.createTransport({
...smtp,
auth: {
user: smtp.user,
pass: smtp.pass,
},
});
}
private async send(template: string, subject: string) {
// Generate HTML template based on the template string
const html = pug.renderFile(`${__dirname}/../views/${template}.pug`, {
firstName: this.#firstName,
subject,
url: this.url,
});
// Create mailOptions
const mailOptions = {
from: this.#from,
to: this.#to,
subject,
text: convert(html),
html,
};
// Send email
const info = await this.newTransport().sendMail(mailOptions);
console.log(nodemailer.getTestMessageUrl(info));
}
async sendVerificationCode() {
await this.send('verificationCode', 'Your account verification code');
}
async sendPasswordResetToken() {
await this.send(
'resetPassword',
'Your password reset token (valid for only 10 minutes)'
);
}
}
Create Controllers
Now it’s time to define the controllers that will send the password reset token and reset the password.
Forgot Password Controller
The forgot password controller is responsible for validating the user’s email, generating the password reset token, and sending the password reset token to the user’s email address.
src/controllers/auth.controller.ts
export const forgotPasswordHandler = async (
req: Request<
Record<string, never>,
Record<string, never>,
ForgotPasswordInput
>,
res: Response,
next: NextFunction
) => {
try {
// Get the user from the collection
const user = await findUser({ email: req.body.email.toLowerCase() });
const message =
'You will receive a reset email if user with that email exist';
if (!user) {
return res.status(200).json({
status: 'success',
message,
});
}
if (!user.verified) {
return res.status(403).json({
status: 'fail',
message: 'Account not verified',
});
}
if (user.provider) {
return res.status(403).json({
status: 'fail',
message:
'We found your account. It looks like you registered with a social auth account. Try signing in with social auth.',
});
}
const resetToken = crypto.randomBytes(32).toString('hex');
const passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
await updateUser(
{ id: user.id },
{
passwordResetToken,
passwordResetAt: new Date(Date.now() + 10 * 60 * 1000),
},
{ email: true }
);
try {
const url = `${config.get<string>('origin')}/resetPassword/${resetToken}`;
await new Email(user, url).sendPasswordResetToken();
res.status(200).json({
status: 'success',
message,
});
} catch (err: any) {
await updateUser(
{ id: user.id },
{ passwordResetToken: null, passwordResetAt: null },
{}
);
return res.status(500).json({
status: 'error',
message: 'There was an error sending email',
});
}
} catch (err: any) {
next(err);
}
};
Below is a summary of what happened in the forgotPasswordHandler
:
- I first retrieved the email from the request body and called the
findUser()
service to check if a user with that email exists in the database. - Next, I generated the password reset token with the Node.js Crypto module and hashed it.
- Lastly, I sent the unhashed reset token to the user’s email and stored the hashed one in the PostgreSQL database.
Create the Password Reset Controller
Now let’s define the resetPasswordHandler
that will validate the reset token and update the user’s password in the PostgreSQL database.
src/controllers/auth.controller.ts
export const resetPasswordHandler = async (
req: Request<
ResetPasswordInput['params'],
Record<string, never>,
ResetPasswordInput['body']
>,
res: Response,
next: NextFunction
) => {
try {
// Get the user from the collection
const passwordResetToken = crypto
.createHash('sha256')
.update(req.params.resetToken)
.digest('hex');
const user = await findUser({
passwordResetToken,
passwordResetAt: {
gt: new Date(),
},
});
if (!user) {
return res.status(403).json({
status: 'fail',
message: 'Invalid token or token has expired',
});
}
const hashedPassword = await bcrypt.hash(req.body.password, 12);
// Change password data
await updateUser(
{
id: user.id,
},
{
password: hashedPassword,
passwordResetToken: null,
passwordResetAt: null,
},
{ email: true }
);
logout(res);
res.status(200).json({
status: 'success',
message: 'Password data updated successfully',
});
} catch (err: any) {
next(err);
}
};
Here is a breakdown of what I did in the above code snippets:
- First, I extracted the reset token from the request params and hashed it.
- Next, I called the
findUser()
service to check if a user with that reset token exists in the database. - Next, I hashed the new password with BcryptJs and called the
updateUser()
service to update the user’s password in the database. - Also, I set the
passwordResetToken
andpasswordResetAt
to null to prevent a user from resetting the password twice. - Lastly, I logged the user out of the application by sending expired cookies.
Add the routes
Next, add the following routes to the auth.routes.ts
file. Also, remember to call the schema validation middleware before the controllers.
src/routes/auth.routes.ts
router.post(
'/forgotpassword',
validate(forgotPasswordSchema),
forgotPasswordHandler
);
router.patch(
'/resetpassword/:resetToken',
validate(resetPasswordSchema),
resetPasswordHandler
);
The auth.routes.ts
file should now look like this:
src/routes/auth.routes.ts
import express from 'express';
import {
forgotPasswordHandler,
loginUserHandler,
logoutUserHandler,
refreshAccessTokenHandler,
registerUserHandler,
resetPasswordHandler,
verifyEmailHandler,
} from '../controllers/auth.controller';
import { deserializeUser } from '../middleware/deserializeUser';
import { requireUser } from '../middleware/requireUser';
import { validate } from '../middleware/validate';
import {
forgotPasswordSchema,
loginUserSchema,
registerUserSchema,
resetPasswordSchema,
verifyEmailSchema,
} from '../schemas/user.schema';
const router = express.Router();
router.post('/register', validate(registerUserSchema), registerUserHandler);
router.post('/login', validate(loginUserSchema), loginUserHandler);
router.get('/refresh', refreshAccessTokenHandler);
router.get(
'/verifyemail/:verificationCode',
validate(verifyEmailSchema),
verifyEmailHandler
);
router.get('/logout', deserializeUser, requireUser, logoutUserHandler);
router.post(
'/forgotpassword',
validate(forgotPasswordSchema),
forgotPasswordHandler
);
router.patch(
'/resetpassword/:resetToken',
validate(resetPasswordSchema),
resetPasswordHandler
);
export default router;
Conclusion
In this article, you learned how to implement forget/reset password functionalities with Node.js, Prisma, PostgreSQL, Nodemailer, Redis, Pug, and Docker-compose.
Check out the source codes:
Hi,
Frontend code has a posts CRUD API, but backend doesn’t have it.
Please let me know where I can get that code.
Thanks
You can find the backend and frontend code here.
This project’s backend uses TypeORM for database access instead of Prisma.
How can i deploy it to cyclic without the front end?
Yes, you can deploy only the backend on Cyclic, but you need to use a Cloud PostgreSQL connection URL in the
.env
file. You can easily obtain a PostgreSQL connection URL from Supabase and use it in the.env
file.