Role-based authorization || Role-based access-control in React.js


In my recent project, my task was to implement role-based authorization/access-control on routes as well as on components, for achieving this purpose I have read couple of good articles but finally, I decided to write my own role-based authorization/access-control logic from scratch.

I am not too much smart but here I think my logic is quite better than the articles I have read.


So how can we do that?

The idea is, simply prevent the app to generate unnecessary routes, rather checking current user role on each route it is great to generate only the routes that user have access.

Yup, I am damn serious.
Ok, so let’s start the magic 🧙 🧙 🧙

First of all, define the routing scheme in your app I have defined my global routes in App.js you can define in the same or somewhere else.

/*  /src/App.js  */
import React from 'react';
import { BrowserRouter, Route, Switch } from "react-router-dom";
import PublicRoutes from './routes/PublicRoutes';
import PrivateRoutes from './routes/PrivateRoutes';
import history from './util/history';

const App = () => (
<BrowserRouter history={history}>
<Switch>
<Route path="/app" component={PrivateRoutes} />
<Route path="/" component={PublicRoutes} />
</Switch>
</BrowserRouter>
);

export default App;

In this example, all private routes start after /app/ but you can use your own route’s path/address convention.
you can add other global/public routes here or in PublicRoutes.js

/*  /src/routes/PublicRoutes.js  */
import React from 'react';
import { Route } from "react-router-dom";
import LoginForm from '../container/LoginForm';
import
RegistrationForm from '../container/RegistrationForm';
import
ForgotPasswordForm from '../container/ForgotPasswordForm';

const PublicRoutes = ({match}) => (
<div>
<Route
path=
{`${match.path}forgot-password`}
component={ForgotPasswordForm}
/>
<Route
path=
{`${match.path}register`}
component={RegistrationForm}
/>
<Route
path=
{`${match.path}`}
exact component={LoginForm}
/>
</div>
);

export default PublicRoutes;

when you use the component as a route couple of props are available in component match one of them, so we have to get parent component route address using match.path and concatenate child component route the result could be something likeyour-domain-name/parent-route/child-route but in the above example PublicRoutes component render on the / means no parent route.

Hmmm, you are right but the magic gonna start here 😃😃😃

Create a roles configuration file in which define all the roles and their corresponding routes.

roles in my case.
/*  /config/rolesConfig.js || /config/roles.js  */
export default {
//role name as a key.
headOfOperation: {
routes: [
{
component: 'OnlyForHeadOfOperation',
url: '/only-for-head-of-operation'
},
{
component: 'HeadOfOperationAndManager',
url: '/hoo-manager'
},
{
component: 'HeadOfOperationManagerAndHeadCashier',
url: '/hoo-manager-head-cashier'
}
],
},
manager: {
routes: [
{
component: 'HeadOfOperationAndManager',
url: '/hoo-manager'
},
{
component: 'OnlyForManager',
url: '/manager-only'
},
{
component: 'HeadOfOperationManagerAndHeadCashier',
url: '/hoo-manager-head-cashier'
}
],
},
headCashier: {
routes: [
{
component: 'HeadOfOperationManagerAndHeadCashier',
url: '/hoo-manager-head-cashier'
}
],
},
branchLead: {
routes: [],
},
branchMember: {
routes: [],
},
teamLead: {
routes: [],
},
teamMember: {
routes: [],
},
common: {
routes: [
{
component: 'CommonRoute',
url: '/common-component'
}
]
}
}
  1. Define each role as a key to the object.
  2. Each key holds a config object, for now, this object contains only the routes key but later we will further extend this object when adding permission on components.
  3. Define objects inside of routes array, the object typically contains two fields component and url both are a string.
  4. Remember component key hold the same name that exports from the component file but as a string.
  5. common is accessible for all registered users,

Note all private routes export from the single file inside an object like below.

import React from 'react';

// TODO: Import all private routes here and add in export object.

const
OnlyForHeadOfOperation = () => (
<h1>Only For HeadOfOperation</h1>
);
const HeadOfOperationAndManager = () => (
<h1>HeadOfOperation And Manager</h1>
);
const OnlyForManager = () => (
<h1>Only For Manager</h1>
);
const HeadOfOperationManagerAndHeadCashier = () => (
<h1>HeadOfOperation Manager And HeadCashier</h1>
);
const CommonRoute = () => (
<h1>Common Route</h1>
);

export {
OnlyForHeadOfOperation,
HeadOfOperationAndManager,
OnlyForManager,
HeadOfOperationManagerAndHeadCashier,
CommonRoute
}

See all export name same as to define in /src/congig/roles.js

import React from 'react';
import { Route, Link } from "react-router-dom";
import { uniqBy } from 'lodash';
import rolesConfig from '../config/roles';
import * as Routes from './index';


// TODO: Replace hardcoded roles with redux, localStorage, or get from server.
const roles = [
//user roles + concatinate common role
...['headOfOperation', 'manager'],
'common'
];

let allowedRoutes = roles.reduce((acc, role) => {
return [
...acc,
...rolesConfig[role].routes
]
}, []);

// For removing duplicate entries.
allowedRoutes = uniqBy(allowedRoutes, 'component');

const PrivateRoutes = ({match}) => (
<div>
{/*<Header> Header </Header>*/}
<section>
<div>
{
allowedRoutes.map(({component, url}) => (
<Route
key=
{component}
path={`${match.path}${url}`}
component={Routes[component]}
/>
))
}
</div>
</section>
{/*<Footer> Footer </Footer>*/}
</div>
);

export default PrivateRoutes;

In my case, the user can hold multiple roles and we have added common routes for every registered user, and reduce them in a single array called allowedRoutes then remove redundant routes from the allowedRoutes array using lodash's uniqBy method and finally map over allowedRoutes array.

Now have a close look at the map.

{
{/*
component contain name of the route component as a string.
due to same name we can access Routes object key using array
notation like Routes[component]
*/}
allowedRoutes.map(({component, url}) => (
<Route
key=
{component}
path={`${match.path}${url}`}
component={Routes[component]}
/>
))
}

Hur Hur hurry, we have all done.

Benefits

  1. Check at once
  2. Generate only the routes that user have access
  3. The central roles configuration file
  4. Easy to update roles permission
  5. Multiple roles support
  6. Common routes support, etc

Happy coding…