Scope of discussion:
- Understanding about
Guard
in Nest JS - Understanding about Nest JS decorator
- How to handle private or public routes
In the first part, we've created some users endpoints:
POST /users/register
,
POST /users/login
,
GET /users/me
,
PATCH /users/:id
, and
DELETE /users/:id
but we haven't handled this endpoint GET /users/me
yet.
Don't worry, we're going to fix it.
We'll only use access_token
obtained from the login response to get the user's data.
First, we must understand a guard
in Nest JS. What does guard
mean? According to the Nest JS documentation, guard
is a class annotated with the @Injectable()
decorator, which implements the CanActivate
interface
And what is the purpose of Guard
? Basically Guard
is like a layer that is responsible for validating or updating any incoming request before being forwarded to their main process.
So for me
endpoint, we're gonna validate the access_token
and modify the information. Let's dig into this.
Let's create a new file called auth.guard.ts
inside src/common/guards/
folder:
// src/common/guards/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
In the code above, we created a class that implements CanActivate
interface. Since we implement CanActivate
interface, we need to define canActivate
function that will be responsible for validating and updating the incoming request. There is also a function called extractTokenFromHeader
for validating the access_token
included through the header as a Bearer token
.
Moving down, we'll see the jwt
verification
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
To make this work, we need to add a new variable to our .env
file JWT_SECRET="super_secret_key"
We might need to update our
app.module.ts
as well by updating thejwt
secret to use.env
filesecret: 'super_secret_key',
If all validation is passed, it will modify the incoming request information. In this case, request will receive user
data from access_token request['user'] = payload;
After we created auth.guard
. The next step is to register auth.guard
to our root module (a.k.a app.module.ts
). Open up app.module.ts
and add the auth.guard
to providers array:
providers: [
AppService,
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
Great! Now let's continue to users.controller
. Open src/modules/users/users.controller.ts
file and move to me()
function:
// This is before the changes
@Get('me')
me(): string {
return 'Get my Profile!';
}
Let's modify that function by adding @Request
decorator imported from @nestjs/common
:
@Get('me')
me(@Request() req) {
return req.user;
}
Now we're ready to test GET /users/me
endpoint:
You might not be able to access other endpoints (including login) since we registered
auth.guard
globally. We'll fix this in the section below.
Is that all about GET /users/me
endpoint? Nope, we can still make an adjustment. If you take a look at the function, req
doesn't have an explicit type @Request() req
. Since we're using typescript to write our code, will be better to add explicit type.
Let's do this!
We need to create an interface:
src/modules/users/interfaces/express-request-with-user.interface.ts
import { Request as ExpressRequest } from 'express';
import { UserPayload } from './users-login.interface';
export interface ExpressRequestWithUser extends ExpressRequest {
user: UserPayload & { iat: number; exp: number };
}
Then we can use new interfaces to our me
function
@Get('me')
me(@Request() req: ExpressRequestWithUser): UserPayload {
return req.user;
}
Finally, we're done with GET /users/me
endpoint!
Let's move on to the last part of this section. We already created auth.guard
and set it as a global Guard
. But now we have a problem. If we access the login or register endpoint, we'll get Unauthorized
response
{
"message": "Unauthorized",
"statusCode": 401
}
As we will have some public endpoints (No need to pass access_token
), this will be troublesome.
How do we fix that?
Now we must provide a mechanism for declaring routes as public. For this, we can create a custom decorator using the SetMetadata decorator factory function.
Create a new file src/common/decorators/public.decorator.ts
:
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Update our users.controller
by adding @Public()
decorator in register and login endpoints:
...
@Public() // <--- Set register as public route
@Post('register')
async registerUser(@Body() createUserDto: CreateUserDto): Promise<User> {
// call users service method to register new user
return this.usersService.registerUser(createUserDto);
}
@Public() // <--- Set login as public route
@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): Promise<LoginResponse> {
// call users service method to login user
return this.usersService.loginUser(loginUserDto);
}
...
Lastly, open again auth.guard.ts
and add some modify:
// src/common/guards/auth.guard.ts
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
// 💡 See this line
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 💡 See this line
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
// 💡 See this condition
if (isPublic) {
return true;
}
...
}
}
Now we're able to access login and register endpoints :)
The final step of this part.
After everything we've done here, let's finalize our users
endpoints. So we want to prevent a user from updating or deleting another user's data, for example: in the access_token
, it contains user id
equal to 2. We don't want user id
2 able to change the name of user id
1.
To achieve that, let's create a new Guard
called is-mine.guard
// src/modules/users/users.controller.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class IsMineGuard implements CanActivate {
constructor() {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// 💡 We can access the user payload from the request object
// because we assigned it in the AuthGuard
return parseInt(request.params.id) === request.user.sub;
}
}
We won't apply is-mine.guard
globally like what we did in auth.guard
, but we'll apply to routes manually.
Open up users.controller
and add @UseGuards(IsMineGuard)
decorator to our updateUser
anddeleteUser
@Patch(':id')
@UseGuards(IsMineGuard) // <--- 💡 Prevent user from updating other user's data
async updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUsertDto,
): Promise<User> {
// call users service method to update user
return this.usersService.updateUser(+id, updateUserDto);
}
@Delete(':id')
@UseGuards(IsMineGuard) // <--- 💡 Prevent user from deleting other user's data
async deleteUser(@Param('id', ParseIntPipe) id: number): Promise<string> {
// call users service method to delete user
return this.usersService.deleteUser(+id);
}
The full code of part 2 can be accessed here: https://github.com/alfism1/nestjs-api/tree/part-two
Moving on to part 3:
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-3-3j34
Top comments (0)