Auth Service with JWT token and mail module, Part 1

depak379mandal

depak379mandal

Posted on May 29, 2024

Auth Service with JWT token and mail module, Part 1

Love to work with you, You can hire me on Upwork.

Now we have to go with big changes and setup, We need more of base for basic libraries like mail, JWT and bcrypt. So let me describe more on the whole scenario with flowchart and some textual context. We are going to add more complex ideas in future like refresh and access token with more complex media upload.

DB Diagram

Above is a very clear sequential diagram to understand the entire architecture that we are going to build to solve the Authentication flow. We are also going to include login API that is not included right now in the Diagram. So let me start with something we call Service, So I am just attaching an email service that will help you to understand how we are going to solve the service. And its work distribution over multiple modules like mail, auth, and user module. Each of the modules have its own functionality that they will be fulfilling, and we are going to arrange all the required functions according to them. About DTOs and Controllers, we have discussed them in previous article.

I am including below the EmailService but there is also AuthService that actually holds the core parts of Authentication service like creating JWT token and sending required email. We will discuss on AuthService at the end of the EmailService.

// src/modules/auth/services/email.service.ts

import {
  Injectable,
  NotFoundException,
  UnprocessableEntityException,
} from '@nestjs/common';
import {
  EmailVerifyDto,
  LoginDto,
  RegisterDto,
  ResetPasswordDto,
  SendVerifyMailDto,
} from '../email.dto';
import { UserService } from '../../user/services/user.service';
import { TokenService } from '../../user/services/token.service';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/entities/user.entity';
import { Repository } from 'typeorm';
import { AuthService } from './auth.service';

@Injectable()
export class EmailService {
  constructor(
    private authService: AuthService,
    private userService: UserService,
    private tokenService: TokenService,
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}

  async register(registerDto: RegisterDto) {
    const user = await this.userService.create(registerDto);
    const token = await this.tokenService.create(user, 'REGISTER_VERIFY');
    await this.authService.userRegisterEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }

  async verify(verifyDto: EmailVerifyDto) {
    try {
      const user = await this.tokenService.verify(
        verifyDto.verify_token,
        'REGISTER_VERIFY',
      );
      user.email_verified_at = new Date();
      user.is_active = true;
      await user.save();
    } catch (e) {
      throw new UnprocessableEntityException({ verify_token: e.message });
    }
  }

  async login(loginDto: LoginDto) {
    const user = await this.userRepository.findOne({
      where: { email: loginDto.email.toLowerCase() },
    });

    if (!user) {
      throw new UnprocessableEntityException({ email: 'User not found' });
    }
    if (!user.is_active) {
      throw new UnprocessableEntityException({ email: 'User not active' });
    }
    if (!user.email_verified_at) {
      throw new UnprocessableEntityException({ email: 'User not verified' });
    }
    if (!user.comparePassword(loginDto.password)) {
      throw new UnprocessableEntityException({
        password: 'Password is incorrect',
      });
    }
    const auth_token = this.authService.createJwtToken(user);
    return { auth_token };
  }

  async sendVerifyMail(sendVerifyMailDto: SendVerifyMailDto) {
    const user = await this.userRepository.findOne({
      where: { email: sendVerifyMailDto.email.toLowerCase() },
    });

    if (!user) {
      throw new NotFoundException({ email: 'User not found' });
    }
    if (user.email_verified_at) {
      throw new UnprocessableEntityException({
        email: 'User already verified',
      });
    }
    const token = await this.tokenService.create(user, 'REGISTER_VERIFY');
    await this.authService.userRegisterEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }

  async sendForgotMail(sendForgotMailDto: SendVerifyMailDto) {
    const user = await this.userRepository.findOne({
      where: { email: sendForgotMailDto.email.toLowerCase() },
    });

    if (!user) {
      throw new UnprocessableEntityException({ email: 'User not found' });
    }

    if (!user.email_verified_at) {
      throw new UnprocessableEntityException({
        email: 'Please verify email first.',
      });
    }

    const token = await this.tokenService.create(user, 'RESET_PASSWORD');
    await this.authService.forgotPasswordEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }

  async resetPassword(resetPasswordDto: ResetPasswordDto) {
    try {
      const user = await this.tokenService.verify(
        resetPasswordDto.reset_token,
        'RESET_PASSWORD',
      );
      user.password = resetPasswordDto.password;
      await user.save();
    } catch (e) {
      throw new UnprocessableEntityException({ reset_token: e.message });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That is too much to even understand, So I will be breaking functions one by one for you guys, and we will understand how strings are attached and works. It is going to be a long ride, that is good as no system does not have small components. We need to just understand structure/architecture/organization of all multiple small components that make whole big system.

Register User

// src/modules/auth/services/email.service.ts

async register(registerDto: RegisterDto) {
    const user = await this.userService.create(registerDto);
    const token = await this.tokenService.create(user, 'REGISTER_VERIFY');
    await this.authService.userRegisterEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }
Enter fullscreen mode Exit fullscreen mode

In above whatever we do of anything related to user we do in userService like create user, updating user Or anything related to user. And we know userService belongs to UserModule So we can use that from UserModule as we don’t want over-distribution of something that particularly belongs to some module. NestJS provide us a way to exports the providers and by importing the module other module any module can use injected dependency without mentioned in providers list as it is directly exported from imported module. So in above module UserServiceand TokenService is coming from UserModule. register function does create user and token, then sends email with token for required verify process in Authentication.

Verify User

// src/modules/auth/services/email.service.ts

async verify(verifyDto: EmailVerifyDto) {
    try {
      const user = await this.tokenService.verify(
        verifyDto.verify_token,
        'REGISTER_VERIFY',
      );
      user.email_verified_at = new Date();
      user.is_active = true;
      await user.save();
    } catch (e) {
      throw new UnprocessableEntityException({ verify_token: e.message });
    }
}
Enter fullscreen mode Exit fullscreen mode

Above is very basic idea what happens on verification it tries to verify the token by helper function this.tokenService.verify that we will look into later and if the token is valid it updates the email_verified_at field with current date and also sets is_active to true. So any user can log in to their account and use APIs in further.

Resend Verify Mail (If user is not verified)

// src/modules/auth/services/email.service.ts

async sendVerifyMail(sendVerifyMailDto: SendVerifyMailDto) {
    const user = await this.userRepository.findOne({
      where: { email: sendVerifyMailDto.email.toLowerCase() },
    });

    if (!user) {
      throw new NotFoundException({ email: 'User not found' });
    }
    if (user.email_verified_at) {
      throw new UnprocessableEntityException({
        email: 'User already verified',
      });
    }
    const token = await this.tokenService.create(user, 'REGISTER_VERIFY');
    await this.authService.userRegisterEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }
Enter fullscreen mode Exit fullscreen mode

If user is not verified and unable to verify in particular, expire time. They can resend verify email again from this endpoint. We have injected userRepository to find user if they exist in our DB, or they are already verified, If they are verified we will not re-send verify email. If they are not verified, we will send another email as per request from user.

Send Forgot Email

// src/modules/auth/services/email.service.ts

async sendForgotMail(sendForgotMailDto: SendVerifyMailDto) {
    const user = await this.userRepository.findOne({
      where: { email: sendForgotMailDto.email.toLowerCase() },
    });

    if (!user) {
      throw new UnprocessableEntityException({ email: 'User not found' });
    }

    if (!user.email_verified_at) {
      throw new UnprocessableEntityException({
        email: 'Please verify email first.',
      });
    }

    const token = await this.tokenService.create(user, 'RESET_PASSWORD');
    await this.authService.forgotPasswordEmail({
      to: user.email,
      data: {
        hash: token.token,
      },
    });
  }
Enter fullscreen mode Exit fullscreen mode

This function in service is to provide functionality to send forgot password email, So whenever user requests for forgot password, we can just use this particular function. As same above, we fetch the user details if they exist, we proceed ahead. If they are not verified, we don’t proceed, as they need to first verify the user. Verification is not necessary, it is really up to you if you wanted to include it or not. In ahead we create a token, So user are tracked, then we send password to user.

Reset Password

// src/modules/auth/services/email.service.ts

async resetPassword(resetPasswordDto: ResetPasswordDto) {
    try {
      const user = await this.tokenService.verify(
        resetPasswordDto.reset_token,
        'RESET_PASSWORD',
      );
      user.password = resetPasswordDto.password;
      await user.save();
    } catch (e) {
      throw new UnprocessableEntityException({ reset_token: e.message });
    }
  }
Enter fullscreen mode Exit fullscreen mode

Above is steps to verify user, we verify the use tokenService that verifies and returns the connected user. After that, we proceed with saving the password in DB, but you will notice we are just adding it to DB directly without encrypting. That part actually handled by a hook we implement in User entity.

// src/entities/user.entity.ts

import {
  AfterLoad,
  BeforeInsert,
  BeforeUpdate,
  Column,
  Entity,
  OneToMany,
} from 'typeorm';
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base';
import { Token } from './user_token.entity';
import * as bcrypt from 'bcryptjs';

@Entity({ name: 'users' })
export class User extends BaseEntity {
  ...

  @ApiHideProperty()
  previousPassword: string;

  @AfterLoad()
  storePasswordInCache() {
    this.previousPassword = this.password;
  }

  @BeforeInsert()
  @BeforeUpdate()
  async setPassword() {
    if (this.previousPassword !== this.password && this.password) {
      const salt = await bcrypt.genSalt();
      this.password = await bcrypt.hash(this.password, salt);
    }
    this.email = this.email.toLowerCase();
  }

  comparePassword(password: string) {
    return bcrypt.compareSync(password, this.password);
  }
}
Enter fullscreen mode Exit fullscreen mode

Above, we are using listeners and subscribers (hooks). We always create a cache saved as previousPassword, So whenever we update a new password, we can use the previous password from that property to compare and encrypt using bcryptjs. The naming of hooks is very clear, @AfterLoad works after fetching the data from DB, @BeforeInsert and @BeforeUpdate are respectively called after insertion and update of data in DB. We also require a library bcryptjs to be installed.

npm i bcryptjs
npm i -D @types/bcryptjs
Enter fullscreen mode Exit fullscreen mode

Login User

// src/modules/auth/services/email.service.ts

async login(loginDto: LoginDto) {
    const user = await this.userRepository.findOne({
      where: { email: loginDto.email.toLowerCase() },
    });

    if (!user) {
      throw new UnprocessableEntityException({ email: 'User not found' });
    }
    if (!user.is_active) {
      throw new UnprocessableEntityException({ email: 'User not active' });
    }
    if (!user.email_verified_at) {
      throw new UnprocessableEntityException({ email: 'User not verified' });
    }
    if (!user.comparePassword(loginDto.password)) {
      throw new UnprocessableEntityException({
        password: 'Password is incorrect',
      });
    }
    const auth_token = this.authService.createJwtToken(user);
    return { auth_token };
}
Enter fullscreen mode Exit fullscreen mode

At last, we are a place where we can discuss, How user actually logged in. In log in DTO we get email and password, taking both of them we check if user actually exist or not then comparePassword If correct then we create token from authService.

Now is the time to reveal the core of services that we have as AuthService and MailerService.
Auth Service

We need libraries like jwt and nodemailer that will help us in tokenization and mailing respectively. To install them use below command

npm i @nestjs/jwt @nestjs-modules/mailer
Enter fullscreen mode Exit fullscreen mode

Now we can define Auth Service that holds our JWT token creation and sending mail using nodemailer. That is very simple functions we have implemented, So you can take a look at it. It needs multipart to be in places like MailModule as a global module. Also needs MailData an interface that helps to define common data to be passed in functions of email.

// src/modules/auth/services/auth.service.ts

import { Injectable } from '@nestjs/common';
import { User } from 'src/entities/user.entity';
import { JwtService } from '@nestjs/jwt';
import { MailerService } from '@nestjs-modules/mailer';
import { ConfigService } from '@nestjs/config';
import { MailData } from 'src/modules/mail/mail.interface';

@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    private mailerService: MailerService,
    private configService: ConfigService,
  ) {}

  createJwtToken(user: User) {
    return this.jwtService.sign({
      id: user.id,
      timestamp: Date.now(),
    });
  }

  async userRegisterEmail(
    mailData: MailData<{
      hash: string;
    }>,
  ) {
    await this.mailerService.sendMail({
      to: mailData.to,
      subject: 'Thank You For Registration, Verify Your Account.',
      text: `${this.configService.get(
        'app.frontendDomain',
      )}/auth/verify?token=${mailData.data.hash}`,
      template: 'auth/registration',
      context: {
        url: `${this.configService.get(
          'app.frontendDomain',
        )}/auth/verify?token=${mailData.data.hash}`,
        app_name: this.configService.get('app.name'),
        title: 'Thank You For Registration, Verify Your Account.',
        actionTitle: 'Verify Your Account',
      },
    });
  }

  async forgotPasswordEmail(
    mailData: MailData<{
      hash: string;
    }>,
  ) {
    await this.mailerService.sendMail({
      to: mailData.to,
      subject: 'Here is your Link for Reset Password.',
      text: `${this.configService.get(
        'app.frontendDomain',
      )}/auth/reset-password?token=${mailData.data.hash}`,
      template: 'auth/registration',
      context: {
        url: `${this.configService.get(
          'app.frontendDomain',
        )}/auth/reset-password?token=${mailData.data.hash}`,
        app_name: this.configService.get('app.name'),
        title: 'Here is your Link for Reset Password.',
        actionTitle: 'Reset Password',
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

For JWT, we need only need to register it as module in AuthModule. To use any of the Entity in services, we always need it to be injected from module. Otherwise, it will raise concern, that we don’t have any service or injectable available in opposite of mentioned one. That is why to use userRepository we are just importing the TypeOrmModule.forFeature([User]) including the required entity to be injected as repository.

// src/modules/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { EmailController } from './email.controller';
import { EmailService } from './services/email.service';
import { AuthService } from './services/auth.service';
import { UserModule } from '../user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/entities/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UserModule,
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('auth.secret'),
        signOptions: { expiresIn: configService.get('auth.expires') },
      }),
    }),
  ],
  controllers: [EmailController],
  providers: [EmailService, AuthService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

To define basic options for JWT, we provide it directly in imported modules as follows, Also registerAsync to use any other module as that imported module depends on. It dynamically gets instantiated after dependent module creation. So we have provided basic options like secret and expiration time.

// src/modules/auth/auth.module.ts

JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => ({
    secret: configService.get('auth.secret'),
    signOptions: { expiresIn: configService.get('auth.expires') },
  }),
}),
Enter fullscreen mode Exit fullscreen mode

We need to discuss more on Mail Module and User module that are used in above code snippets. But this article is getting bigger that expected. So let us move the parts in next article. Thank you very much for reading, check my profile in further to get into other articles of this series.
See you in the next.

💖 💪 🙅 🚩
depak379mandal
depak379mandal

Posted on May 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related