Controlling access to pages using React Router

July 07, 2018 0 Comments

Controlling access to pages using React Router

 

 

When I first started with React, I had an issue. I needed to control access to certain pages in a SPA I built using React and React Router and I couldn't find any libraries that could help me with this, so I decided to write a little utility to help me with this issue. I called it my React Router middleware. Cheesy, right?

A sample scenario

Say I have two views. Login (<Login />), admin dashboard (<AdminDashboard />)and super admin (<SuperAdminDashboard />). For login, I want only users who are not currently logged in to view this page. Admin should only be seen by admins and super admins by user's with role super admin.

Say I have my Routes setup this way:

<Router history={browserHistory} onUpdate={(a) => console.log(a)}> <ErrorBoundary> <Route exact path='/login' render={() => <Login />} /> <Route exact path='/admin' render={() => <AdminDashboard />} /> <Route exact path='/super-admin' render={() => <SuperAdminDashboard />} /> <ErrorBoundary/>
<Router />

The middleware

My middleware code looks like this

 routeToDisplay (middlewares = [], routeToVisit, directedFrom = '', extra = {}) { const mware = { privateRoute: (routeToVisit, directedFrom) => this.privateRoute(routeToVisit, ), alreadyLoggedIn: (routeToVisit) => this.alreadyLoggedIn(routeToVisit), adminAccess: (routeToVisit) => this.adminAccess(routeToVisit), superAdminAccess: (routeToVisit, directedFrom) => this.superAdminAccess(routeToVisit, directedFrom), } let ret = null try{ for (let i = 0; i < middlewares.length; i++) { ret = mware[middlewares[i]](routeToVisit, directedFrom, extra) if (ret.status === false) { break } } return ret.routeObject }catch(e){ //handle error here } }

Confusing, right? Don't worry I will break it down. The function routeToDisplay is the brain behind the whole middleware idea. The function itself expects three arguments

routeToDisplay (middlewares = [], route, directedFrom = '/') { 

middlewares: A set of strings representing the names of the access protocols I'd like to implement on each route. For example, private router represents routes that should not be allowed access without being logged in first, alreadyLoggedIn represents routes I don't want a user to access once they are logged in, you can guess the rest.

routeToVisit: The route the user wishes to visit.

directedFrom: In case authorization isn't successful where do we direct the user to. For example in the case of private routes, we direct the user back to login.

The next line has a list of the middlewares that are available

const mware = { privateRoute: (routeToVisit, directedFrom) => this.privateRoute(routeToVisit, ), alreadyLoggedIn: (routeToVisit) => this.alreadyLoggedIn(routeToVisit), adminAccess: (routeToVisit) => this.adminAccess(routeToVisit), superAdminAccess: (routeToVisit, directedFrom) => this.superAdminAccess(routeToVisit, directedFrom), }

Basically, this is a just a list of middlewares maped to the function that implements the access protocol to that route. The function it maps to simply does the desired check for us, for example privateRoute would check if the user is logged in or not and allow access or deny access.

let ret = null
try{ for (let i = 0; i < midwares.length; i++) { ret = mware[midwares[i]](routeToVisit, directedFrom, extra) if (ret.status === false) { break } } return ret.routeObject }catch(e){ //handle error here }

The function above simply loops through the middleware names I passed in and calls the function associated with them. The functions return a status property which is true if access should be allowed or false if it should not be.

Since I am receiving an array of middlewares, it means I can pass multiple middlewares. This for loop can go through them and everything will be fine as long as status is true, but as soon as false is encountered, access will be denied. For example, I can pass in privateRoute and adminAccess. Private route may return true and adminAccess may return false, access will be denied in this case.

The last line return ret.routeObject returns whatever route the function returns to me. The thing is, if access is to be denied, the function will return a <Redirect /> route which will redirect the user back to login or someplace else. Let's see what one or two of the functions looks like.

privateRoute (component, pathname = '/') { return (auth.fetchCurrentUser !== null ? this._getRouteReturn(true, component) : this._getRouteReturn(false, <Redirect to={{ pathname: '/login', state: { from: pathname } }} />) ) }

The function takes the component we would like to display the route we want to redirect to in case authorization is denied. Inside the function, we check if the current user is defined or if the current user is logged in depending on how your authentication looks. If the user is we call the function

_getRouteReturn()

passing status and the component it should send back. Here is what _getRouteReturn looks like

_getRouteReturn (status, routeObject) { return {status, routeObject} }

If authorization was successful, we will get the current route back if not we will get the redirect route back. In this case, it will redirect us back to login. After login has been successful, we will be taken to the route we tried to access that we got denied from. For example, if we tried to access admin dashboard without being logged in, we will be redirected to login and once login is successful we will be directed to admin dashboard.

You can implement thesame for the other routes, for example the adminAccess route could look something like this.

adminAccess (component, pathname = '/') {
//in my case i stored my allowed roles in array form in my db, yours could be different if (utils.arrayContains(role, 'admin')) { return this._getRouteReturn(true, component) } return this._getRouteReturn(false, <Redirect to={{ pathname: `${this._account_help}${Constants.userRoles.admin}` }} />) }

So the above method like the one before it just takes the component we want to display. I am not passing the route I want to redirect to because this is not a login issue. Rather, I am passing a path to a route that displays information about what an admin account is so the user knows what has to be done to be an admin.

I think it's pretty straight forward.

Using it in our routes

Now our routes looked like this before

<Router history={browserHistory} onUpdate={(a) => console.log(a)}> <ErrorBoundary> <Route exact path='/login' render={() => <Login />} /> <Route exact path='/admin' render={() => <AdminDashboard />} /> <Route exact path='/super-admin' render={() => <SuperAdminDashboard />} /> <ErrorBoundary/>
<Router />

Let's give it a little makeover. Say I exported my middleware as default from its own class. I will import it like this earlier

import middleware from './middleware'
<Router history={browserHistory} onUpdate={(a) => console.log(a)}> <ErrorBoundary> <Route exact path='/login' render={() => middleware.routeDisplay(['alreadyLoggedIn'],<Login />) } /> <Route exact path='/admin' render={() => middleware.routeDisplay(['privateRoute','adminAccess'],<AdminDashboard />)} /> <Route exact path='/super-admin' render={() => middleware.routeDisplay(['privateRoute','superAdminAccess'], <SuperAdminDashboard />) } /> <ErrorBoundary/>
<Router />

And that's it. Let's look at the code for the entire middleware below.

import React from 'react'
import { Redirect
} from 'react-router-dom'
import auth from './stores/authenticationStore'
import utils from './stores/utils' class Middleware { routeToDisplay (middlewares = [], routeToVisit, directedFrom = '', extra = {}) { const mware = { privateRoute: (routeToVisit, directedFrom) => this.privateRoute(routeToVisit, ), alreadyLoggedIn: (routeToVisit) => this.alreadyLoggedIn(routeToVisit), adminAccess: (routeToVisit) => this.adminAccess(routeToVisit), superAdminAccess: (routeToVisit, directedFrom) => this.superAdminAccess(routeToVisit, directedFrom), } let ret = null try{ for (let i = 0; i < middlewares.length; i++) { ret = mware[middlewares[i]](routeToVisit, directedFrom, extra) if (ret.status === false) { break } } return ret.routeObject }catch(e){ //handle error here } } _getRouteReturn (status, routeObject) { return {status, routeObject} } adminAccess (component, pathname = '/') { //in my case i stored my allowed roles in array form in my db, yours could be different if (utils.arrayContains(role, 'admin')) { return this._getRouteReturn(true, component) } return this._getRouteReturn(false, <Redirect to={{ pathname: `${this._account_help}${Constants.userRoles.admin}` }} />) } privateRoute (component, pathname = '/') { return (auth.fetchCurrentUser !== null ? this._getRouteReturn(true, component) : this._getRouteReturn(false, <Redirect to={{ pathname: '/login', state: { from: pathname } }} />) ) }
}


Tag cloud