meteor Meteor + React + ReactRouter Add roles


Example

1- Add roles package ( https://github.com/alanning/meteor-roles)

meteor add alanning:roles

2- Create some roles constants. In file imports/api/accounts/roles.js

const ROLES = {
  ROLE1: 'ROLE1',
  ROLE2: 'ROLE2',
  ADMIN: 'ADMIN'
};

export default ROLES;

3- I'll not show how to add/update roles on a user, just will mention that on server side, you can set user roles by Roles.setUserRoles(user.id, roles); Check for more info in https://github.com/alanning/meteor-roles and http://alanning.github.io/meteor-roles/classes/Roles.html

4- Assuming you already setup all the accounts and roles files (see full example in https://github.com/rafa-lft/Meteor_React_Base. Look for tag Step4_roles) we can now create a method that will be in charge of allowing (or not) access to the different routes. In imports/startup/client/Routes.jsx


class Routes extends Component {
  constructor(props) {
    super(props);
  }

  authenticate(roles, nextState, replace) {
    if (!Meteor.loggingIn() && !Meteor.userId()) {
      replace({
        pathname: '/login',
        state: {nextPathname: nextState.location.pathname}
      });
      return;
    }
    if ('*' === roles) { // allow any logged user
      return;
    }
    let rolesArr = roles;
    if (!_.isArray(roles)) {
      rolesArr = [roles];
    }
    // rolesArr = _.union(rolesArr, [ROLES.ADMIN]);// so ADMIN has access to everything
    if (!Roles.userIsInRole(Meteor.userId(), rolesArr)) {
      replace({
        pathname: '/forbidden',
        state: {nextPathname: nextState.location.pathname}
      });
    }
  }

  render() {
    return (
      <Router history={ browserHistory }>
        <Route path="/" component={ App }>
          <IndexRoute name="index" component={ Index }/>
          <Route name="login" path="/login" component={ Login }/>
          <Route name="signup" path="/signup" component={ Signup }/>

          <Route name="users" path="/users" component={ Users }/>

          <Route name="editUser" path="/users/:userId" component={ EditUser }
                 onEnter={_.partial(this.authenticate, ROLES.ADMIN)} />


          {/* ********************
           Below links are there to show Roles authentication usage.
           Note that you can NOT hide them by
           { Meteor.user() && Roles.userIsInRole(Meteor.user(), ROLES.ROLE1) &&
           <Route name=.....
           }
           as doing so will change the Router component on render(), and ReactRouter will complain with:
           Warning: [react-router] You cannot change <Router routes>; it will be ignored

           Instead, you can/should hide them on the NavBar.jsx component... don't worry: if someone tries to access
           them, they will receive the Forbidden.jsx component
           *************/ }
          <Route name="forAnyOne" path="/for_any_one" component={ ForAnyone }/>

          <Route name="forLoggedOnes" path="/for_logged_ones" component={ ForLoggedOnes }
                 onEnter={_.partial(this.authenticate, '*')} />

          <Route name="forAnyRole" path="/for_any_role" component={ ForAnyRole }
                 onEnter={_.partial(this.authenticate, _.keys(ROLES))}/>

          <Route name="forRole1or2" path="/for_role_1_or_2" component={ ForRole1or2 }
                 onEnter={_.partial(this.authenticate, [ROLES.ROLE1, ROLES.ROLE2])} />

          <Route name="forRole1" path="/for_role1" component={ ForRole1 }
                 onEnter={_.partial(this.authenticate, ROLES.ROLE1)}/>

          <Route name="forRole2" path="/for_role2" component={ ForRole2 }
                 onEnter={_.partial(this.authenticate, ROLES.ROLE2)} />


          <Route name="forbidden" path="/forbidden" component={ Forbidden }/>

          <Route path="*" component={ NotFound }/>
        </Route>
      </Router>
    );
  }
}

We added an onEnter trigger to some routes. For those routes, we are also passing which Roles are allowed to enter. Note that the onEnter callback, receives 2 params originally. We are using underscore's partial (http://underscorejs.org/#partial), to add another one (roles) The authenticate method (called by onEnter) receives the roles and:

  • Check if the user is logged in at all. If not, redirects to '/login'.
  • If roles === '*' we assume any logged in user can enter, so we allow it
  • Else, we verify if the user is allowed (Roles.userIsInRole) and if not, we redirect to forbidden.
  • Optionally, you can uncomment a line, so ADMIN has access to everything.

The code has several examples of different routes that are allowed for anyone (no onEnter callback), for any logged user, for any logged user with at least 1 role, and for specific roles.

Also note, that ReactRouter (at least on version 3), doesn't allow to modificate the routes on Render. So you can not hide the routes within the Routes.jsx. For that reason, we redirects to /forbidden in the authenticate method.

5- A common bug with ReactRouter and Meteor, relates to user status updates not being shown. For example the user logged out, but we are still showing his/her name on the nav-bar. That happens because Meteor.user() has changed, but we are not re-rendering.

That bug can be solved by calling Meteor.user() in the createContainer. Here is an example of it, used in imports/ui/layouts/NavBar.jsx:

export default createContainer((/* {params}*/) =>{
  Meteor.user(); // so we render again in logout or if any change on our User (ie: new roles)
  const loading = !subscription.ready();
  return {subscriptions: [subscription], loading};
}, NavBar);

Note:

  • I'm skipping some other files that you will need, to make things shorter. Specifically, check imports/startup/server/index.js imports/ui/layouts/{App,NavBar}.jsx and import/ui/pages/{Login,Signup,Users,EditUser}.jsx

  • You can view a full example in https://github.com/rafa-lft/Meteor_React_Base. Look for tag Step4_roles