user5968867
user5968867

Reputation:

Angular SPA with multiple roles

I'm building a SPA with Angular 1.5 where users have multiple roles [admin, 'see everything', 'edit specific stuff', basic user]. I spent whole day googling, stumbling across ton of different solutions, most seem to be outdated. My backend is Slim application, using token authentication.

My questions: How do I implement displaying same ng-view differently for each role? Should backend return my role and token after I login? How do I check if user is logged in when he tries to change location? I assume just checking if token is stored on client wont be enough, since I have no way of knowing when if it's expired?

I'm really new to token based authentication, and to Angular. I have lack of understanding how front end should be handled in my case, and I have no idea where to start. I'd appreciate any answer, suggestion, tip that'll help me understand what and how things should be done. Thank you.

Upvotes: 3

Views: 2764

Answers (2)

Ritesh Jagga
Ritesh Jagga

Reputation: 1442

First of all, use UI-Router to manage your states i.e. urls.

Login Mechanism

Upon successful login, your backend must return an access token and but may or may not return role with it. But you can always send another request to get user data and roles and save them to your local storage. You can create a service e.g. LoggedInUser which gets user information and roles.

angular
  .module('myApp')
  .factory('LoggedInUser', [
    '$q',
    'AppConfig',
    'User',
    function ($q,
              AppConfig,
              User) {

      function getUser() {
        var defer = $q.defer();
        var userId = localStorage.getItem('$MyApp$userId');
        var user = null;

        var userStringified = localStorage.getItem('$MyApp$currentUser');
        try {
          user = JSON.parse(userStringified);
        } catch (error) {
          defer.reject(error);
        }

        if (user) {
          defer.resolve(user);
        } else {
          User.findById({ // This is a server query
            id: userId,
            filter: {
              include: 'roles'
            }
          })
            .$promise
            .then(function (userData) {
              localStorage.setItem('$MyApp$currentUser', JSON.stringify(userData));
              defer.resolve(userData);
            }, function (error) {
              defer.reject(error);
            });
        }

        return defer.promise;
      }

      function clearUser() {
          localStorage.removeItem('$MyApp$currentUser');
      }

      return {
        getUser: getUser,
        clearUser: clearUser
      }

    }]);

Token Expiry

You can use Angular Interceptors to intercept server responses having a particular status code or any custom field that you can add from server code e.g. code: 'TOKEN_EXPIRED' on the response object and then take some action like logging out the user or sending another request from interceptor to get another token in order to continue operation.

It depends upon how important your request is. In my case I can show a message, logout the user.

Multiple Roles

Handling multiple roles have different meaning for front end and back end.

1. Front End

In front end, restriction is placed at two levels

1.1. To entire view

You may want to prevent a state from being active based on certain condition like

  • Forgot password page should only be visible to NOT-logged in user and if any logged in user tries to access by typing url, it should redirect to their home or dashboard page.
  • Dashboard page should be visible to logged in user having any role.
  • List users page should be visible to logged in user having role of admin.

In order to have this restriction, you will need 2 data items, one is to check if state needs authentication and another to check allowed roles of that state. I'm using authenticate having true/false and allowedRoles having an array of allowed roles. You can then use them in $stateChangeStart event. Here is the code:

In some js file where you are configuring states:

$stateProvider
    .state('home', {
      abstract: true,
      url: '',
      templateUrl: 'app/home/home.html',
      controller: 'HomeController',
      resolve: {
        loggedInUser: [
          'LoggedInUser',
          '$window',
          '$q',
          'User',
          function (LoggedInUser,
                    $window,
                    $q,
                    User) {
            var defer = $q.defer();
            LoggedInUser.getUser()
              .then(function (user) {
                defer.resolve(user);
              }, function (error) {
                $window.alert('User is not authenticated.');
                defer.reject(error);
              });

            return defer.promise;
          }
        ]
      },
      data: {
        authenticate: true
      }
    })
    .state('home.dashboard', {
      url: '/dashboard',
      templateUrl: 'app/dashboard/dashboard.html',
      controller: 'DashboardController',
      data: {
        roles: ['admin', 'basic-user']
      }
    })
   .state('home.list-users', {
      url: '/list-users',
      templateUrl: 'app/users/list/list-users.html',
      controller: 'ListUsersController',
      data: {
        roles: ['admin']
      }
    })
    // More states

In app.js, run block

$rootScope.$on('$stateChangeStart', function (event, next, nextParams) {
    var userLoggedIn = User.isAuthenticated();
    /* If next state needs authentication and user is not logged in
     * then redirect to login page.
     */
    var authenticationRequired = (next.data && next.data.authenticate);
    if (authenticationRequired) {
      if (userLoggedIn) {
       // Check role of logged in user and allowed roles of state to see if state is protected
        var allowedRoles = next.data && next.data.roles;
        if (allowedRoles && allowedRoles.length > 0) {
          console.log('State access allowed to: ' + allowedRoles);
          LoggedInUser.getUser()
            .then(function (result) {
              var allowed = false;
              var user = result;
              var role = user.role;
              var allowed = (allowedRoles.indexOf(role) >= 0);
              console.log(role + ' can access ' + allowed);

              if (!allowed) {
                var isAdministrator = user.isAdministrator;
                if (isAdministrator) {
                  console.log('User is administrator: ' + isAdministrator);
                  role = 'administrator';
                  allowed = (allowedRoles.indexOf(role) >= 0);
                  console.log(role + ' can access ' + allowed);
                }
              }

              if (!allowed) {
                event.preventDefault(); //prevent current page from loading
                $state.go('home.dashboard');
                AppModalBox.show('danger', 'Not Authorized', 'You are not authorized to view this page.');
              }
            }, function (error) {
              console.log(error);
            });
        }
      } else {
        event.preventDefault(); // Prevent current page from loading
        $state.go('publicHome.auth.login');
      }
    } else {
      /* This code block handles publicly accessible page like login, forgot password etc.
       * Public states (pages) have redirectToLoggedInState data set to either true or false.
       * redirectToLoggedInState set to true/false means that public page cannot/can be accessed by the logged in user.
       *
       * If user is logged in and any public state having redirectToLoggedInState data set to true
       * then redirect to dashboard page and prevent opening page.
       */
      var redirectToLoggedInState = (next.data && next.data.redirectToLoggedInState);
      if (userLoggedIn && redirectToLoggedInState) {
        event.preventDefault(); //prevent current page from loading
        $state.go('home.dashboard');
      }
    }
  });

Data defined on super state is available on child states so you can group all your private states (which needs authentication) under one state (In the above example, it is home) and put data.authenticate on super most state.

1.2. To certain sections within a view

In order to have this restriction, you can use the same LoggedInUser service in your controller or use the resolve in the super most state loggedInUser. Attach roles to $scope and use ng-if to show and hide section based on roles.

Resolves defined on the super most state are accessible in all child states.

Here is the code:

angular.module('myApp')
  .controller('DashboardController', [
    '$scope',
    'loggedInUser',
    function ($scope,
              loggedInUser) {

      $scope.roles = loggedInUser.roles;

    }]);

2. Back End

You can get access token from localStorage, which you must have saved upon successful login, and pass it in every request. In back end, parse the request to retrieve access token, get user id for this access token, get roles of the user, and check if user having these roles is allowed to perform operation (like query) or not.

I am using NodeJS - ExpressJS where middlewares are used to handle such role based authentication.

Upvotes: 3

Nair Athul
Nair Athul

Reputation: 821

please check

http://arthur.gonigberg.com/2013/06/29/angularjs-role-based-auth/

use the resolve to make any custom functions to be checked in the route

$routeProvider.when('/app', { templateUrl: 'app.html', controller: 'AppController' resolve: { //... } });

you can find examples of using resolve here

http://odetocode.com/blogs/scott/archive/2014/05/20/using-resolve-in-angularjs-routes.aspx

Upvotes: 0

Related Questions