Role Based Access Control (RBAC)

The RBAC component provides a clean and reusable middleware to restrict API access based on user roles (e.g., admin, user, manager). It integrates seamlessly with the authentication system.

Installation Guide

npx servercn-cli add rbac

Prerequisites

This component assumes you have:

  1. Authentication Middleware: Detailed in the Verify Auth Middleware component.
  2. User Model: Your user schema/model should include a role field.
  3. Populated User: Your req.user object must contain the role property.

If your authentication middleware only attaches the user ID (e.g., from a JWT payload), you may need to fetch the full user profile or include the role in the JWT payload.

Or follow the guides:

Ensure the following environment variables are defined in .env:

PORT = "3000";
NODE_ENV = "development";
LOG_LEVEL = "info";
JWT_ACCESS_SECRET = "your-access-secret";
JWT_REFRESH_SECRET = "your-refresh-secret";

Ensure the following configuration are defined:

src/configs/env.ts
interface Config {
  PORT: number;
  NODE_ENV: string;
  LOG_LEVEL: string;
  JWT_REFRESH_SECRET: string;
  JWT_ACCESS_SECRET: string;
}
 
const env: Config = {
  PORT: Number(process.env.PORT) || 3000,
  NODE_ENV: process.env.NODE_ENV || "development",
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
  JWT_REFRESH_SECRET: process.env.JWT_ACCESS_SECRET!,
  JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET!
};
 
export default env;

To ensure the authentication middleware functions correctly, your project must define a User model with a structure similar to the following.

src/models/user.model.ts
import mongoose, { Document, Model, Schema } from "mongoose";
import { USER_ROLES, userRoles } from "../types/user";
 
export interface IUser extends Document {
  _id: mongoose.Types.ObjectId;
  name: string;
  email: string;
  password: string;
  role: userRoles;
  isEmailVerified: boolean;
 
  createdAt: Date;
  updatedAt: Date;
}
 
const userSchema = new Schema<IUser>(
  {
    name: {
      type: String,
      required: [true, "Name is required"],
      trim: true
    },
    email: {
      type: String,
      required: [true, "Email is required"],
      unique: true,
      lowercase: true,
      trim: true
    },
    password: {
      type: String,
      select: false,
      default: null
    },
    role: {
      type: String,
      enum: USER_ROLES,
      default: "user"
    },
    isEmailVerified: {
      type: Boolean,
      default: false
    }
  },
  {
    timestamps: true
  }
);
 
const User: Model<IUser> = mongoose.model<IUser>("User", userSchema);
export default User;

To access authenticated user data inside request handlers, define a custom request type.

src/types/user.ts
import { Request } from "express";
import mongoose from "mongoose";
 
export const USER_ROLES = ["admin", "user", "super-admin"] as const;
export type userRoles = (typeof USER_ROLES)[number];
 
export interface UserRequest extends Request {
  user?: {
    _id: string | mongoose.Types.ObjectId;
    role: userRoles;
    email: string;
    [key: string]: any;
  };
}

How It Works

The middleware accepts a list of allowed roles. When a request hits a protected route:

  1. It checks if req.user exists.
  2. It compares req.user.role against the allowed roles.
  3. If matched, the request proceeds (next()).
  4. If not matched, it returns a 403 Forbidden error.

Basic Implementation

Here is the core logic for the RBAC middleware.

src/middlewares/authorize-role.ts
import { NextFunction, Request, Response } from "express";
import { ApiError } from "../utils/api-error";
import { UserRequest, userRoles } from "../types/user";
 
export const authorizeRoles = (...allowedRoles: userRoles[]) => {
  return (req: UserRequest, res: Response, next: NextFunction) => {
    // 1. Check if user is authenticated
    if (!req.user) {
      return next(ApiError.unauthorized("Unauthorized, Please login first."));
    }
 
    // 2. Check if user has required role
    // Note: Ensure 'role' exists on req.user. You might strictly type this.
    if (
      !req.user.role ||
      !allowedRoles.includes(req?.user?.role as userRoles)
    ) {
      return next(
        ApiError.forbidden(
          "Forbidden. You do not have permission to access this resource"
        )
      );
    }
    next();
  };
};
src/shared/middlewares/authorize-role.ts
import { NextFunction, Request, Response } from "express";
import { ApiError } from "../errors/api-error";
import { UserRequest, userRoles } from "../../types/user";
 
export const authorizeRoles = (...allowedRoles: userRoles[]) => {
  return (req: UserRequest, res: Response, next: NextFunction) => {
    // 1. Check if user is authenticated
    if (!req.user) {
      return next(ApiError.unauthorized("Unauthorized, Please login first."));
    }
 
    // 2. Check if user has required role
    // Note: Ensure 'role' exists on req.user. You might strictly type this.
    if (
      !req.user.role ||
      !allowedRoles.includes(req?.user?.role as userRoles)
    ) {
      return next(
        ApiError.forbidden(
          "Forbidden. You do not have permission to access this resource"
        )
      );
    }
    next();
  };
};

Usage Example

Use authorizeRoles in your route definitions. It typically runs after your authentication middleware.

import { Response, Router } from "express";
import { verifyAuthentication } from "../middlewares/verify-auth";
import { UserRequest } from "../types/user";
import { ApiResponse } from "../utils/api-response";
import { authorizeRoles } from "../middlewares/authorize-role";
 
const router = Router();
 
router.get(
  "/profile",
  verifyAuthentication,
  authorizeRoles("user", "admin"),
  (req: UserRequest, res: Response) => {
    return ApiResponse.ok(res, "User profile", req.user);
  }
);
 
export default router;

Extending Types

To make TypeScript happy with req.user.role, extend your Express Request type definition.

src/types/user.ts
import { Request } from "express";
 
// Define your roles type
export type UserRole = "admin" | "user" | "manager";
 
export interface UserRequest extends Request {
  user?: {
    _id: string;
    role: UserRole; // Add this line
    email?: string;
    [key: string]: any;
  };
}

File & Folder Structure

Loading files...

Installation

npx servercn-cli add rbac