32

I have developed a Single Page App that uses a REST api. Users are required to login to access the application. When a user logs in they are redirected to /dashboard. On this URL / route, I would like to load a different template and controller based on the role of the user (e.g. normal user or admin user).

I have looked at https://github.com/angular-ui/ui-router/wiki under the templates section but none of the options support what I am trying to achieve.

  • By using templateUrl and function (stateParams) I am not able to inject the service that helps me to determine the user role so that I can load the template, e.g. views/user/dashboard.html or views/admin/dashboard.html
  • By using templateProvider I am to inject the service that helps me to determine the user role, but how do I then load the template?

Any solution should also load different controllers based on the user role, for example UserDashboardController or AdminDashboardController.

So essentialy what I need is a single route that loads a different template AND controller based on a user role variable that is set in a service when a user logs in.

Am I thinking along the right lines, or should I be implementing another solution?

Any help on this would be greatly appreciated.

user3596298
  • 321
  • 1
  • 3
  • 3
  • @MyTitle, Is your goal just to functionally separate user/admin tools? Are you concerned about security, functionality, both? Are you looking for admin screens to be a super-set of user screens (having admin links and tools like edit, delete, create), or do you want to create completely distinct user experiences? – Dave Alperovich Sep 04 '14 at 17:59
  • @DaveA yes, first option: ` looking for admin screens to be a super-set of user screens (having admin links and tools like edit, delete, create),`. I.e. no much different between regular user and admin screens – WelcomeTo Sep 04 '14 at 20:53
  • @MyTitle: You could try the first solution in my answer. It's the same idea about toggling functions on the page. In this case, you don't configure the rights, the rights are assumed to be hard-coded into each role (You could extend this in the future to make the rights configurable or add more roles). – Khanh TO Sep 07 '14 at 12:11

9 Answers9

24

Loading template and controller based on user role

While technically ui-router templateUrl function does not support injecting services you can use templateProvider to inject service that holds role variable or loads it asynchronously and then use $templateFactory to return HTML content. Consider following example:

var app = angular.module('app', ['ui.router']);

app.service('session', function($timeout, $q){
    this.role = null;

    this.loadRole = function(){
        //load role using axax request and return promise
    };
});

app.config(function($stateProvider, $urlRouterProvider){
    $stateProvider.state('dashboard', {
        url: '/dashboard',
        templateProvider: function(session, $stateParams, $templateFactory){
          return session.loadRole().then(function(role){
              if(session.role == 'admin'){
                return $templateFactory.fromUrl('/admin/dashboard.html', $stateParams);
              } else {
                return $templateFactory.fromUrl('/user/dashboard.html', $stateParams);
              }
          });
        }
      });

    $urlRouterProvider.otherwise('/dashboard');
});

As for controller you can either state that you would like to use specific controller inside root element of each template with ng-controller. Or similarly you can use controllerProvider option to inject service that will already have role resolved by templateProvider. Take a look at following example of controllerProvider option inside ui-router state definition:

controllerProvider: function(session){
  if(session.role == 'admin'){
    return 'AdminCtrl';
  } else {
    return 'UserCtrl';  
  }
}

Of course you can remove duplicates from this code easily and define a more accessible micro DSL to make defining different rules for particular roles and views easier.

The following demo should help you understand the code.

Is this a right approach?

As usually this greatly depends on context. To help you come up with an answer let me suggest following questions first:

  • How much views presented to roles differ?

Are you going to hide only couple of buttons and other action elements basically making a page read only for regular users and editable for superusers? If the changes will be small I would probably go with using the same views and only hiding particular elements, probably forging a directive similar to ng-if that would allow enabling/disabling particular functionality declaratively only-role='operator, admin'. On the other hand if views are going to be vastly different then employing different templates can simplify markup greatly.

  • How much actions available on particular page differ depending on role?

Do actions that look similar on surface differ in inner workings for different roles? In example if you have Edit action available both for user and admin role but in one case it starts wizard like UI and in other a complex form for advanced users then having a separate controller makes more sense. On the other hand if admin actions are a superset of user actions then having single controller seems easier to follow. Note that in both cases keeping controller things pays off - they should only glue views to behaviour that is encapsulated in services/view models/models/pick a name

  • Will you have many contextually separate links leading to particular page from different places of the app?

For instance being able to provide navigation to particular page by simply writing ui-sref="dashboard" regardless of the current user role may be beneficial if it exists in various contexts. If that's the case then having them defined under single route/state seems more maintainable then a conditional logic used to build different ui-sref/ng-href based on role. However you could also define routes/states dynamically based on user role - loaded dynamically or not

  • Will views and actions available to different roles on particular page evolve separately or together?

Sometimes we first build features for regular users then for premium and then for ultimate. It's not unusual to divide work on pages for user and admin between team members especially if clear boundaries can be drawn easily. In such case having separate views and controllers can simply developers work avoiding conflicts. Of course it's not all rainbows and unicorns - team must be very disciplined to remove duplication that most likely will happen.

Hope that my suggestions will help you decide.

miensol
  • 39,733
  • 7
  • 116
  • 112
15

Am I thinking along the right lines, or should I be implementing another solution?

IMO, You should not do it this way.

Here, I propose 2 other solutions depending on how your application is implemented.

1) If the rights of your roles can be configured (you could have a separate page to configure your roles, assign rights to your roles,...). Then use only 1 template and 1 controller for your roles (normal users, admin users, and more......) and use ng-show, ng-class,.. to display your HTML accordingly.

In this case, we don't care much whether the user is normal user or admin user, that's just the name of our role. What we do care about is the rights and it's dynamic => Therefore, we should display the html dynamically based on the configured rights (for sure, there are also checks on server side when users perform an action to prevent the user from crafting a malicious http request and posting to server). If we were to use separate templates for that, there are countless cases.

The point of this solution is that the functions of the page are the same to your roles, you just need to show/hide the functions of the page based on the user.

2) If the rights of the roles are fixed (cannot be configured) and the functionality of the views for normal users and admin users are different. It's better to use separate states for these views and authorize access to these views based on the logged-in user (for sure, there is also authorization on server side when users perform an action).

The reason is: the admin user view and normal user view have different functionality (which should be separated from each other)

Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • I guess half a bounty is better than no bounty. Should have been full. Then again, hard to satisfy somebody who doesn't know what they want. – Dave Alperovich Sep 08 '14 at 17:32
7

If you are using the a version of angular greater than 1.2 you can do a directive with a templateUrl as a function.

So the basic ideas is you have a dashboard view that has a custom directive on it that will determine the template based on the user level. So something like this:

(function () {
  'use strict';
  angular.module('App.Directives')
    .directive('appDashboard', ['UserManager', function (UserManager) {
      return {
        restrict: 'EA',
        templateUrl: function(ele, attr){
            if (UserManager.currentUser.isAdmin){
                return 'admin.html';
            }else{
                return 'user.html';
            }
        }
      };
    }]);
})(); 
Shawn C.
  • 6,446
  • 3
  • 34
  • 38
4

I. Do not use "...single route that loads a different template...", would be my suggestion, my answer.

If possible:

Try to step back and reconsider the entire desing and
Try to weaken the sense that our application users are interested in url.

They are not. And if they really do understand what is url, address bar... they use it to copy, send and paste... not to investigate its parts...

II. Suggestion: Enforce the usage of the ui-router states:

... UI-Router is organized around states, which may OPTIONALLY have routes, as well as other behavior, attached...

That means, let's reconsider our application as group/hierarchy of well defined states. They can have url defined , but do not have to (e.g. error state without url)

III. How can we profit from building our application arround the states?

Separation of concern - should be our aim.

The state is a unit which gathers some view/controllers, resolvers, custom data...

That means, that there could be more states reusing views, controllers, etc. Such states could really differ (same view, different controller). But they are single purpose - they are there to handle some scenarios:

  • administration of User/Emplyoee record
  • list of User/Employee - information ala PhoneList (just email, phone...)
  • Security administration - What are the rights of a User ...

And again, there could be many many states. Having even hundred states won't be peformance issue. These are just definitions, a set of references to other pieces, which should be used ... later... if really needed.

Once we've defined use cases, user stories on the level of the state, we can group them into sets/heirarchies.
These groups could be later presented to different user Roles in a different format (different menu items)

But at the end, we gained lot of freedom and simplified maintainablity

IV. Keep application running and growing

If there are few states, maintainanace does not seem to be an issue. But it could happen, that the applicaiton will succeed. Succeed and grow... inside of its design.

Spliting the sate definitions (as a unit of work) and their hierarchies (which user Role can access which states) would simplify its management.

Appling security outside of the states (Event listeners ala '$stateChangeStart') is much more easier, then never ending refactoring of template Providers. Also, the main part of security, should be still be applied on a server, regardless what UI allows

V. Summary:

While there is such a great feature as a templateProvider, which could do some interesting stuff for us (e.g. here: Changing Navigation Menu using UI-Router in AngularJs)...

... we should not use it for security. That could be implemented as some menu/hierarchy built from existing states, based on current Role. Event listeners should check if user is coming to granted state, but the main check must be applied on a server...

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • thanks. Its sounds good, but can you please provide any example? – WelcomeTo Sep 01 '14 at 06:43
  • Not sure if this design suggestion could be provide with a *"simple enough example"*... but will think about it later today... or later. The essential part of my view is: Define states as simple as possible. There could be lot of them. Once you create navigation for users - make it Role dependent (more navi settings per each Role). If needed, introduce some check on events... but the real seccurity apply on a server (Get data only if user has required Role). So, this is more a design/architetural principle, than simple use case answer... Will be glad if this could help even a bit... later ;) – Radim Köhler Sep 01 '14 at 06:48
  • I see a problem with approach offered by this answer. User opens the ww.someapp.com/ and gets redirected by angular to #!/ that assumes that user can be signed in or not signed in at this moment. Obviously, registered users does not need in seeing "marketing" home page, they prefer to be redirected effectively to dashboard when visit "/#!/" or "/" path. – Konstantin Isaev Mar 31 '15 at 14:56
3

I have employed the following solution (which might not be ideal, but it has worked for me in such kind of scenarios):

  1. Specify the controller in the template itself, using ngController.

  2. Load the template using a generic view name (e.g. views/dashboard.html).

  3. Change what views/dashboard.html refers to by using $templateCache.put(...) whenever the logged in user-role changes.


Here is a striiped down example of the approach:

app.controller('loginCtrl', function ($location, $scope, User) {
    ...
    $scope.loginAs = function (role) {
        // First set the user role
        User.setRole(role);

        // Then navigate to Dashboard
        $location.path('/dashboard');
    };
});

// A simplified `User` service that takes care of swapping templates,
// based on the role. ("User" is probably not the best name...)
app.service('User', function ($http, $templateCache) {
    var guestRole = 'guest';
    var facadeUrl = 'views/dashboard.html';
    var emptyTmpl = '';
    var errorTmpl = 'Failed to load template !';
    var tempTmpl  = 'Loading template...';

    ...

    // Upon logout, put an empty template into `$templateCache`
    this.logout = function () {
        this.role = guestRole;
        $templateCache.put(facadeUrl, emptyTmpl);
    };

    // When the role changes (e.g. upon login), set the role as well as the template
    // (remember that the template itself will specify the appropriate controller) 
    this.setRole = function (role) {
        this.role = role;

        // The actual template URL    
        var url = 'views/' + role + '/dashboard.html';

        // Put a temporary template into `$templateCache`
        $templateCache.put(facadeUrl, tempTmpl);

        // Fetch the actual template (from the `$templateCahce` if available)
        // and store it under the "generic" URL (`views/dashboard.html`)
        $http.get(url, {cache: $templateCache}).
              success(function (tmpl) {
                  $templateCache.put(facadeUrl, tmpl);
              }).
              error(function () {
                  // Handle errors...
                  $templateCache.put(facadeUrl, errorTmpl);
              });
    };

    // Initialize role and template        
    this.logout();
});

// When the user navigates to '/dashboard', load the `views/dashboard.html` template.
// In a real app, you should of course verify that the user is logged in etc...
// (Here I use `ngRoute` for simplicity, but you can use any routing module.)
app.config(function ($routeProvider) {
    $routeProvider.
        when('/dashboard', {
            templateUrl: 'views/dashboard.html'
        }).
        ...
});

See, also, this short demo.
(I use ngRoute for simplicity, but it doesn't make any difference since all work is done by the User service.)

gkalpak
  • 47,844
  • 8
  • 105
  • 118
3

You don't really need to do it with router.

The simplest thing is to use one single template for all roles and to use dynamic ng-include inside it. Suppose you have injector in $scope:

<div ng-include="injector.get('session').role+'_dashboard.html'"></div>

So you should have user_dashboard.html and admin_dashboard.html views. Inside each you can apply separate controller, for example user_dashboard.html:

<div id="user_dashboard" ng-controller="UserDashboardCtrl">
    User markup
</div>
gorpacrate
  • 5,109
  • 3
  • 21
  • 18
1

No need for a long explanation here.

Use resolve and change $route.$$route.templateUrl, or use routeChangeError by passing the new route or relevant parameter to the promise.

var md = angular.module('mymodule', ['ngRoute']);
md.config(function($routeProvider, $locationProvider) {
    $routeProvider.when('/common_route/:someparam', {
        resolve: {
            nextRoute: function($q, $route, userService) {
                defer = $q.defer()
                userService.currentRole(function(data) { defer.reject({nextRoute: 'user_based_route/'+data) });
                return defer.promise;
            }
        }
    });
    $rootScope.$on("$routeChangeError", function(evt, current, previous, rejection) {
      if (rejection.route) {
        return $location.path(rejection.route).replace();
      }
    });
});
Gepsens
  • 673
  • 4
  • 14
1

I know this has been awhile since this question was posted but I am adding my answer since I the method I use is different from the other answers here.

In this method, I am completely separating the route and template urls based on that user's role and redirecting the user to index page if they are in a route they are not authorized to view.

With UI Router, I basically add a data attribute like this to the state:

.state('admin', {
            url: "/admin",
            templateUrl: "views/admin.html",
            data: {  requireRole: 'admin' }
        })

When the user is authenticated, I store their role data into the localstorage and $rootscope from the controller like this:

var role = JSON.stringify(response.data); // response from api with role details

// Set the stringified user data into local storage
localStorage.setItem('role', role);

// Putting the user's role on $rootScope for access by other controllers
$rootScope.role = response.data;

Lastly, I use the $stateChangeStart to check the role and redirect the user if user is not supposed to view the page:

.run(['$rootScope', '$state', function($rootScope, $state) {

        // $stateChangeStart is fired whenever the state changes. We can use some parameters
        // such as toState to hook into details about the state as it is changing
        $rootScope.$on('$stateChangeStart', function(event, toState) {

                var role = JSON.parse(localStorage.getItem('role'));
                $rootScope.role = role;

                // Redirect user is NOT authenticated and accesing private pages
                var requireRole = toState.data !== undefined
                                  && toState.data.requireRole;

                 if( (requireRole == 'admin' && role != 'admin')) )
                 {
                   $state.go('index');
                   event.preventDefault();
                   return;
                 }
     }

});

Further to the above, you would still need to do server side authorization check before you display any data to the user.

Neel
  • 9,352
  • 23
  • 87
  • 128
0

There is an excellent project https://github.com/Narzerus/angular-permission it needs ui-router. The project is new nevertheless works well.

TJ_
  • 255
  • 3
  • 12