Skip to content

Commit

Permalink
Feature/issue 199/protect routing with role (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
lcaohoanq authored Jul 5, 2024
1 parent 267069f commit 8a5082d
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 42 deletions.
13 changes: 12 additions & 1 deletion src/errors/errors.entityError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@ import { USER_MESSAGES } from "~/modules/user/user.messages";
type ErrorsType = Record<string, { msg: string; [key: string]: string }>;

interface IErrorWithStatus {
from?: string; // Make sure the from field is optional
message: string;
status: number;
}

export class ErrorWithStatus implements IErrorWithStatus {
from?: string; // Include the from field here
message: string;
status: number;
constructor({ message, status }: IErrorWithStatus) {

// Adjusted constructor to handle the from field
constructor({ from, message, status }: IErrorWithStatus) {
this.from = from;
this.message = message;
this.status = status;
}
}

export class ProtectRouterError extends ErrorWithStatus {
constructor({ message, status }: IErrorWithStatus) {
super({ from: "ProtectRouterError", message, status });
}
}

export class ErrorEntity extends ErrorWithStatus {
data: ErrorsType;
constructor({
Expand Down
4 changes: 4 additions & 0 deletions src/modules/protectRouting/protect.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export const PROTECT_MESSAGES = {
ROLE_ADMIN: "You are admin",
ROLE_CUSTOMER: "You are customer",
ROLE_EMPLOYEE: "You are employee",
ROLE_NOT_FOUND: "Role not found",
ACCESS_DENIED: "Access denied",
UNAUTHORIZED: "Unauthorized",
ROUTE_AND_ROLE_MIS_MATCH: "Route and role mismatch",
ROUTES_CONFIG_WRONG: "Routes config is wrong",
MISSING_ACCESS_TOKEN: "Missing access token",
} as const;
25 changes: 25 additions & 0 deletions src/modules/protectRouting/protect.utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UserRole } from "../user/user.enum";
const routes: Module[] = require("./mapRouteWithRole.json");

// Interface for a Route object within a module
Expand All @@ -14,6 +15,20 @@ interface Module {
route: { [key: string]: Route };
}

interface RouteConfig {
path: string;
roles: UserRole[];
}

export const routesConfig: RouteConfig[] = [
{
path: "/admin",
roles: [UserRole.Employee, UserRole.Customer, UserRole.Admin],
}, //Admin can access all
{ path: "/user", roles: [UserRole.Customer] }, // User only access to Customer
{ path: "/employee", roles: [UserRole.Employee] }, // Employee only access to Employee
];

export function getOpenRoutes(): string[] {
const openRoutes: string[] = [];

Expand All @@ -28,3 +43,13 @@ export function getOpenRoutes(): string[] {

return openRoutes;
}

// math the route.path with the req.path (/user/login) but take the first part only (/user)
export function checkRole(
path: string,
pattern: string,
): RouteConfig | undefined {
return routesConfig.find(
(route) => route.path === pattern + path.split(pattern)[1],
);
}
108 changes: 67 additions & 41 deletions src/modules/user/user.middlewares.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import 'dotenv/config'
import { NextFunction, Request, Response } from 'express'
import { ParamsDictionary } from 'express-serve-static-core'
import { ParamSchema, checkSchema } from 'express-validator'
import "dotenv/config";
import { NextFunction, Request, Response, response } from "express";
import { ParamsDictionary } from "express-serve-static-core";
import { ParamSchema, checkSchema } from "express-validator";
import { StatusCodes } from "http-status-codes";
import { JsonWebTokenError } from 'jsonwebtoken'
import { capitalize, escape } from 'lodash'
import { ObjectId } from 'mongodb'
import validator from 'validator'
import { HTTP_STATUS } from '~/constants/httpStatus'
import databaseService from '~/database/database.services'
import { ErrorEntity, ErrorWithStatus } from '~/errors/errors.entityError'
import { USER_MESSAGES } from '~/modules/user/user.messages'
import { isDeveloperAgent } from '~/utils/agent'
import { encrypt, hashPassword } from '~/utils/crypto'
import { JsonWebTokenError } from "jsonwebtoken";
import { capitalize, escape } from "lodash";
import { ObjectId } from "mongodb";
import validator from "validator";
import { HTTP_STATUS } from "~/constants/httpStatus";
import databaseService from "~/database/database.services";
import {
ErrorEntity,
ErrorWithStatus,
ProtectRouterError,
} from "~/errors/errors.entityError";
import { USER_MESSAGES } from "~/modules/user/user.messages";
import { isDeveloperAgent } from "~/utils/agent";
import { encrypt, hashPassword } from "~/utils/crypto";
import { numberToEnum } from "~/utils/handler";
import { verifyToken } from '~/utils/jwt'
import { isValidPhoneNumberForCountry, validate } from '~/utils/validation'
import { OTP_STATUS } from '../otp/otp.enum'
import { OTP_MESSAGES } from '../otp/otp.messages'
import otpService from '../otp/otp.services'
import { NoticeUser, Subscription, UserVerifyStatus, UserRole, } from './user.enum'
import { LoginRequestBody, TokenPayload } from './user.requests'
import usersService from './user.services'
import { StatusCodes } from 'http-status-codes'


import { verifyToken } from "~/utils/jwt";
import { isValidPhoneNumberForCountry, validate } from "~/utils/validation";
import { OTP_STATUS } from "../otp/otp.enum";
import { OTP_MESSAGES } from "../otp/otp.messages";
import otpService from "../otp/otp.services";
import { PROTECT_MESSAGES } from "../protectRouting/protect.messages";
import { checkRole, routesConfig } from "../protectRouting/protect.utils";
import { NoticeUser, UserRole, UserVerifyStatus } from "./user.enum";
import { LoginRequestBody, TokenPayload } from "./user.requests";
import usersService from "./user.services";
//! Prevent db injection, XSS attack
export const paramSchema: ParamSchema = {
customSanitizer: {
Expand Down Expand Up @@ -1000,12 +1003,12 @@ export const accessTokenValidatorV2 = validate(
custom: {
options: async (value: string, { req }) => {
const access_token = value.split(" ")[1];
// if do not have access_token, throw error
// if do not have access_token, throw error bad request (reason: lack of access_token)
// because we already passed openRoutes
if (!access_token) {
throw new ErrorWithStatus({
message: USER_MESSAGES.ACCESS_TOKEN_IS_REQUIRED,
status: HTTP_STATUS.UNAUTHORIZED,
throw new ProtectRouterError({
message: PROTECT_MESSAGES.ACCESS_DENIED,
status: HTTP_STATUS.BAD_REQUEST,
});
}

Expand All @@ -1023,22 +1026,45 @@ export const accessTokenValidatorV2 = validate(
const user = await usersService.findUserByID(
decoded_authorization.user_id,
);

const role = user?.role;

if (role === UserRole.Admin) {
console.log("User is Admin");
} else if (role === UserRole.Customer) {
console.log("User is Customer");
} else {
console.log("User is Employee");
// if role not found, throw error
// this case only happen when data in database do not have role field
if (!role) {
throw new ProtectRouterError({
message: PROTECT_MESSAGES.ROLE_NOT_FOUND,
status: HTTP_STATUS.BAD_REQUEST,
});
}

const route = checkRole(req.path, "/");

// route not found when req.path is undefined or req.path is not in routesConfig
if (!route) {
throw new ProtectRouterError({
message:
PROTECT_MESSAGES.ROUTE_AND_ROLE_MIS_MATCH +
PROTECT_MESSAGES.ROUTES_CONFIG_WRONG,
status: HTTP_STATUS.BAD_REQUEST,
});
} else if (!route.roles.includes(role)) {
throw new ProtectRouterError({
message: PROTECT_MESSAGES.ACCESS_DENIED,
status: HTTP_STATUS.NOT_FOUND,
});
}
} catch (error) {
throw new ErrorWithStatus({
message: capitalize(
(error as JsonWebTokenError).message,
),
status: HTTP_STATUS.UNAUTHORIZED,
});
if (error instanceof JsonWebTokenError) {
throw new ProtectRouterError({
message: capitalize(
(error as JsonWebTokenError).message,
),
status: HTTP_STATUS.UNAUTHORIZED,
});
} else if (error instanceof ProtectRouterError) {
throw error;
}
}
return true;
},
Expand Down

0 comments on commit 8a5082d

Please sign in to comment.