From 66e1bb0f2855f4669f2f79c47ff44f6ae9060223 Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Tue, 10 Dec 2024 10:11:47 +0300 Subject: [PATCH] feat: protecting endpoint by roles --- .../decorators/allowed-roles.decorator.ts | 5 ++++ src/common/decorators/index.ts | 1 + src/common/guards/access-token.guard.ts | 2 +- src/common/guards/index.ts | 1 + src/common/guards/roles-guard.ts | 28 +++++++++++++++++++ .../controllers/customer.controller.ts | 9 ++++-- src/junior/controllers/junior.controller.ts | 17 +++++++---- 7 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 src/common/decorators/allowed-roles.decorator.ts create mode 100644 src/common/guards/roles-guard.ts diff --git a/src/common/decorators/allowed-roles.decorator.ts b/src/common/decorators/allowed-roles.decorator.ts new file mode 100644 index 0000000..1b77868 --- /dev/null +++ b/src/common/decorators/allowed-roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { Roles } from '~/auth/enums'; + +export const ROLE_METADATA_KEY = 'roles'; +export const AllowedRoles = (...roles: Roles[]) => SetMetadata(ROLE_METADATA_KEY, roles); diff --git a/src/common/decorators/index.ts b/src/common/decorators/index.ts index c5aae70..c7bb450 100644 --- a/src/common/decorators/index.ts +++ b/src/common/decorators/index.ts @@ -1,2 +1,3 @@ +export * from './allowed-roles.decorator'; export * from './public.decorator'; export * from './user.decorator'; diff --git a/src/common/guards/access-token.guard.ts b/src/common/guards/access-token.guard.ts index 8d03365..f8ba82b 100644 --- a/src/common/guards/access-token.guard.ts +++ b/src/common/guards/access-token.guard.ts @@ -6,7 +6,7 @@ import { IS_PUBLIC_KEY } from '../decorators'; @Injectable() export class AccessTokenGuard extends AuthGuard('access-token') { - constructor(private reflector: Reflector) { + constructor(protected reflector: Reflector) { super(); } canActivate(context: ExecutionContext): boolean | Promise | Observable { diff --git a/src/common/guards/index.ts b/src/common/guards/index.ts index 874d51c..223b7d6 100644 --- a/src/common/guards/index.ts +++ b/src/common/guards/index.ts @@ -1 +1,2 @@ export * from './access-token.guard'; +export * from './roles-guard'; diff --git a/src/common/guards/roles-guard.ts b/src/common/guards/roles-guard.ts new file mode 100644 index 0000000..3a18eaa --- /dev/null +++ b/src/common/guards/roles-guard.ts @@ -0,0 +1,28 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Roles } from '~/auth/enums'; +import { ROLE_METADATA_KEY } from '../decorators'; +import { AccessTokenGuard } from './access-token.guard'; + +@Injectable() +export class RolesGuard extends AccessTokenGuard { + async canActivate(context: ExecutionContext): Promise { + await super.canActivate(context); + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user) { + return false; + } + const allowedRoles = this.reflector.getAllAndOverride(ROLE_METADATA_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!allowedRoles) { + return true; + } + + return allowedRoles.some((role) => user.roles.includes(role)); + } +} diff --git a/src/customer/controllers/customer.controller.ts b/src/customer/controllers/customer.controller.ts index 2365633..3c48c4c 100644 --- a/src/customer/controllers/customer.controller.ts +++ b/src/customer/controllers/customer.controller.ts @@ -1,8 +1,9 @@ import { Body, Controller, Patch, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Roles } from '~/auth/enums'; import { IJwtPayload } from '~/auth/interfaces'; -import { AuthenticatedUser } from '~/common/decorators'; -import { AccessTokenGuard } from '~/common/guards'; +import { AllowedRoles, AuthenticatedUser } from '~/common/decorators'; +import { AccessTokenGuard, RolesGuard } from '~/common/guards'; import { ResponseFactory } from '~/core/utils'; import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request'; import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response'; @@ -11,11 +12,12 @@ import { CustomerService } from '../services'; @Controller('customers') @ApiTags('Customers') @ApiBearerAuth() -@UseGuards(AccessTokenGuard) export class CustomerController { constructor(private readonly customerService: CustomerService) {} @Patch('') + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) { const customer = await this.customerService.updateCustomer(sub, body); @@ -23,6 +25,7 @@ export class CustomerController { } @Patch('settings/notifications') + @UseGuards(AccessTokenGuard) async updateNotificationSettings( @AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateNotificationsSettingsRequestDto, diff --git a/src/junior/controllers/junior.controller.ts b/src/junior/controllers/junior.controller.ts index e63b18e..4b99a56 100644 --- a/src/junior/controllers/junior.controller.ts +++ b/src/junior/controllers/junior.controller.ts @@ -1,8 +1,9 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Roles } from '~/auth/enums'; import { IJwtPayload } from '~/auth/interfaces'; -import { AuthenticatedUser } from '~/common/decorators'; -import { AccessTokenGuard } from '~/common/guards'; +import { AllowedRoles, AuthenticatedUser } from '~/common/decorators'; +import { RolesGuard } from '~/common/guards'; import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators'; import { PageOptionsRequestDto } from '~/core/dtos'; import { CustomParseUUIDPipe } from '~/core/pipes'; @@ -18,7 +19,8 @@ export class JuniorController { constructor(private readonly juniorService: JuniorService) {} @Post() - @UseGuards(AccessTokenGuard) + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) @ApiDataResponse(JuniorResponseDto) async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) { const junior = await this.juniorService.createJuniors(body, user.sub); @@ -27,7 +29,8 @@ export class JuniorController { } @Get() - @UseGuards(AccessTokenGuard) + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) @ApiDataPageResponse(JuniorResponseDto) async findJuniors(@AuthenticatedUser() user: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) { const [juniors, count] = await this.juniorService.findJuniorsByGuardianId(user.sub, pageOptions); @@ -43,7 +46,8 @@ export class JuniorController { } @Get(':juniorId') - @UseGuards(AccessTokenGuard) + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) @ApiDataResponse(JuniorResponseDto) async findJuniorById( @AuthenticatedUser() user: IJwtPayload, @@ -55,7 +59,8 @@ export class JuniorController { } @Post('set-theme') - @UseGuards(AccessTokenGuard) + @UseGuards(RolesGuard) + @AllowedRoles(Roles.JUNIOR) @ApiDataResponse(JuniorResponseDto) async setTheme(@Body() body: SetThemeRequestDto, @AuthenticatedUser() user: IJwtPayload) { const theme = await this.juniorService.setTheme(body, user.sub);