JWT Authentication with NestJS
Introduction
In this post, I’ll show you how to implement a REST Api with the following endpoints:
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/posts (authenticated user)
DELETE /api/v1/posts (admin only)
Current node version I’m using: v20.12.1
Current NestJS version I’m using: 10.4.9
Auth Service
Create new project
First, let’s create the auth service, which will handle login, registration, and user information like roles.
nest new auth --strict
Add database and ORM dependencies
Now, we need to configure the database. In this case, we’re using TypeORM and PostgreSQL. Let’s install the necessary dependencies:
npm install --save @nestjs/typeorm typeorm pg
// Filepath: ./src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'username',
password: 'password',
database: 'databaseName',
synchronize: true,
autoLoadEntities: true,
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
To keep our credentials secure, we’ll use dotenv to manage environment variables through a .env file:
npm install dotenv
Our .env file will look like this:
# Filepath: .env
DB_URL=localhost
DB_PORT=5432
DB_USER=username
DB_PASS=password
DB_NAME=databaseName
Update the AppModule to use environment variables:
// Filepath: ./src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import 'dotenv/config';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
synchronize: true,
autoLoadEntities: true,
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
Create Auth and Users modules
Next, we create the auth and users modules:
nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec
nest g module users
nest g service users --no-spec
Inside the users folder, we should create an ’entities’ folder and then add the user.entity.ts file:
// Filepath: .src/users/entities/user.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
username: string;
@Column({ name: 'password_hash' })
passwordHash: string;
}
In the src/users/ folder, we should also create the user repository:
// Filepath: .src/users/user.repository.ts
import { DataSource, Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository extends Repository<User> {
constructor(dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
}
Next, we need to update the UsersModule (src/users/users.module.ts), adding TypeOrmModule.forFeature([User]) in imports and UserRepository in providers. The file should look like this:
// Filepath: .src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, UserRepository],
exports: [UsersService],
})
export class UsersModule {}
Similarly, we need to update the AuthModule (src/auth/auth.module.ts), importing UsersModule:
// Filepath: .src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
Add Register logic using bcrypt
Now we are going to implement the register endpoint, we will use bcrypt to encrypt passwords:
npm install bcrypt
and
npm install --save-dev @types/bcrypt
Now, we need to add the logic to save the user to the database. But before that, we should create a DTO, I’ll name it “RegisterRequest” (register-request.ts). Since this DTO will be shared between auth and users module, I’ll create a new folder ‘shared’ and within that, a dto folder. Inside the dto folder, add a new class:
// Filepath: .src/shared/dto/register-request.ts
export class RegisterRequest {
username: string;
password: string;
email: string;
}
Then, in UsersService we inject userRepository and add a register method which takes registerRequest as parameter. Inside the method, we create an User object from the properties of the DTO (registerRequest), but the password will be encrypted using bcrypt with an iteration of 10. Once the object is created, we call the repository to save the object into the database.
// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';
@Injectable()
export class UsersService {
constructor(private readonly userRepository: UserRepository) {}
async save(registerRequest: RegisterRequest): Promise<User> {
const user = new User();
user.email = registerRequest.email;
user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
user.username = registerRequest.username;
return this.userRepository.save(user);
}
}
Next, in AuthService, we should inject UsersService to call the save function:
// Filepath: .src/users/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private readonly userService: UsersService) {}
async register(registerRequest: RegisterRequest): Promise<string> {
await this.userService.save(registerRequest);
return 'we should replace this line to return the jwt';
}
}
Then, let’s edit the AuthController. I prefer to use a route like /api/v{number}/auth instead of just /auth, so I’ll update it too:
// Filepath: .src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
@Controller('api/v1/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() registerRequest: RegisterRequest): Promise<string> {
return await this.authService.register(registerRequest);
}
}
NOTE: In a real project, we should add validations to the DTO, such as ensuring it doesn’t allow null values, requiring a minimum password length, etc. We should also add validation in the entity, such as ensuring the email is unique, etc.
Add JWT
Next, let’s add the JWT dependency:
npm install --save @nestjs/jwt
Now, we should add the JWT configuration:
# Filepath: .env
DB_URL=localhost
DB_PORT=5432
DB_USER=username
DB_PASS=password
DB_NAME=databaseName
JWT_SECRET=00000000000000000000000000000000
JWT_EXPIRES=3600
// Filepath: .src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import 'dotenv/config';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
signOptions: {
algorithm: 'HS256',
expiresIn: process.env.JWT_EXPIRES,
},
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
Then, we will modify AuthService.register to return the JWT after the user signs up:
// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UsersService,
private readonly jwtService: JwtService,
) {}
async register(registerRequest: RegisterRequest): Promise<{ token: string }> {
await this.userService.save(registerRequest);
const payload = { sub: registerRequest.username };
return {
token: await this.jwtService.signAsync(payload),
};
}
}
// Filepath: .src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
@Controller('api/v1/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() registerRequest: RegisterRequest): Promise<{ token: string }> {
return await this.authService.register(registerRequest);
}
}
Add Login logic
Now, we’re implementing the login method, So we need a LoginRequest DTO (with username and password), a findByUsername method in UserService, a login method in both AuthService and AuthController and a TokenResponse DTO:
// Filepath: .src/auth/dto/token-response.ts
export class TokenResponse {
token: string;
}
// Filepath: .src/shared/dto/login-request.ts
export class LoginRequest {
username: string;
password: string;
}
// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';
@Injectable()
export class UsersService {
constructor(private readonly userRepository: UserRepository) {}
async save(registerRequest: RegisterRequest): Promise<User> {
const user = new User();
user.email = registerRequest.email;
user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
user.username = registerRequest.username;
return this.userRepository.save(user);
}
async findByUsername(username: string): Promise<User | null> {
return this.userRepository.findOne({
where: { username: username },
});
}
}
// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginRequest } from '../shared/dto/login-request';
import * as bcrypt from 'bcrypt';
import { TokenResponse } from './dto/token-response';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UsersService,
private readonly jwtService: JwtService,
) {}
async register(registerRequest: RegisterRequest): Promise<TokenResponse> {
await this.userService.save(registerRequest);
return this.createTokenFromUsername(registerRequest.username);
}
async login(loginRequest: LoginRequest): Promise<TokenResponse> {
const user = await this.userService.findByUsername(loginRequest.username);
if (!user) {
throw new Error('Username or password invalid');
}
const passwordMatches = await bcrypt.compare(
loginRequest.password,
user.passwordHash,
);
if (!passwordMatches) {
throw new Error('Username or password invalid');
}
return this.createTokenFromUsername(loginRequest.username);
}
private async createTokenFromUsername(
username: string,
): Promise<TokenResponse> {
const payload = { sub: username };
const tokenResponse = new TokenResponse();
tokenResponse.token = await this.jwtService.signAsync(payload);
return tokenResponse;
}
}
// Filepath: .src/auth/auth.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
ForbiddenException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
import { LoginRequest } from '../shared/dto/login-request';
import { TokenResponse } from './dto/token-response';
@Controller('api/v1/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(
@Body() registerRequest: RegisterRequest,
): Promise<TokenResponse> {
return await this.authService.register(registerRequest);
}
@HttpCode(200)
@Post('login')
async login(@Body() loginRequest: LoginRequest): Promise<TokenResponse> {
try {
return await this.authService.login(loginRequest);
} catch (error) {
throw new ForbiddenException();
}
}
}
Add Roles
First, we will configure the TypeOrmModule to allow us to use snakenaming strategy:
npm i --save typeorm-naming-strategies
In app.module.ts :
// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
@Module({
imports: [
AuthModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
synchronize: true,
autoLoadEntities: true,
namingStrategy: new SnakeNamingStrategy(),
}),
UsersModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
Now, we’ll add UserRole and Role entities, inside src/users/entities folder:
// Filepath: .src/users/entities/role.entity.ts
import {
BaseEntity,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { UserRole } from './user-role.entity';
@Entity()
export class Role extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ nullable: true })
deletedAt: Date;
@OneToMany(() => UserRole, (userRole: UserRole): Role => userRole.role)
userRoles: UserRole[];
}
// Filepath: .src/users/entities/user-role.entity.ts
import {
BaseEntity,
Column,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
import { Role } from './role.entity';
@Entity('user_role')
export class UserRole extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
fromDate: Date;
@Column({ nullable: true })
toDate: Date;
@ManyToOne(
(): typeof User => User,
(user: User): UserRole[] => user.userRoles,
)
user: User;
@ManyToOne(() => Role, (role: Role): UserRole[] => role.userRoles, {
eager: true,
})
role: Role;
}
Also we should update the User entity:
// Filepath: .src/users/entities/user.entity.ts
import {
BaseEntity,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { UserRole } from './user-role.entity';
@Entity('users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
username: string;
@Column()
passwordHash: string;
@OneToMany(
(): typeof UserRole => UserRole,
(role: UserRole): User => role.user,
{
eager: true,
cascade: true,
},
)
userRoles: UserRole[];
}
Add RoleRepository in users folder, it has a method to search for a Role entity by name:
// Filepath: .src/users/role.repository.ts
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { Role } from './entities/role.entity';
@Injectable()
export class RoleRepository extends Repository<Role> {
constructor(dataSource: DataSource) {
super(Role, dataSource.createEntityManager());
}
findByName(name: string): Promise<Role | null> {
return this.findOne({ where: { name } });
}
}
Update UsersModule:
// Filepath: .src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRole } from './entities/user-role.entity';
import { Role } from './entities/role.entity';
import { RoleRepository } from './role.repository';
@Module({
imports: [TypeOrmModule.forFeature([User, UserRole, Role])],
providers: [UsersService, UserRepository, RoleRepository],
exports: [UsersService],
})
export class UsersModule {}
Now we add the logic to assign the USER role when a user registers:
// Filepath: .src/shared/constants.ts
export const USER = 'USER';
export const ADMIN = 'ADMIN';
export const ROLE_PREFIX = 'ROLE_';
// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';
import { Role } from './entities/role.entity';
import { UserRole } from './entities/user-role.entity';
import { RoleRepository } from './role.repository';
import { USER } from '../shared/constants';
@Injectable()
export class UsersService {
constructor(
private readonly userRepository: UserRepository,
private readonly roleRepository: RoleRepository,
) {}
async save(registerRequest: RegisterRequest): Promise<User> {
let role: Role | null = await this.roleRepository.findByName(USER);
if (!role) {
role = new Role();
role.name = USER;
role = await this.roleRepository.save(role);
}
const userRole = new UserRole();
userRole.fromDate = new Date();
userRole.role = role;
const user = new User();
user.email = registerRequest.email;
user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
user.username = registerRequest.username;
user.userRoles = [userRole];
return this.userRepository.save(user);
}
async findByUsername(username: string): Promise<User | null> {
return this.userRepository.findOne({
where: { username: username },
});
}
}
And finally we update AuthService.register to include the roles in the JWT payload:
// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginRequest } from '../shared/dto/login-request';
import * as bcrypt from 'bcrypt';
import { TokenResponse } from './dto/token-response';
import { UserRole } from '../users/entities/user-role.entity';
import { User } from '../users/entities/user.entity';
import { ROLE_PREFIX } from '../shared/constants';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UsersService,
private readonly jwtService: JwtService,
) {}
async register(registerRequest: RegisterRequest): Promise<TokenResponse> {
const user: User = await this.userService.save(registerRequest);
return this.createTokenFromUsername(user.username, user.userRoles);
}
async login(loginRequest: LoginRequest): Promise<TokenResponse> {
const user: User | null = await this.userService.findByUsername(
loginRequest.username,
);
if (!user) {
throw new Error('Username or password invalid');
}
const passwordMatches = await bcrypt.compare(
loginRequest.password,
user.passwordHash,
);
if (!passwordMatches) {
throw new Error('Username or password invalid');
}
return this.createTokenFromUsername(user.username, user.userRoles);
}
private async createTokenFromUsername(
username: string,
userRoles: UserRole[],
): Promise<TokenResponse> {
const payload = {
sub: username,
roles: userRoles.map(
(ur: UserRole): string => ROLE_PREFIX + ur.role.name,
),
};
const tokenResponse = new TokenResponse();
tokenResponse.token = await this.jwtService.signAsync(payload);
return tokenResponse;
}
}
Posts endpoint
Now, let’s start generating the Posts module and in it, PostsController and its DTOs:
nest g module posts
nest g controller posts --no-spec
Create a dto folder and add PostRequest (post-request.ts) and PostResponse (post-response.ts)
// Filepath: .src/posts/dto/post-request.ts
export class PostRequest {
userId: number;
content: string;
}
// Filepath: .src/posts/dto/post-response.ts
export class PostRequest {
userId: number;
content: string;
}
Update PostsController, we’re keeping it simple, so we won’t implement the service, repository, etc., just the controller and its DTOs. The GET method will be allowed for authenticated users and the POST for ADMIN role.
// Filepath: .src/posts/posts.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';
import { PostResponse } from './dto/post-response';
import { PostRequest } from './dto/post-request';
@Controller('api/v1/posts')
export class PostsController {
posts: PostResponse[] = [
{ username: 'admin', content: 'entry 1' },
{ username: 'admin', content: 'entry 2' },
];
@Get()
getPosts(): PostResponse[] {
return this.posts;
}
@Post()
createPost(@Body() post: PostRequest): PostResponse[] {
this.posts.push({ username: 'admin', content: post.content });
return this.posts;
}
}
Create AuthGuard
To do this, we need a Public decorator to allow request to be public, an AuthGuard to get the Authorization header, extract the JWT and add the user to the request.
First, create the Public decorator. Create a file at .src/auth named public.decorator.ts:
// Filepath: .src/auth/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Then, we’ll create the AuthGuard
nest g guard auth --no-spec
Update AuthGuard to extract token and add the user to the request:
// Filepath: .src/auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import 'dotenv/config';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const header = request.headers.authorization;
if (header == null || !header.startsWith('Bearer ')) {
throw new UnauthorizedException();
}
const token = header.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
request['user'] = { username: payload.sub, roles: payload.roles };
} catch {
throw new UnauthorizedException();
}
return true;
}
}
Update the AppModule to enable AuthGuard globally:
// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { PostsModule } from './posts/posts.module';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/auth.guard';
@Module({
imports: [
AuthModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
synchronize: true,
autoLoadEntities: true,
namingStrategy: new SnakeNamingStrategy(),
}),
UsersModule,
PostsModule,
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}
Next, update AuthController to make register and login public
// Filepath: .src/auth/auth.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
ForbiddenException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
import { LoginRequest } from '../shared/dto/login-request';
import { TokenResponse } from './dto/token-response';
import { Public } from './public.decorator';
@Controller('api/v1/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('register')
async register(
@Body() registerRequest: RegisterRequest,
): Promise<TokenResponse> {
return await this.authService.register(registerRequest);
}
@Public()
@HttpCode(200)
@Post('login')
async login(@Body() loginRequest: LoginRequest): Promise<TokenResponse> {
try {
return await this.authService.login(loginRequest);
} catch (error) {
throw new ForbiddenException();
}
}
}
Restrict createPost to only allow admins
Now, let’s add a Role decorator:
// Filepath: .src/auth/role.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { ROLE_PREFIX } from '../shared/constants';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) =>
SetMetadata(
ROLES_KEY,
roles.map((r) => ROLE_PREFIX + r),
);
Create the RoleGuard:
// Filepath: .src/auth/role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
Then we add the RoleGuard to AppModule to enable it globally:
// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { PostsModule } from './posts/posts.module';
import { RolesGuard } from './auth/roles.guard';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/auth.guard';
@Module({
imports: [
AuthModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
synchronize: true,
autoLoadEntities: true,
namingStrategy: new SnakeNamingStrategy(),
}),
UsersModule,
PostsModule,
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
Finally, update PostsController to allow creating a post only if the user is an admin:
// Filepath: .src/posts/posts.controller.ts
import { Body, Controller, Get, Post, Request } from '@nestjs/common';
import { PostResponse } from './dto/post-response';
import { PostRequest } from './dto/post-request';
import { Roles } from '../auth/roles.decorator';
import { ADMIN } from '../shared/constants';
@Controller('api/v1/posts')
export class PostsController {
posts: PostResponse[] = [
{ username: 'admin', content: 'entry 1' },
{ username: 'admin', content: 'entry 2' },
];
@Get()
getPosts(): PostResponse[] {
return this.posts;
}
@Roles(ADMIN)
@Post()
createPost(@Body() post: PostRequest, @Request() req: any): PostResponse[] {
this.posts.push({ username: req.user.username, content: post.content });
return this.posts;
}
}