HomeCategoriesAll Tags

Role based routes protection in React + MobX app

It's quite common feature these days to have multiple user roles and based upon the user role we grant him access to different features of the application. Let's see how to create a similar feature in a React + MobX application along with react-router

Packages used are-

  1. react-router
  2. mobx
  3. mobx-react
  4. mobx-react-router

High level steps to follow-

  1. Create a json config of all the routes of your application.

  2. Create another config of roleAccess.

  3. Create a mobx router store.

  4. Call the updateAccess() @action of router store with user data. Thus, setting the required access based on the user role.

  5. Use react-router and sync it's history with mobx router store using mobx-react-router

  6. Setup ProtectedRoutes component which verifies authentication from auth store.

export const allPages = {
  home: {
    component: lazy(() => import(/* webpackChunkName: "home" */ '../containers/home')),
    path: '/',
    exact: true,
  },
  about: {
    component: lazy(() => import(/* webpackChunkName: "about" */ '../containers/about')),
    path: '/about',
  },
  contact: {
    component: lazy(() => import(/* webpackChunkName: "contact" */ '../containers/contact')),
    path: '/contact',
  }
}
}```


```javascript
const roleAccess = {
  admin: {
    page: [home, about, contact],
  },
  editor: {
    page: [home, about],
  },
  contributor: {
    page: [home],
  },
}
class RouterStore extends MobxRouterStore {
  @observable pages = []
  hasAccess = (path) => this.pages.includes(path)
  @action
  updateAccess = (user) => {
    const role = (user.role || lastRole).toLowerCase()
    this.pages = roleAccess[role].page
  }
}
const ReactRouter = (props) => (
  <Router history={syncHistoryWithStore(createBrowserHistory(), router)}>
    <Switch>
      <Route path="/login" component={Login} {...props} />
      <ProtectedRoutes path="" component={AppRoutes} {...props} />
    </Switch>
  </Router>
)
const AppRoutes = inject('router')(({ router: { pages } }) => {
  return (
    <App>
      <Suspense fallback={<LoadingSvg />}>
        <Switch>
          <NestedRoutes routes={pages} />
          <Route component={() => <div>Not Found</div>} />
        </Switch>
      </Suspense>
    </App>
  )
})
const NestedRoutes = ({ routes }) =>
  routes.map(({ component: Component, routes: nestedRoutes, switchRoutes, ...rest }) => {
    const component = (props) => (
      <>
        <Component {...props} {...rest} />
        {nestedRoutes && <NestedRoutes routes={nestedRoutes} />}
      </>
    )
    return <Route {...rest} render={component} key={rest.path} />
  })
@inject('auth')
@observer
class ProtectedRoutes extends Component {
  render() {
    const {
      component: Component,
      auth: { isAuthenticated, isAuthenticating },
      ...rest
    } = this.props

    if (isAuthenticating) {
      return null
    }

    return (
      <Route
        {...rest}
        render={props =>
          isAuthenticated ? (
            <Component {...props} />
          ) : (
            <Redirect
              to={{
                pathname: '/login',
                state: { from: props.location },
              }}
            />
          )
        }
      />
    )
  }
const GET_ME = gql`
  {
    me {
      email
      firstName
      lastName
      role
      id
    }
  }
`

class AuthStore {
  @observable googleAuthUrl = process.env.AUTH_URL
  @observable isAuthenticated = false
  @observable isAuthenticating = false
  @observable token = null

  setAuthToken = (token) => {
    let tokenParam
    let currentUrl

    if (!token) {
      currentUrl = new URL(window.location.href)
      tokenParam = currentUrl.searchParams.get('token')
      token = tokenParam || localStorage.get('token')
    }

    if (token) {
      localStorage.set('token', token)
      this.token = token
    }

    // remove tokens from the url
    if (tokenParam) {
      currentUrl.searchParams.delete('token')
      window.history.replaceState(window.history.state, window.title, currentUrl.href)
    }

    return token
  }

  @action
  logout = () => {
    localStorage.remove('token')
    this.isAuthenticated = false
  }

  @action
  checkAuthToken = async () => {
    this.isAuthenticating = true
    // eslint-disable-next-line no-console
    const response = await graphqlClient.query({ query: GET_ME }).catch((err) => console.error(err))
    this.isAuthenticated = !!response
    if (this.isAuthenticated) {
      router.updateAccess(response.data.me)
    }
    this.isAuthenticating = false
    return response && response.data && response.data.me
  }
}

- Ayush 🙂