Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save bvaughn/90343c06467e9bcb8d27 to your computer and use it in GitHub Desktop.
Save bvaughn/90343c06467e9bcb8d27 to your computer and use it in GitHub Desktop.
RE: Reddit Angular JS question "Advice on separating the logical parts of an application"

This gist is in response to a question asked on the Reddit Angular JS forum about how to structure an Angular app with roles and permissions.

There are many ways to approach this, but I'll share one that I've used before and maybe it will give you an idea. I'd combine all of the above into a single app and control who gets to see what using permissions. There are a few components to this approach...

A local session service

First off I'd advise creating some sort of local session management service. This should be able to track whether you have an authenticated user and- if so- what types of permissions that user has. (Lots of ways to manage permissions. I'll assume your user object has either a 'role' enum or something simple like an array of string permissions.) You could roll your own session service or you could check out something like satellizer.

Let's assume you're rolling your own. At a high level the interface could look something like this:

angular.module('myApp').service('Session', function() {
  this.authenticate = function(username, password) {
    // Use a service like $http or Restangular to verify your user's credentials.
    // If successful, store a user object in this.user and an oauth token (or similar) in a cookie.
  };

  this.isAuthenticated = function() {
    // This function should return a Promise.
    // Next, check for this.user first and if so, resolve the promise with that user object.
    // Otherwise check the cookie and if we have an oauth token, send an HTTP request to see if it's still valid.
    // If it's valid it should return our user object and then we can repeat the steps above.
    // Otherwise we should reject the promise.
  };

  this.logout = function(username, password) {
    // Remove the local this.user object and potentially send an HTTP request to destroy the session.
  };

  this.userHasPermission = function(permission) {
    // Your implementation depends on the structure of your User data but could look something like:
    return user && user.permissions.indexOf(permission) >= 0;
  };
});

Routing

Next up you'll need to setup routing for your application. For Angular routing I love ui-router. If you aren't already using this, I recommend you check it out. Using UI Router you can define various routes for things like you've mentioned- adding/editing categories, creating stores and products, etc. You can also nest routes in clever ways. For instance you could nest all of your states requiring an authenticated user within an 'authenticated' state. (I usually do this.)

Attribute states with required permissions

Continuing with the above example and assuming you're using UI Router, we can take advantage of the fact that UI router states are just objects (that we can attach additional, arbitrary data to). For your 'add category' state you may require the 'admin' role/permission. Here's an example of how you can tag a state with a permission:

$stateProvider.state('authenticated.someState', {
  url: '/some-url',
  templateUrl: 'views/some-state-view.html',
  controller: 'SomeStateController',
  permission: 'admin'
});

The above state, 'authenticated.someState', extends a base 'authenticated' state. UI Router allows your states to define a resolves object which specifies dependencies that must be loaded before your state can be entered. Assuming you choose to use a parent 'authenticated' state as well, I'd suggest you configure it to ensure that there's a current user, like so:

resolve: {
  currentUser: function(Session) {
    return Session.isAuthenticated();
  }
}

Now all of your authenticated states will only be accessible to authenticated users. And because of how UI Router manages nested states, the authentication check will only be run once (and then shared between subsequent child states as you navigate around).

Decorate uiSref to show/hide links based on permissions

Next I like to decorate uiSref (a UI router directive used to create links) to be aware of our permissions approach:

angular.module('myApp').config(function($provide) {
  $provide.decorator('uiSrefDirective',
    function ($delegate, $log) {
      var directive = $delegate[0];

      directive.compile = function() {
        return function(scope, element, attrs) {
          var stateName = attrs.uiSref.replace(/\(.+\)$/g, ''); // strip out the state params
          var injector = element.injector();
          var state = injector && injector.get('$state').get(stateName);

          // Watch for null (abstract) states and warn about them rather than erroring.
          if (!state) {
            $log.warn('Could not find state:', attrs.uiSref);

          } else if (state.permission) {
            var Session = injector.get('Session');

            // If the user lacks sufficient permissions, hide this state from them.
            if (!Session.userHasPermission(state.permission)) {
              element.remove();
            }
          }

          // Otherwise pass through and let uiSref handle the rest
          directive.link.apply(this, arguments);
        };
      };

      return $delegate;
    });
});

Using the above decorator, we can define a full navigation structure without having to worry about a lot of conditional logic to show/hide different groups of links. It will be handled automatically!

Secondary layer of state permission checks

We should also guard against manually-entered URLs to prevent someone from working around the lack of a direct link. We can do this using UI Router's $stateChangeStart event:

$rootScope.$on('$stateChangeStart', function(event, toState) {
  if (toState.permission && !Session.userHasPermission(toState.permission)) {
    event.preventDefault();
  }
});

With the above event handler, we don't have to clutter controllers with permissions checks. We can allow the global handler to enforce them.

Directive to help simplify conditional views

There may be times where you want to allow mor ethan one type of user to enter a state but show/hide parts of the state based on permissions. For this sort of thing, you could create a directive like:

angular.module('myApp').directive('ifPermission', function(PermissionChecker) {
  return {
    restrict: 'A',
    link: function($scope, $element, $attributes) {
      if (!Session.userHasPermission($attributes.ifPermission)) {
        $element.html('');
      }
    }
  }
});

You can use that directives to conditionally show parts of the UI like so:

<label>This thing is shown to everyone</label>

<!-- But only users with the 'delete' permission see the action below: -->
<button if-permission="delete" ng-click="deleteThing()">Delete the thing</button>

I hope this helps! Please let me know if anything is unclear.

@iamakimmer
Copy link

Thanks...I have the stateChangeStart event in my run method, but how about when the app is first loaded, I want to check if the user is already logged into the app, should I create a Session.init and make a call to api/isLoggedIn?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment