NestJS API: Creating A Password Change Route

by SLV Team 45 views

Hey guys! Ever needed to let your users change their passwords in your NestJS API? It's a super common feature, and today, we're going to break down exactly how to implement it. We'll walk through setting up the route, handling the logic, and making sure everything is secure. So, let's dive in and get this done!

Why a Password Change Route is Essential

Before we jump into the code, let's quickly chat about why this feature is so important. In today's digital world, security is key. Users need to be able to update their passwords regularly to keep their accounts safe. If a user suspects their password might be compromised, they need a straightforward way to change it.

  • Enhanced Security: Providing a password change route is a fundamental security practice. It empowers users to take control of their account security. Regular password updates can significantly reduce the risk of unauthorized access and data breaches. This is especially crucial in applications that handle sensitive user data, such as personal information, financial details, or confidential communications.
  • User Empowerment: Allowing users to change their passwords themselves gives them a sense of control over their accounts. This autonomy fosters trust and confidence in your application. Users appreciate the ability to proactively manage their security settings, and it can lead to increased user satisfaction and engagement. When users feel they have control, they are more likely to remain active and loyal to your platform.
  • Compliance and Best Practices: Many security standards and compliance regulations mandate that applications provide a mechanism for users to change their passwords. For example, GDPR (General Data Protection Regulation) and other privacy laws emphasize the importance of user data protection and control. Implementing a password change feature helps your application adhere to these requirements and demonstrates your commitment to user privacy and security.
  • Improved User Experience: A well-designed password change process enhances the overall user experience. It should be easy to find, simple to use, and provide clear feedback to the user. A smooth and intuitive process can reduce frustration and improve user satisfaction. By making password updates straightforward, you encourage users to take the necessary steps to protect their accounts without hassle.
  • Reduced Support Burden: By offering a self-service password change option, you can significantly reduce the burden on your support team. Users can resolve their password-related issues independently, freeing up support staff to focus on more complex inquiries. This efficiency not only saves time and resources but also improves the responsiveness of your support system.

In short, a password change route isn't just a nice-to-have feature – it's a must-have for any application that values security and user trust. Now that we understand the importance, let's get into the nitty-gritty of building this in NestJS.

Setting Up the NestJS Project

Okay, first things first, let's make sure we have a NestJS project up and running. If you already have one, awesome! If not, no worries, it's super easy to set up. You'll need Node.js and npm (or yarn) installed. Then, just run these commands:

npm i -g @nestjs/cli
nest new project-name
cd project-name
npm install

This will create a new NestJS project with all the basic files and dependencies. Once that's done, let's dive into the modules and services we'll need.

Core Modules and Services

For our password change feature, we'll need a few key components. Think of these as the building blocks of our functionality:

  • User Module: This module will handle all user-related operations. It's where we'll define our user model, service, and controller.
  • Auth Module: The auth module will take care of authentication and authorization. It's crucial for verifying the user's identity before allowing them to change their password.
  • User Service: This service will contain the logic for interacting with the user data. It'll handle fetching user details, updating passwords, and other user-related tasks.
  • Auth Service: The auth service will handle the authentication logic, including verifying user credentials and generating tokens.

Let's start by creating these modules and services. We'll use the NestJS CLI to make this super quick. Run these commands in your project:

nest generate module user
nest generate service user
nest generate module auth
nest generate service auth

This will create the necessary files and directories for our user and auth modules. Now, let's move on to defining our user model.

Defining the User Model

Our user model will represent the structure of our user data in the database. We'll keep it simple for this example, but you can always add more fields as needed. We'll need fields like id, username, email, and, of course, password. Let's create a user.entity.ts file in the user directory:

// src/user/entities/user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;
}

Here, we're using TypeORM, a popular ORM (Object-Relational Mapping) library for TypeScript. We've defined a User entity with properties for id, username, email, and password. The @Entity() decorator marks this class as a database entity, and the @Column() decorators define the columns in our database table. We've also marked username and email as unique, which is a good practice to prevent duplicate user accounts.

Password Hashing

Now, here's a critical point: we should never store passwords in plain text. It's a huge security risk. Instead, we'll hash the passwords before storing them in the database. Hashing is a one-way process that transforms the password into a string of characters that can't be easily reversed. We'll use bcrypt, a widely-used library for password hashing.

First, let's install bcrypt:

npm install bcrypt
npm install --save-dev @types/bcrypt

Then, we'll update our User entity to include a method for hashing the password before saving it:

// src/user/entities/user.entity.ts

import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 10);
  }
}

We've added a @BeforeInsert() decorator, which tells TypeORM to run the hashPassword method before inserting a new user into the database. Inside this method, we use bcrypt.hash() to hash the password with a salt factor of 10. This salt adds an extra layer of security to our password hashing.

Building the User Service

Next up, we'll create the user service. This service will handle the logic for creating users, fetching user details, and, most importantly, updating passwords. Let's open up src/user/user.service.ts and start coding:

// src/user/user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}

  async findOne(id: number): Promise<User | undefined> {
    return this.userRepository.findOne({ where: { id } });
  }

  async findByUsername(username: string): Promise<User | undefined> {
    return this.userRepository.findOne({ where: { username } });
  }

  async updatePassword(userId: number, newPassword: string): Promise<void> {
    const user = await this.findOne(userId);
    if (!user) {
      throw new Error('User not found');
    }

    user.password = newPassword; // We'll hash this later
    await user.hashPassword(); // Hash the new password
    await this.userRepository.save(user);
  }
}

In this service, we've injected the UserRepository, which allows us to interact with the User entity in the database. We've also defined three methods:

  • findOne(): Fetches a user by their ID.
  • findByUsername(): Fetches a user by their username.
  • updatePassword(): Updates the user's password. This is the method we'll use for our password change route.

Notice that in the updatePassword() method, we're first fetching the user by their ID. Then, we're setting the new password and calling the hashPassword() method we defined in the User entity. Finally, we're saving the updated user to the database. This ensures that the new password is also hashed before being stored.

Creating the Auth Controller and Route

Now, let's create the auth controller and define the route for changing the password. We'll add a new endpoint to our auth controller that handles this request. Open up src/auth/auth.controller.ts and let's get to work:

// src/auth/auth.controller.ts

import { Body, Controller, HttpCode, HttpStatus, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';

@Controller('auth')
export class AuthController {
  constructor(
    private authService: AuthService,
    private userService: UserService,
  ) {}

  @UseGuards(JwtAuthGuard)
  @Post('change-password')
  @HttpCode(HttpStatus.OK)
  async changePassword(
    @Request() req,
    @Body('oldPassword') oldPassword,
    @Body('newPassword') newPassword,
  ): Promise<any> {
    const user = await this.userService.findOne(req.user.userId);

    if (!user) {
      return { message: 'User not found' };
    }

    const passwordMatch = await bcrypt.compare(oldPassword, user.password);

    if (!passwordMatch) {
      return { message: 'Invalid old password' };
    }

    await this.userService.updatePassword(req.user.userId, newPassword);
    return { message: 'Password changed successfully' };
  }
}

Let's break this down:

  • We've added a new route /auth/change-password using the @Post() decorator.
  • We're using JwtAuthGuard to protect this route. This means that only authenticated users with a valid JWT token can access it.
  • We're injecting the AuthService and UserService to handle authentication and user-related logic.
  • In the changePassword method, we're first fetching the user by their ID from the request object (req.user.userId). This ID is added to the request by the JwtAuthGuard after verifying the JWT token.
  • We're then comparing the provided oldPassword with the user's current password using bcrypt.compare(). This is crucial to ensure that the user knows their current password before changing it.
  • If the passwords match, we're calling the updatePassword() method from the UserService to update the user's password.
  • Finally, we're returning a success message.

Securing the Route with JWT Authentication

As you saw in the controller, we're using JwtAuthGuard to secure our password change route. JWT (JSON Web Token) authentication is a common way to protect API endpoints. It involves issuing a token to the user after they log in, and then requiring that token to be included in subsequent requests.

If you haven't already set up JWT authentication in your NestJS project, you'll need to do that. Here's a quick rundown of the steps involved:

  1. Install the @nestjs/jwt and @nestjs/passport packages.
  2. Create a JWT strategy using Passport.js.
  3. Create a JWT auth guard that uses the JWT strategy.
  4. Implement the login endpoint that issues JWT tokens.

I won't go into all the details here, but NestJS has excellent documentation and tutorials on how to set up JWT authentication. Once you have that in place, you can use JwtAuthGuard to protect your routes, like we did in the AuthController.

Testing the Password Change Route

Alright, we've built our password change route! Now, let's make sure it works as expected. We can use tools like Postman or Insomnia to send requests to our API and test the functionality.

Here's what we'll need to do:

  1. Log in as a user to get a JWT token.

  2. Send a POST request to /auth/change-password with the following body:

    {