This is a detailed analysis of how to make a reset for a user when he has forgotten his password and how to send emails from Node JS and validate sending messages. Most of us have experienced the account recovery process at least once — when we forget a password, there’s a need for procedures to create a new one and regain access to the system. This article focuses on implementing such a process using Node.js, Knex, and some undisclosed tools, alongside Express to handle routes and perform the necessary operations. We’ll cover the router implementation, handling URL parameters, determining what to send to the user when only an email or phone number is available as proof, managing email submissions, and addressing security concerns. Forgot Password Flow Before diving into coding, I’d like to ensure that we’re working with the same codebase, which you can access from my public . We will upgrade step by step to implement the forgot password flow. For email transport, we will utilize Google’s email service. repository on GitHub Now, take a look at the schema of the forgot password flow. The server will be responsible for sending emails to the user mailbox containing a valid link for password resetting, and will also validate the token and user existence. Packages and Migration To begin utilizing the email service and sending emails with Node.js, we need to install the following packages in addition to our existing dependencies: npm i --save nodemailer handlebars : Powerful module that allows to send emails easily using SMTP or other transport mechanisms. Nodemailer : Handlebars is a popular templating engine for JavaScript. It will allow us to define templates with placeholders that can be filled with data when rendering. Handlebars Now, we need to create the migration, so in my case, I have to add a new column to table: forgot_password_token users knex migrate:make add_field_forgot_password_token -x ts and in the generated file, I set the code: import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); } Migration for Forgot Password Token in Users table and then migrate the latest file: knex migrate:knex So now we can set to the table our users forgot_password_token Routers To manage controllers responsible for handling the logic of password forgetfulness and resetting, we must establish two routes. The first route initiates the forgot password process, while the second handles the reset process, expecting a token parameter in the URL for verification. To implement this, create a file named within the directory, and insert the following code: forgotPasswordRouter.ts src/routes/ import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController); Forgot Password Router Two controllers will manage the logic for sending emails and resetting the password. Forgot Password Controller When the client forgets his password he has no session, which means we can’t get user data except email or any other security identifiers. In our case, we are sending an email to handle a password reset. That logic we’re going to set into the controller. forgotPasswordRouter.post('/', forgotPasswordController); Remember the ‘forgot password?’ link below the login form usually in the UI of any clients in the login form? Clicking on it directs us to a view where we can request a password reset. We simply input our email, and the controller handles all the necessary procedures. Let’s examine the following code: import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }; Forgot Password Controller From the body, we going to get an email, and then we will find the user using . If the user exists, we create a JWT token using and save the token to the user with an expiration of 1 day. Then we will send the message to the email with a proper link together with a token where the user will be able to change his password. UserModel.findByEmail TokenService.sign forgot_password_token Google Setup To be able to send the email, we have to create our new email address which will be a sender. Let’s go to Google, to create a new email account, and then, when the account is created, proceed to the link. You can find it on the top right by clicking on avatar. Then, on the left menu, click on the item, and then press . Below you will find the section, click on the arrow: Manage your Google Account Security 2-Step Verification App passwords Input the name that needs to be used. In my case, I set and press . Nodemailer Create Copy the generated password, and set it to your file. We need to set to file two variables: .env MAIL_USER="mygoogleemail@gmail.com" MAIL_PASSWORD="vyew hzek avty iwst" Of course, to have a proper email like , you have to set up Google Workspace or AWS Amazon WorkMail together with AWS SES, or any other services. But in our case, we are using a simple Gmail account for free. info@company_name.com Email Service With the file prepared, we are ready to set up our service for sending emails. The controller will use the service with the generated token and the recipient email address for our message. .env await EmailService.sendPasswordResetEmail(email, token); Let’s create and define the class for the service: src/services/EmailService.ts export class EmailService {} And now as initial data, I have to get the environment to use with : nodemailer import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; } Email Service We have to take care of service initialization. I wrote about it before in my previous . Here is an example: article import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); }; Initializing Services Now, let’s proceed with creating the initialization within our class: EmailService import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } } Email Service Initialization There is initialization , a method provided by the library. It creates a transporter object that will be used to send our emails. The method accepts an options object as an argument where you specify the configuration details for the transporter. nodemailer.createTransport() nodemailer We are using Google: specifies the email service provider. Nodemailer provides built-in support for various email service providers, and indicates that the transporter will be configured to work with Gmail's SMTP server. service: 'gmail' gmail For authentication , it’s necessary to set the credentials that are required to access the email service provider's SMTP server. auth For should be set to the email address from which we are going to send emails, and that password has been generated in the Google account from App Passwords. user Now, let’s set the last part of our service: import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } } Send Password Reset Email Before proceeding, it’s crucial to determine the appropriate host for when the client receives an email. Establishing a link with a token in the email body is essential. import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; }; Get host For templates, I am using and for that, we need to create in our first HTML template: handlebars src/temlates/passwordResetTemplate.hbs <!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html> Password Reset Template and now we can reuse this template with the helper: import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); }; Generate Template Helper To enhance our email, we can even include attachments. To do so, add the file to the folder. We can then render this image within the email using the following helper function: email_logo.png src/assets import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; }); Generate Attachments Helper After collecting all of those helpers, we have to be able to send email using: const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); This approach offers decent scalability, enabling the service to employ various methods for sending emails with diverse content. Now, let’s try to trigger the controller with our router and send the email. For that, I am using : Postman The console will tell you that the message has been sent: Message sent: <1k96ah55-c09t-p9k2–8bv2-j25r9h77f763@gmail.com> Check for new messages in the inbox: The link to has to contain the token and host: Reset Password http://localhost:3000/reset-password/<token> The port is specified here because this message pertains to the development process. This indicates that the client responsible for handling forms for password reset will also be operating within the development environment. 3000 Reset Password The token has to be validated on the controller side with TokenService from where we can get the user who sent that email. Let’s recover the router which uses the token: forgotPasswordRouter.post('/reset/:token', resetPasswordController); The controller will only update the password if the token is valid and not expired, as per the expiration time set to one hour. To implement this functionality, navigate to the folder and create a file named containing the following code: src/controllers/ resetPasswordController.ts import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } }; Reset Password Controller This controller will receive the token, verify it, extract the user ID from the decrypted data, retrieve the corresponding user, acquire the new password sent by the client in the request body, and proceed to update the password in the database. Ultimately, this enables the client to log in using the new password. Conclusion The scalability of the email service is demonstrated through various approaches, such as sending confirmations or success messages, like those indicating a password update and enabling subsequent login. However, managing passwords is a great challenge, particularly when enhancing application security is imperative. There are numerous options available to bolster security, including additional checks before permitting password changes, such as token comparison, email, and password validation. Another option is to implement a PIN code system, where a code is sent to the user’s email for validation on the server side. Each of these measures necessitates the utilization of email-sending capabilities. All implemented code you can find in the . GitHub repository here Please feel free to conduct any experiments with this build, and share your feedback on what aspects you appreciate about this topic. Thank you so much. References Here, you can find several references that I utilized in this article: Repository Initialization Services with Node JS Nodemailer Handlebars Knex Express Postman Also published here
Share Your Thoughts