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!

Conversation
  • Chris says:

    Thanks for sharing your approach. I like the ease of how you can just put the files you need into the folders without having to write extra logic. It seems a bit rigid though. What if we need to redirect somewhere else for a specific employee page? Also, it seems like you now have the `employee` or `customer` part in your URL now, which I would like to avoid.

    I took the following approach and added an object to each page that needs authentication:

    “`
    Dashboard.auth = {
    acceptedRoles: [‘admin’, ‘user’],
    redirectPath: ‘/’,
    };
    “`

    Then I add some logic to the `getInitialProps` method in `_app.jsx`:

    “`
    if (
    get(Component, ‘auth.acceptedRoles’) &&
    !AuthHelpers.isAcceptedRole(loggedInUser.role, Component.auth.acceptedRoles)
    ) {
    return redirect(ctx, Component.auth.redirectPath || ‘/’);
    }
    “`

  • sad dev says:

    Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

  • Ori says:

    Hey and thanks for the article.
    I’m getting the same error as “sad dev”, which makes sense as you’re using hooks inside of a class component. So how is this working for you?

    Thanks!

    • Grace Du Mez Grace says:

      Hi there!
      I updated the code and I believe it’s compiling now. Sorry about that, React has changed since the writing of this post :)

  • hamza says:

    Hey Grace ,

    Thanks a lot for the very useful tutorial , I am having an error , how can you use the router in server side : I am still getting this error :

    No router instance found. you should only use “next/router” inside the client side of your app .

    Cordialemnent

  • Vahid Kh says:

    Thanks Grace for your solution :)

  • Noushin says:

    Hi. Thank you for the article. I tried to use the same approach for authentication and it does not work! Have you written about authentication in Next.js?

  • james says:

    Hi Grace,

    Great article!!! Just what I was looking for. Do you have the github link for this? Or this is private? If it is private do you have an example app that implement this redirect pattern?

  • Comments are closed.