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
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
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
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
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.
Link to code: https://gitlab.com/adh.ranjan/nestjs/-/tree/dSuahailTwo
Also published at https://dev.to/krishnakurtakoti/custom-role-based-access-in-nest-js-mongodb-5b3b