DEV Community

Krishna Kurtakoti
Krishna Kurtakoti

Posted on

1

Custom role based access in Nest.js, MongoDB

Here, we have four roles: Sme, Sponsor, Admin, Operations.Initially, we had only 3 roles.Operations role was added later and Operations user has permissions similar to the Admin user.In the code, we had to replace every instance of if (user.type == USER_TYPES.ADMIN) with if (user.type == USER_TYPES.ADMIN || user.type == USER_TYPES.OPERATIONS).As this is time consuming and we can also miss many instances, we have created a roles module. In the roles module,the roles are defined along with their respective permissions as seen in Code (Part-III). Based on the permissions for each role, we will evaluate the authorization for the user in each of our controller methods.If the user has access, only then he will be granted the resources.
Code (Part-I):
src/common/constants/enum.ts

// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 export enum USER_TYPES { SME = "Sme", SPONSOR = "Sponsor", ADMIN = "Admin", OPERATIONS_TEAM = "Operations" } //rolesAccessAction export enum ROLES_ACCESS_ACTION { USERS_CONTROLLER_FINDLIST_OPERATIONS = "users.controller.findList_operations", USERS_CONTROLLER_FINDLIST_ADMIN = "users.controller.findList_admin", USERS_CONTROLLER_FIND_ONE = "users.controller.findOne", USERS_CONTROLLER_KYC_FILTER = "users.controller.findListFilterKYCStatus", USERS_CONTROLLER_USER_STATUS_FILTER = "users.controller.findListFilterUserStatus", USERS_CONTROLLER_USER_UPDATE = "users.controller.update", USERS_CONTROLLER_DELETE = "users.controller.delete", USERS_SERVICE_CHECK_FOR_UPDATE_STATUS_ERROR = "users.service.checkForUpdateStatusError", USERS_SERVICE_CREATE = "users.service.create", USERS_SERVICE_UPDATE_USER_CRMID_AND_ENTITYDETAILCODE = "users.service.updateUserCrmIdAndEntityDetailCode", REMARKS_CONTROLLER_CREATE = "remarks.controller.create", REMARKS_CONTROLLER_FINDLIST = "remarks.controller.findList", REMARKS_CONTROLLER_FINDLIST_SME = "remarks.controller.findList_sme", REMARKS_CONTROLLER_FINDLIST_SPONSOR = "remarks.controller.findList_sponsor", SME_PROJECT_CONTROLLER_FINDLIST = "sme-project.controller.findList", SME_PROJECT_CONTROLLER_FINDONE = "sme-project.controller.findOne", SME_PROJECT_CONTROLLER_RECOMMENDED_PROJECTS = "sme-project.controller.getRecommendedProjects", SME_PROJECT_CONTROLLER_CREATE = "sme-project.controller.create", SME_PROJECT_CONTROLLER_SPONSOR_FILTER = "sme-project.controller.projectSponsorFilter", SME_PROJECT_CONTROLLER_UPDATE = "sme-project.controller.update", BID_DETAILS_CONTROLLER_FINDLIST = "bid-details.controller.findList", BID_DETAILS_CONTROLLER_FINDLIST_SME = "bid-details.controller.findList_sme", BID_DETAILS_CONTROLLER_FINDLIST_SPONSOR = "bid-details.controller.findList_sponsor", BID_DETAILS_CONTROLLER_CREATE = "bid-details.controller.create", BID_DETAILS_CONTROLLER_COMPLETE_BID_PROCESS = "bid-details.controller.completeBidProcess", BID_DETAILS_CONTROLLER_REJECT_ALL_BIDS_DELETE_PROJECT = "bid-details.controller.rejectAllBidsDeleteProject", BID_DETAILS_CONTROLLER_UPDATE = "bid-details.controller.update", BID_DETAILS_CONTROLLER_UPDATE_SPONSOR = "bid-details.controller.update_sponsor", BID_DETAILS_SERVICE_CALCULATE_BID_DETAILS = "bid-details.controller.calculatebiddetails", BID_DETAILS_CONTROLLER_CREATE_TRANSACTION = "bid-details.controller.createTransaction" //BID_DETAILS_CONTROLLER_GET_FUNDED_PROJECTS = "bid-details.controller.getfundedProjects" }

Above, we have defined the rolesAction for each of the methods in our project.The convention used here is the controller/service name of the file followed by method name. For example, USERS_CONTROLLER_FINDLIST_OPERATIONS = "users.controller.findList_operations", we have users.controller as the controller name followed by method name as findList.
Code (Part-II):
src/users/users.controller.ts
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 import { Body, Controller, Param, Post, UseGuards, Get, Request, Query, Put, NotFoundException, Delete, BadRequestException, } from "@nestjs/common"; import { UsersService } from "./users.service"; import { CreateUserDto, UpdateUserDto, UserDto, CloseAccount, } from "./objects/create-user.dto"; import { abstractBaseControllerFactory } from "../common/base/base.controller"; import { LoggedInToken } from "./objects/login-user.dto"; import { BASEROUTES, USER_TYPES, KYC_VERIFICATION_STATUS, USER_STATUS, ROLES_ACCESS_ACTION, } from "../common/constants/enum"; import { JwtAuthGuard } from "../auth/auth.guard"; import { RequestUser, LastUpdatedTime, IdOrCodeParser, } from "../common/utils/controller.decorator"; import { UNKNOWN_PARAM, NOT_FOUND, PAGE_NOT_FOUND_404, NEW_PASSWORD_AND_CONFIRM_NEW_PASSWORD_ERROR, USERNAME_OR_PASSWORD_INCORRECT, CURRENT_PASSWORD_AND_NEW_PASSWORD_ERROR, KYC_PENDING_STATUS_CHANGE_ERROR, KYC_APPROVED_STATUS_CHANGE_ERROR, KYC_REJECTED_STATUS_CHANGE_ERROR, USER_ACTIVE_STATUS_CHANGE_ERROR, USER_CLOSED_STATUS_CHANGE_ERROR, USER_IN_REVIEW_STATUS_CHANGE_ERROR, USER_KYC_INCOMPLETE_STATUS_CHANGE_ERROR, ONLY_FOR_ADMIN, } from "../common/constants/string"; import { plainToClass } from "class-transformer"; import { success } from "../common/base/httpResponse.interface"; import { AbstractClassTransformerPipe } from "../common/pipes/class-transformer.pipe"; import { normalizeObject } from "../common/utils/helper"; import * as _ from "lodash"; import { InjectModel } from "@nestjs/mongoose"; import { Model } from "mongoose"; import { IUser, User } from "./objects/user.schema"; import { CrmService } from "./crm/crm.service"; import { KycPendingEmail } from "./objects/user.registered.email"; import { EmailDto } from "../email/objects/email.dto"; import { normalizePaginateResult } from "../common/interfaces/pagination"; import { RolesService } from "../roles/roles.service"; const BaseController = abstractBaseControllerFactory({ DTO: UserDto, DisabledRoutes: [ BASEROUTES.PATCH, //, BASEROUTES.DETELEONE ], }); @Controller("users") export class UsersController extends BaseController { constructor( private usersService: UsersService, private crmService: CrmService, @InjectModel("User") private readonly usersModel: Model, private rolesservice: RolesService ) { super(usersService); } @UseGuards(JwtAuthGuard) @Get() async findList(@Request() req, @Query() query, @RequestUser() user) { let _user = await this.rolesservice.findOneByQuery({roleName: user.type}) // if(user.type == USER_TYPES.OPERATIONS_TEAM){ let hasAccessOperations = _user.rolesAccessAction.some( (e) => e === ROLES_ACCESS_ACTION.USERS_CONTROLLER_FINDLIST_OPERATIONS ); let hasAccessAdmin = _user.rolesAccessAction.some( (e) => e === ROLES_ACCESS_ACTION.USERS_CONTROLLER_FINDLIST_ADMIN ); if(hasAccessOperations) { console.log('userstype', user.type, _user, hasAccessOperations, hasAccessAdmin) let t = { $or: [{ type: USER_TYPES.SME }, { type: USER_TYPES.SPONSOR }] }; return await super.findList(req, { ...query, ...t }); } //if (user.isAdmin) { if (hasAccessAdmin){ // <--- only admin can see the user lists return await super.findList(req, { ...query }); } throw new NotFoundException(); } @UseGuards(JwtAuthGuard) @Get(":idOrCode") async findOne( @IdOrCodeParser("idOrCode") idOrCode: string, @RequestUser() user ) { let _user = await this.rolesservice.findOneByQuery({roleName: user.type}); console.log('userstype', user.type, _user) let hasAccess = _user.rolesAccessAction.some( (e) => e === ROLES_ACCESS_ACTION.USERS_CONTROLLER_FIND_ONE ); if (hasAccess || user.code === idOrCode || user.id === idOrCode) { // <--- only admin or the same person can view a profile return await super.findOne(idOrCode); } throw new NotFoundException(); } @UseGuards(JwtAuthGuard) @Post("filter/kycFilter") async findListFilterKYCStatus( @Request() req, @Query() query, @RequestUser() user, @Body() body: { isProfileCompleted: number } ) { let t = { $or: [{ type: USER_TYPES.SME }, { type: USER_TYPES.SPONSOR }] }; //if (user.type == USER_TYPES.ADMIN) { let _user = await this.rolesservice.findOneByQuery({roleName: user.type}); console.log('userstype', user.type, _user) let hasAccess = _user.rolesAccessAction.some( (e) => e === ROLES_ACCESS_ACTION.USERS_CONTROLLER_KYC_FILTER ); // console.log('userstype', user.type, _user, hasAccess) if(hasAccess) { var options = { limit: 30, page: 1, sort: "_id", skip: query.page ? (query.page - 1) : 0 }; // <--- only admin and Sponsor can see all the USER lists console.log("filterrrrrrrrrrr", query, query.page, body.isProfileCompleted); let d = await this.usersModel.find( { "verification.isProfileCompleted": body.isProfileCompleted, ...t }, {}, { sort: { _id: 1 }, skip: options.skip * options.limit, limit: options.limit, projection: {} } ); let dCount = await this.usersModel.count( { "verification.isProfileCompleted": body.isProfileCompleted } ); console.log(d.length, dCount); await d.map((data) => { return plainToClass(UserDto, data, { excludeExtraneousValues: true }); }); let pagination = normalizePaginateResult({ total: dCount,//d.length, limit: options.limit, page: options.page, pages: d.pages, }); return success({ d, pagination }); } throw new NotFoundException(); }

In the 3 methods listed above, findList, findOne, findListFilterKYCStatus, we are checking if the user has access/authorization.For the method findListFilterKYCStatus let hasAccess = _user.rolesAccessAction.some(
(e) => e === ROLES_ACCESS_ACTION.USERS_CONTROLLER_KYC_FILTER
);
, we are checking if the user has ROLES_ACCESS_ACTION.USERS_CONTROLLER_KYC_FILTER listed in his roles Schema as shown below in the file.Here, only users of type USER_TYPES.OPERATIONS_TEAM and USER_TYPES.ADMIN have the permissions and only they are allowed access to the findListFilterKYCStatus() method.
Code (Part-III):
src/roles/roles.controller.ts
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 import { RolesDto, CreateRolesDto } from './objects/roles.dto'; import { abstractBaseControllerFactory } from '../common/base/base.controller'; import { BASEROUTES, USER_TYPES, ROLES_ACCESS_ACTION } from '../common/constants/enum'; import { RolesService } from './roles.service'; import { JwtAuthGuard } from '../auth/auth.guard'; import { Controller, Get, UseGuards, Request, Query, Put, Body, Post, BadRequestException, NotFoundException, Delete, } from "@nestjs/common"; import { AbstractClassTransformerPipe } from '../common/pipes/class-transformer.pipe'; import { RequestUser } from '../common/utils/controller.decorator'; import { plainToClass } from 'class-transformer'; import { success } from '../common/base/httpResponse.interface'; const BaseController = abstractBaseControllerFactory({ DTO: RolesDto, //Todo: Remove after creating records in Db. CreateDTO: CreateRolesDto, DisabledRoutes: [ //Todo: Uncomment BASEROUTES.CREATE after creating records in Db. // BASEROUTES.CREATE, // BASEROUTES.DETELEONE, BASEROUTES.PATCH, // BASEROUTES.UPDATEONE, ], }); @UseGuards(JwtAuthGuard) @Controller('roles') export class RolesController extends BaseController { constructor(private rolesservice: RolesService) { super(rolesservice); } @Post() public async create( @Request() req, @Body(AbstractClassTransformerPipe(CreateRolesDto)) body: any, @Query() query, @RequestUser() user ) { switch(body.roleName){ case USER_TYPES.ADMIN: body.rolesAccessAction = [ ROLES_ACCESS_ACTION.USERS_CONTROLLER_FINDLIST_ADMIN, ROLES_ACCESS_ACTION.USERS_CONTROLLER_FIND_ONE, ROLES_ACCESS_ACTION.USERS_CONTROLLER_KYC_FILTER, ROLES_ACCESS_ACTION.USERS_CONTROLLER_USER_STATUS_FILTER, ROLES_ACCESS_ACTION.USERS_CONTROLLER_USER_UPDATE, ROLES_ACCESS_ACTION.USERS_CONTROLLER_DELETE, ROLES_ACCESS_ACTION.USERS_SERVICE_CHECK_FOR_UPDATE_STATUS_ERROR, ROLES_ACCESS_ACTION.USERS_SERVICE_CREATE, ROLES_ACCESS_ACTION.USERS_SERVICE_UPDATE_USER_CRMID_AND_ENTITYDETAILCODE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_CREATE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDONE, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_UPDATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_COMPLETE_BID_PROCESS, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_REJECT_ALL_BIDS_DELETE_PROJECT, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_UPDATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_CREATE_TRANSACTION ]; break; case USER_TYPES.OPERATIONS_TEAM: body.rolesAccessAction = [ ROLES_ACCESS_ACTION.USERS_CONTROLLER_FINDLIST_OPERATIONS, ROLES_ACCESS_ACTION.USERS_CONTROLLER_FIND_ONE, ROLES_ACCESS_ACTION.USERS_CONTROLLER_KYC_FILTER, ROLES_ACCESS_ACTION.USERS_CONTROLLER_USER_STATUS_FILTER, ROLES_ACCESS_ACTION.USERS_CONTROLLER_USER_UPDATE, ROLES_ACCESS_ACTION.USERS_CONTROLLER_DELETE, ROLES_ACCESS_ACTION.USERS_SERVICE_CHECK_FOR_UPDATE_STATUS_ERROR, ROLES_ACCESS_ACTION.USERS_SERVICE_CREATE, ROLES_ACCESS_ACTION.USERS_SERVICE_UPDATE_USER_CRMID_AND_ENTITYDETAILCODE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_CREATE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDONE, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_UPDATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_COMPLETE_BID_PROCESS, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_REJECT_ALL_BIDS_DELETE_PROJECT, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_UPDATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_CREATE_TRANSACTION ]; break; case USER_TYPES.SME: body.rolesAccessAction = [ ROLES_ACCESS_ACTION.USERS_SERVICE_UPDATE_USER_CRMID_AND_ENTITYDETAILCODE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_FINDLIST_SME, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_CREATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_FINDLIST_SME ]; break; case USER_TYPES.SPONSOR: body.rolesAccessAction = [ ROLES_ACCESS_ACTION.USERS_SERVICE_UPDATE_USER_CRMID_AND_ENTITYDETAILCODE, ROLES_ACCESS_ACTION.REMARKS_CONTROLLER_FINDLIST_SPONSOR, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDLIST, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_FINDONE, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_RECOMMENDED_PROJECTS, ROLES_ACCESS_ACTION.SME_PROJECT_CONTROLLER_SPONSOR_FILTER, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_FINDLIST_SPONSOR, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_CREATE, ROLES_ACCESS_ACTION.BID_DETAILS_CONTROLLER_UPDATE_SPONSOR, ROLES_ACCESS_ACTION.BID_DETAILS_SERVICE_CALCULATE_BID_DETAILS ]; break; } let roles = await this.rolesservice.create(body); const _data = plainToClass(RolesDto, roles, { excludeExtraneousValues: true }); return success(_data); } }

The role's permissions are stored in the backend (MongoDB)
Code (Part-IV):
src/roles/objects/roles.schema.ts
// hidden setup JavaScript code goes in this preamble area const hiddenVar = 42 import { Schema } from "mongoose"; import { createModel, Entity, IEntity } from "../../common/base/base.model"; export class Roles extends Entity { roleName: string; roleCode: string; type: string; rolesAccessAction: string[]; } export interface IRoles extends Roles, IEntity { id: string; } export const RolesSchema: Schema = createModel("AdminRoles", { roleName: { type: String, required: true }, roleCode: { type: String, required: true }, type: { type: String, required: true}, rolesAccessAction: [ { type: String, }, ] });

This is how custom role based access is implemented without any 3rd party libraries.
Source code: [Link] https://gitlab.com/adh.ranjan/nestjs

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

đź‘‹ Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay