Auth Service with JWT token and mail module, Part 1
depak379mandal
Posted on May 29, 2024
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.
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 });
}
}
}
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,
},
});
}
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 });
}
}
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,
},
});
}
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,
},
});
}
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 });
}
}
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);
}
}
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
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 };
}
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
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',
},
});
}
}
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 {}
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') },
}),
}),
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.
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
November 29, 2024