diff --git a/src/errors/errors.entityError.ts b/src/errors/errors.entityError.ts index c7e09da..e8c51f7 100644 --- a/src/errors/errors.entityError.ts +++ b/src/errors/errors.entityError.ts @@ -4,19 +4,30 @@ import { USER_MESSAGES } from "~/modules/user/user.messages"; type ErrorsType = Record; 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({ diff --git a/src/modules/protectRouting/protect.messages.ts b/src/modules/protectRouting/protect.messages.ts index 3109722..f3d7bcc 100644 --- a/src/modules/protectRouting/protect.messages.ts +++ b/src/modules/protectRouting/protect.messages.ts @@ -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; diff --git a/src/modules/protectRouting/protect.utils.ts b/src/modules/protectRouting/protect.utils.ts index d90f7c8..8f595d0 100644 --- a/src/modules/protectRouting/protect.utils.ts +++ b/src/modules/protectRouting/protect.utils.ts @@ -1,3 +1,4 @@ +import { UserRole } from "../user/user.enum"; const routes: Module[] = require("./mapRouteWithRole.json"); // Interface for a Route object within a module @@ -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[] = []; @@ -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], + ); +} diff --git a/src/modules/user/user.middlewares.ts b/src/modules/user/user.middlewares.ts index 15b2149..2e12fbc 100644 --- a/src/modules/user/user.middlewares.ts +++ b/src/modules/user/user.middlewares.ts @@ -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: { @@ -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, }); } @@ -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; },