5 Comments

Role-Based Routing with Next.js

Applications often have to cater to users with different roles, such as employees, customers, managers, etc. These different users have different permissions, and usually, they need to be restricted from certain functionalities.

The application that I’m working on handles only two kinds of users (customers and employees), but it needs to block the customers from accessing certain pages. We use the Next.js framework for our React app. Next.js uses file-system routing, which means all of our pages are stored in the pages directory. So, if we have a Reports page stored in our pages folder, it will be accessed by viewing www.ourapp.com/reports.

We wielded this routing to easily protect our role-specific pages by keeping them in a corresponding directory. Let’s check out an example.

Creating Our Filesystem

First, we create two folders within our pages directory, employee and customer. Our filesystem looks like this:

├── pages/
|  ├── index.js
|  ├── _app.js
|  ├── customer/
|  ├── employee/  

Our _app.js file provides custom initialization for all of our pages. This will help us with checking roles, and it also gives our app any state that we want to be accessible on every page. You can learn more about this here.

Now, we add the pages that are specific to a user’s role to their respective directories. We also put the pages that we want accessible to every user in our pages directory. Our filesystem now looks like this:

├── pages/
|  ├── index.js
|  ├── _app.js
|  ├── customer/
|     ---- index.js
|  ├── employee/
|     ---- index.js
|     ---- reports.js
|     ---- orders.js
|  ├── login.js
|  ├── logout.js 

Here, we can start to see some of the role-based routing organization. Our login and logout pages are housed under our pages directory because we want all users to be able to access them. But our reporting and ordering pages are held under the employee folder because we only want employees to access them.

Both our employee and customer directories have index files which will be the entry point for each user’s app. This application only provides one page for customers.

Checking User Roles

Now we need to utilize the structure we’ve created in our code. Our pages/_app.js file looks something like this:


// pages/_app.js

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}
export default MyApp;

Every time someone routes to a page, this file renders it as the component provided by the props. This custom file allows us to do our role checking with a few code changes. Every application does authentication a bit differently, so this example assumes that you’ve already logged in a user.


// pages/_app.js
import {React, useContext} from 'react'
import Home from "index.js";
import { useRouter } from "next/router";
import { AppState } from "components/app-state"; 

function MyApp({ Component, pageProps }) {
    const appState = useContext(AppState);
    const user = appState.user;
    const role = user.role;
    let allowed = true;
    const router = useRouter();
    if (router.pathname.startsWith("/employee") && role !== "employee") {
      allowed = false;
    }
    if (router.pathname.startsWith("/customer") && role !== "customer") {
      allowed = false;
    }
    const ComponentToRender = allowed ? Component : Home; 
    return <ComponentToRender {...pageProps} />
}

export default MyApp

We’ve imported the appState in order to get the user. Our app uses React’s useContext hook for authentication and app-wide user information—but we’ll save that for another blog post.

Here, we assume that a user is already logged in and their role has been set. Now can get the path from Next’s router and check if its directory matches the user’s role. If it doesn’t, we have our app redirect to the Home page, assuming that our Home component (which is exported by our index.js file) contains logic similar to this:


// pages/index.js
 if (appState.user.role === "employee") {
    await router.replace("/employee");
}
if (appState.user.role === "customer") {
    await router.replace("/customer");
}

This will route the app to the index.js file for either directory. Now we can add as many pages as we want to the role-specific directories without having to write any more code to handle access. Awesome!