Removing Views (Navigation Menu Items) Dynamically

Posted by jts-law on 26-Jul-2017 13:28

All,

I'm using KUIB 2 and within the app I'm building I need to dynamically remove specific Views based on a users level of access.  The access level is already being handled, and I have working code that runs from the onShow method that removes the View menu items the user doesn't have access to.  My issue is that when I remove them from within the onShow method of controller.public.js, the menu items are visible for a moment, then disappear, and if I try to remove them within the router-events.js file, they don't exist yet.  I don't want the user to know that the other Views exist.

What is the recommended method of allowing/disallowing access to different Views?

TIA

Louis

Posted by Shelley Chase on 02-Aug-2017 06:02

The following approach handles only hiding ui links to certain views and protect them from direct loading (based on users roles):

All custom code for the role based support needs to be put in a custom extension module – we have such place in our app – scripts/extensions/index.js For example:

// Import your custom modules here:

import customModule from './customModule';

export default angular.module('app.extensions.module', [

   customModule

]).name;

In our customModule in order to protect the view we need to handle the angular $stateChangeStart and to prevent (or navigate) if the user do not have permissions to see this view:

export default angular.module('app.customModule', [

])

.run(['$rootScope', '$state', ($rootScope, $state) => {

   $rootScope.$on('$stateChangeStart', function(event, toState) {

// Here based on the info from the state we can prohibit loading some of the view. For example:

       if (toState.name === 'module.default.moduleTwo.grid') {

           event.preventDefault();

           $state.go('unauthorized');

       }

   });

In order to create this unauthorized state we need to use this code:

export default angular.module('app.customModule', [

])

.config(['$stateProvider', ($stateProvider) => {

   $stateProvider

       .state('unauthorized', {

           url: '/unauthorized',

           template: '<h1>Not Authorized</h1>'

       });

Then to not load any ui elements wich points to this view when users with no permission is logger we need to use following code:

export default angular.module('app.customModule', [

])

.config(['$provide', function($provide) {

   $provide.decorator('uiSrefDirective', ['$delegate', function ($delegate) {

       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) {

                   alert('Could not find state:', attrs.uiSref);

               } else {

                   var Session = injector.get('Session'); // The Session is a service which checks for user permission for a view

                   // If the user lacks sufficient permissions, hide this state from them.

                   if (!Session.userHasPermission(state)) {

                        element.remove();

                   }

               }

               // Otherwise pass through and let uiSref handle the rest

               directive.link.apply(this, arguments);

           };

       };

       return $delegate;

   }]);

All Replies

Posted by egarcia on 27-Jul-2017 17:28

Hello Louis,

I received feedback from the developers of this functionality.

They provided the following information:

We don't support that officially yet, however, level access to modules/views and even grid columns is high on our backlog and we will look into it very soon.

Currently there is an unofficial way to replace the navigation panel through the extensibility mechanism of our AngularJS apps.

You can define your own custom AngularJS module to achieve that in app/src/scripts/extensions/index.js

The code would look like the following:

var myModule = angular.module('myCustomModule', []);
myModule.run(['$state', function($state) {
    var state = $state.get('module.default');
    if (state && state.views && state.views['side-navigation']) {
		state.views['side-navigation'].template = '<p>Customized Menu</p>';
    }
}]);

export default angular.module('app.extensions.module', [
	'myCustomModule'
]).name;

You can take a look at app/src/scripts/common/side-navigation/index.html to see the actual text of the side-navigation so that you keep the appropriate style.

Have in mind that this will only replace the navigation panel. You can define different templates and use the right one depending on the user access. If you know the url you will be able to navigate to that view. We are still considering the approach – whether we will need to remove the views from routing (which will have implications on what happens when you login/logout) or we will do the check on view loading showing you “you don’t have access for this view”. And of course how we will get the user access level is still unknown.

Could you please give us more details on your business case? We are interested in how you determine the user access level and how you would like to restrict based on that level.

Thank you and regards.

Posted by jts-law on 28-Jul-2017 16:16

Edsel,

Thanks for the update.  I started working on this today and am still working through the best way to implement in our app.

In our app, I have implemented rules stored in the DB.  In the current code (that needs to change to your new methodology), I invoke a function to return the rules for the menus.  The menu items (Views) that a user has access to is based on their role within the app.  Admins have all menu items, standard Users have a very limited number of menu items, and I envision that a Support role could have a different set of menu items within this app.  The idea is to control the access using the rules within the DB so we should be able to change any access level without touching the actual application.

What would be nice is to have an event that fires when each menu item is processed to determine if it should be available.  The event could determine if the menu item is fully accessible, view only (on the side navigation but disabled, no link), or not on the menu at all.  Along with this, either the same event or a separate event should be available to determine if the requested View is accessible.  This is necessary because if a user doesn't have access to a specific View, they shouldn't be able to go directly to it via the URL.

I'll post an update when I figure out the best way to implement your workaround (within our app).

Louis

Posted by jts-law on 01-Aug-2017 11:36

Edsel,

I decided to put the logic that determines which menu items a user has access to on the backend service.  My custom module that generates the menu invokes a function to get the menus and then generates the HTML from the returned records and sets the state.views['side-navigation'].template to the generated HTML.  This is working fine now.

Regarding the individual view validation, do you have any recommendations on the best place to add logic to validate access to each view before loading/displaying?  I am currently calling my verifyPageAccess() method from the page controller constructor and my issue is that the page partially loads before being redirected.  Making the call from within the router-events.js onInit function results in the same results.  Ideally the user shouldn't see anything that they don't have access to.

Louis

Posted by Shelley Chase on 02-Aug-2017 06:02

The following approach handles only hiding ui links to certain views and protect them from direct loading (based on users roles):

All custom code for the role based support needs to be put in a custom extension module – we have such place in our app – scripts/extensions/index.js For example:

// Import your custom modules here:

import customModule from './customModule';

export default angular.module('app.extensions.module', [

   customModule

]).name;

In our customModule in order to protect the view we need to handle the angular $stateChangeStart and to prevent (or navigate) if the user do not have permissions to see this view:

export default angular.module('app.customModule', [

])

.run(['$rootScope', '$state', ($rootScope, $state) => {

   $rootScope.$on('$stateChangeStart', function(event, toState) {

// Here based on the info from the state we can prohibit loading some of the view. For example:

       if (toState.name === 'module.default.moduleTwo.grid') {

           event.preventDefault();

           $state.go('unauthorized');

       }

   });

In order to create this unauthorized state we need to use this code:

export default angular.module('app.customModule', [

])

.config(['$stateProvider', ($stateProvider) => {

   $stateProvider

       .state('unauthorized', {

           url: '/unauthorized',

           template: '<h1>Not Authorized</h1>'

       });

Then to not load any ui elements wich points to this view when users with no permission is logger we need to use following code:

export default angular.module('app.customModule', [

])

.config(['$provide', function($provide) {

   $provide.decorator('uiSrefDirective', ['$delegate', function ($delegate) {

       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) {

                   alert('Could not find state:', attrs.uiSref);

               } else {

                   var Session = injector.get('Session'); // The Session is a service which checks for user permission for a view

                   // If the user lacks sufficient permissions, hide this state from them.

                   if (!Session.userHasPermission(state)) {

                        element.remove();

                   }

               }

               // Otherwise pass through and let uiSref handle the rest

               directive.link.apply(this, arguments);

           };

       };

       return $delegate;

   }]);

Posted by jts-law on 10-Aug-2017 14:52

Thanks Shelley.  This worked great.  I didn't end up creating my own state, for what I'm doing I'm sending the user to the home page via $state.go('default.module.application.home').

Louis

Posted by jts-law on 14-Aug-2017 08:52

Shelley,

I changed my mind and am going to try to create my own 'Unauthorized' state.  What isn't clear is where to put the new code.  I have everything down to the $state.go() which works for loading the application home, I'm just not sure where to add the new custom module code.  I'm guessing that I'll need a couple new files since your example has 2 separate .config() statements?

Louis

This thread is closed