Securing Email Synchronization in NestJS: Implementing Users module in NestJS
This is the fourteenth story of series Building a Robust Backend: A Comprehensive Guide Using NestJS, TypeORM, and Microservices. Our purpose is to build an email sync system for Gmail, since we have built the main functionality, now let’s proceed with implementing authentication and authorization. Today we will be creating users table.
We will begin by creating migration file with npm run migration:create — name=add-users-table
command, then add below code for migration up and down. This will create users table with name, username, email, password, role and active status
.
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'uuid_generate_v4()',
isGenerated: true,
generationStrategy: 'uuid',
},
{
name: 'full_name',
type: 'varchar',
length: '255',
},
{
name: 'email',
type: 'varchar',
length: '255',
isUnique: true,
},
{
name: 'phone',
type: 'varchar',
length: '255',
isUnique: true,
},
{
name: 'username',
type: 'varchar',
length: '255',
isUnique: true,
},
{
name: 'password',
type: 'varchar',
length: '60',
},
{
name: 'role',
type: 'enum',
enum: Object.values(Role),
default: `'${Role.User}'`,
},
{
name: 'active',
type: 'boolean',
default: true,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'deleted_at',
type: 'timestamp',
isNullable: true,
},
],
}),
true,
);
// Create the trigger function
await queryRunner.query(`
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW."updatedAt" = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
`);
// Attach the trigger to the 'users' table
await queryRunner.query(`
CREATE TRIGGER update_updated_at_trigger
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the trigger from the 'users' table
await queryRunner.query(`
DROP TRIGGER update_updated_at_trigger ON users;
`);
// Drop the trigger function
await queryRunner.query(`
DROP FUNCTION update_updated_at_column;
`);
await queryRunner.dropTable('users');
}
The users, and other DTOs and entity are present in repo code since DTOs and entities have already been discussed and their explanation will be auxiliary. Weneed to add additional packages of bcrypt and @nestjs/jwt with npm i @nestjs/jwt
and npm i bcrypt
to encode passwords. With this we will create a bcrypt util to use later, the code of which is below:
// src\shared\utils\bcrypt.ts
import * as bcrypt from 'bcrypt';
export function encodePassword(rawPassword: string) {
const SALT = bcrypt.genSaltSync();
return bcrypt.hashSync(rawPassword, SALT);
}
export function comparePasswords(rawPassword: string, hash: string) {
return bcrypt.compareSync(rawPassword, hash);
}
Now we will create UserService
, we will make a range of user management functionalities. This will include creating users with unique email and username validations and secure password handling, listing all users, retrieving individual user details, and updating user information. Additionally, we will implement soft deletion for users, enabling us to maintain records while removing them from active use. User status management will also be a key feature, allowing us to activate, deactivate, or change user roles as needed.
import {
ConflictException,
HttpStatus,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { encodePassword } from '../shared/utils/bcrypt';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { SetRoleDTO } from './dto/set-role.dto';
import { SetStateDTO } from './dto/set-state.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Users } from './entities/user.entity';
import { SerializedUser } from './types';
import customMessage from '../shared/responses/customMessage.response';
@Injectable()
export class UserService {
constructor(
@InjectRepository(Users) private readonly userRepository: Repository<Users>,
) {}
// Check if password and confirm passwords match
async create(createUserDto: CreateUserDto): Promise<object> {
if (createUserDto.password !== createUserDto.confirm_password) {
throw new UnprocessableEntityException(
customMessage(HttpStatus.UNPROCESSABLE_ENTITY, "passwords don't match"),
);
}
// Check if user email already exists
const email = createUserDto.email;
if (await this.userRepository.findOneBy({ email })) {
throw new ConflictException(
customMessage(HttpStatus.CONFLICT, 'email already registered'),
);
}
// Check if email is one of the deleted accounts
if (
await this.userRepository.findOne({
withDeleted: true,
where: { email: email },
})
) {
throw new ConflictException(
customMessage(HttpStatus.CONFLICT, 'user blocked or deleted account'),
);
}
// Check if username already exists
const username = createUserDto.username;
if (await this.userRepository.findOneBy({ username })) {
throw new ConflictException(
customMessage(HttpStatus.CONFLICT, 'username already used'),
);
}
const password = encodePassword(createUserDto.password);
const active = true;
const newUser = this.userRepository.create({
...createUserDto,
password,
active,
});
// Create new user
try {
const user = await this.userRepository.save(newUser);
const payload = { id: user.id, email: user.email };
return customMessage(
HttpStatus.OK,
'user created successfully, kindly login',
payload
);
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
// List all users
async findAll(): Promise<object> {
try {
const users = await this.userRepository.find();
return customMessage(
HttpStatus.OK,
'all users list',
users.map((user) => new SerializedUser(user)),
);
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
// Find a user
async findOne(id: string): Promise<object> {
const user: Users = await this.getUserbyId(id);
try {
return customMessage(
HttpStatus.OK,
'user by id',
new SerializedUser(user),
);
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
// Update user
async update(id: string, updateUserDto: UpdateUserDto): Promise<object> {
await this.getUserbyId(id);
try {
await this.userRepository.update(id, updateUserDto);
return customMessage(
HttpStatus.OK,
'user information updated successfully',
);
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
// Soft deletes a user
async remove(id: string): Promise<object> {
try {
await this.getUserbyId(id);
await this.userRepository.softDelete(id);
return customMessage(HttpStatus.OK, 'user deleted successfully');
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
async active(id: string, setStatDTO: SetStateDTO) {
const user = await this.getUserbyId(id);
if (setStatDTO.active === user.active) {
throw new ConflictException(
customMessage(HttpStatus.CONFLICT, 'defined state already set'),
);
}
try {
await this.userRepository.update(id, setStatDTO);
return customMessage(HttpStatus.OK, 'user state set successfully');
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
async admin(id: string, setRoleDTO: SetRoleDTO) {
const user = await this.getUserbyId(id);
if (setRoleDTO.role === user.role) {
throw new ConflictException(
customMessage(
HttpStatus.CONFLICT,
'role is already set to ' + user.role,
),
);
}
try {
await this.userRepository.update(id, setRoleDTO);
return customMessage(HttpStatus.OK, 'user role set successfully');
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
async setRole(setRoleDTO: SetRoleDTO, id: string) {
const user = await this.getUserbyId(id);
if (setRoleDTO.role === user.role) {
throw new ConflictException(
customMessage(
HttpStatus.CONFLICT,
'role is already set to ' + user.role,
),
);
}
try {
await this.userRepository.update(id, setRoleDTO);
return customMessage(HttpStatus.OK, 'user role set successfully');
} catch (err) {
Logger.error('Error encountered: ', err);
throw new InternalServerErrorException(
customMessage(
HttpStatus.INTERNAL_SERVER_ERROR,
'Something went wrong, please try again later',
),
);
}
}
async getUserbyId(id: string) {
const user = await this.userRepository.findOneBy({ id });
if (!user) {
throw new NotFoundException(
customMessage(HttpStatus.NOT_FOUND, "user doesn't exist"),
);
}
return user;
}
}
Lastly we will create controller methods to create, get, get by ID and do other users operations. The controller code is below:
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseInterceptors,
ClassSerializerInterceptor,
UseGuards,
UsePipes,
ValidationPipe,
UseFilters,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { JwtAuthGuard } from '../shared/guards/jwt-auth.guard';
import { Roles } from '../shared/decorators/roles.decorator';
import { Role } from '../shared/enums/roles.enum';
import { RolesGuard } from '../shared/guards/roles.guard';
import { HttpExceptionFilter } from '../shared/filters/http-exception.filter';
import customMessage from '../shared/responses/customMessage.response';
import { SetRoleDTO } from './dto/set-role.dto';
import { SetStateDTO } from './dto/set-state.dto';
@UseInterceptors(ClassSerializerInterceptor)
@UsePipes(ValidationPipe)
@UseFilters(HttpExceptionFilter)
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Get()
findAll() {
return this.userService.findAll();
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(id, updateUserDto);
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(id);
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Patch('active/:id')
active(@Param('id') id: string, @Body() setStatDTO: SetStateDTO) {
return this.userService.active(id, setStatDTO);
}
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Patch('admin/:id')
admin(@Param('id') id: string, @Body() setRoleDTO: SetRoleDTO) {
return this.userService.admin(id, setRoleDTO);
}
}
With everything ready, we can now head over to the postman and hit on the URL, and our user will be create, postman snippet below shows the user created.
With this our users module is almost complete. next, we will setup users module. This story code is available on GitHub in feature/add-users branch. If you appreciate this work, please show your support by clapping for the story and starring the repository.