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

This component requires additional ServerCN components.

👉 Note: You do not need to install any servercn dependencies manually. Installing this component will automatically install all required servercn dependencies. Manual installation is optional if you prefer to manage dependencies yourself.

  • HTTP Status Codes:
npx servercn add http-status-codes

Documentation: HTTP Status Codes

  • Api Response Handler:
npx servercn add response-formatter

Documentation: Api Response Handler

  • Api Error Handler:
npx servercn add error-handler

Documentation: Api Error Handler

  • JWT Authentication:
npx servercn add jwt-utils

Documentation: JWT Authentication

  • Logger:
npx servercn add logger

Documentation: Logger

npx servercn add rbac

⚠️ If this dependency is not installed, the component will not function correctly.

Prerequisites

This component assumes you have:

  1. Authentication Middleware: Detailed in the JWT Auth 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.

Note: 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

Select a file to view its contents

Installation

npx servercn add rbac