How to cancel the StateChangeStart event until ui-router solution is executed

I have an angular application that uses authorization logic and ui-router to ban unauthorized users for certain states / views. I follow standard approaches for listening to the stateChange event, which fires my authorization logic. All this works well until the scary page reloads.

I store session data (including authorization status) in local storage, so when I reload the page, I can use the parent state in ui-router to first allow / get authorization status from local storage before trying to change the views. Here is the configuration of my parent state app :

 $stateProvider. state('app', { url: '/app', abstract: true, controller: 'appCtrl', data: { authorizedRoles: [USER_ROLES.all] }, templateUrl: 'partials/app.html', resolve: { //Try to restore from the previous session before loading any of the app child states RestoredSession: ['SessionService', function(SessionService){ return SessionService.restoreSession(); }] } }) ...various app. child states 

And here is my onStateChange listener:

 //listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser //is authorized to view that page .run( ['$rootScope', 'AUTH_EVENTS', 'SessionService', function ($rootScope, AUTH_EVENTS, SessionService) { $rootScope.$on('$stateChangeStart', function (event, next) { var authorizedRoles = next.data.authorizedRoles; //If the requested page allows guest access, then continue to stateChange if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return; //If the requested page requires authorization, check login and auth privileges if (!SessionService.isAuthorized(authorizedRoles)) { event.preventDefault(); if (SessionService.existingSession()) { // user is not allowed $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); console.log("User attempted to access page for which he is not authorized"); } else { // user is not logged in $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn); console.log("User attempted to access page when he is not logged in"); } } }); }]); 

My problem is that the stateChangeStart event is stateChangeStart before app resolve , so the listener stops the state change (via event.preventDefault ) and then my solution loads the saved session data, which often establishes that the user has been logged in all the time. If I could demand that a decision be made before the event fires, then I would be gold.

Any ideas there

By the way, here is a similar SO question that went unanswered: Postpone angular UI Router $ stateChangeStart until you receive a response to server authorization

+6
source share
4 answers

It turns out that all I had to do was move the loading of the configuration data to the .run() block instead of trying to do this in the parent app resolve state.

 //listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser //is authorized to view that page .run( ['$rootScope', 'AUTH_EVENTS','SessionService', 'localStorageService', function ($rootScope, AUTH_EVENTS, SessionService, localStorageService) { $rootScope.$on('$stateChangeStart', function (event, next) { //function to check to see if the currentUser has one of the required roles to authorize the next state. var checkAuthorization = function(authorizedRoles){ //If the requested page allows guest access, then continue to stateChange if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return; //If the requested page requires authorization, check login and auth privileges if (!SessionService.isAuthorized(authorizedRoles)) { event.preventDefault(); if (SessionService.existingSession()) { // user is not allowed $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); console.log("User attempted to access page for which he is not authorized"); } else { // user is not logged in $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn); console.log("User attempted to access page when he is not logged in"); } } }; //Before calling checkAuthorization(), test to see if the state change was triggered by a reload //If so, load config data before triggering the `checkAuthorization()` function. if (SessionService.freshLoad === true || typeof SessionService.freshLoad === 'undefined'){ SessionService.freshLoad = false; var storedUser = localStorageService.get('currentUser'); //If we have a stored user but no existing session, then we know that we have stored //user data to reload before the checkAuthorization() function. if (typeof storedUser !== "undefined" && storedUser !== null && !SessionService.existingSession()) { SessionService.restoreSession(); } } checkAuthorization(next.data.authorizedRoles); }); }]); 
+1
source

I found a good way to resolve data asynchronously during $stateChangeStart in another answer here . Here is the code:

 rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) { if (dataService.isInitialized()) { proceedAsUsual(); } else { event.preventDefault(); dataService.intialize().success(function () { $state.go(toState, toParams); }); } }); 

Then you can just remember that your data is already initialized in the service the way you like, for example:

 function dataService() { var initialized = false; return { initialize: initialize, isInitialized: isInitialized } function intialize() { return $http.get(...) .success(function(response) { initialized=true; }); } function isInitialized() { return initialized; } }; 
0
source

Arriving a little late, but I think it will help.

The $ on method returns a deregistration function for the listener. This allows you to cancel the event before user processing in the listener.

 var setInterceptedListener = function($scope) { var removeListener = $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { // cancel state change event.preventDefault(); // mock prompt for user input Prompt.continue('Continue?').then(function(result) { // if yes then deregister the listener in order to proceed. if (result == 'yes') { removeListener(); $state.go(toState, toParams); } }); }); // deregister on scope teardown $scope.$on("$destroy", removeListener); }; 

To use this, simply add this method to the service and call setInterceptedListener ($ scope).

0
source

This is client-side protection that can be implemented in regular versions of Angular. I have tried and tested this. (Please find my article here: - http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security ). In addition to client-side security, you also need to provide server-side access. Client-side protection helps avoid an extra round trip to the server. However, if someone is tricking the browser, then server-side security on the server should be able to reject unauthorized access.

Hope this helps!

Step 1: Defining global variables in the app-module

-define roles for the application

  var roles = { superUser: 0, admin: 1, user: 2 }; 

-Specified route for unauthorized access for the application

  var routeForUnauthorizedAccess = '/SomeAngularRouteForUnauthorizedAccess'; 

Step 2: Define a service for authorization

 appModule.factory('authorizationService', function ($resource, $q, $rootScope, $location) { return { // We would cache the permission for the session, to avoid roundtrip to server for subsequent requests permissionModel: { permission: {}, isPermissionLoaded: false }, permissionCheck: function (roleCollection) { // we will return a promise . var deferred = $q.defer(); //this is just to keep a pointer to parent scope from within promise scope. var parentPointer = this; //Checking if permisison object(list of roles for logged in user) is already filled from service if (this.permissionModel.isPermissionLoaded) { //Check if the current user has required role to access the route this.getPermission(this.permissionModel, roleCollection, deferred); } else { //if permission is not obtained yet, we will get it from server. // 'api/permissionService' is the path of server web service , used for this example. $resource('/api/permissionService').get().$promise.then(function (response) { //when server service responds then we will fill the permission object parentPointer.permissionModel.permission = response; //Indicator is set to true that permission object is filled and can be re-used for subsequent route request for the session of the user parentPointer.permissionModel.isPermissionLoaded = true; //Check if the current user has required role to access the route parentPointer.getPermission(parentPointer.permissionModel, roleCollection, deferred); } ); } return deferred.promise; }, //Method to check if the current user has required role to access the route //'permissionModel' has permission information obtained from server for current user //'roleCollection' is the list of roles which are authorized to access route //'deferred' is the object through which we shall resolve promise getPermission: function (permissionModel, roleCollection, deferred) { var ifPermissionPassed = false; angular.forEach(roleCollection, function (role) { switch (role) { case roles.superUser: if (permissionModel.permission.isSuperUser) { ifPermissionPassed = true; } break; case roles.admin: if (permissionModel.permission.isAdministrator) { ifPermissionPassed = true; } break; case roles.user: if (permissionModel.permission.isUser) { ifPermissionPassed = true; } break; default: ifPermissionPassed = false; } }); if (!ifPermissionPassed) { //If user does not have required access, we will route the user to unauthorized access page $location.path(routeForUnauthorizedAccess); //As there could be some delay when location change event happens, we will keep a watch on $locationChangeSuccess event // and would resolve promise when this event occurs. $rootScope.$on('$locationChangeSuccess', function (next, current) { deferred.resolve(); }); } else { deferred.resolve(); } } }; }); 

Step 3: Using security for routing: Let's use all our hard word so far to provide routes

 var appModule = angular.module("appModule", ['ngRoute', 'ngResource']) .config(function ($routeProvider, $locationProvider) { $routeProvider .when('/superUserSpecificRoute', { templateUrl: '/templates/superUser.html',//path of the view/template of route caseInsensitiveMatch: true, controller: 'superUserController',//angular controller which would be used for the route resolve: {//Here we would use all the hardwork we have done above and make call to the authorization Service //resolve is a great feature in angular, which ensures that a route controller(in this case superUserController ) is invoked for a route only after the promises mentioned under it are resolved. permission: function(authorizationService, $route) { return authorizationService.permissionCheck([roles.superUser]); }, } }) .when('/userSpecificRoute', { templateUrl: '/templates/user.html', caseInsensitiveMatch: true, controller: 'userController', resolve: { permission: function (authorizationService, $route) { return authorizationService.permissionCheck([roles.user]); }, } }) .when('/adminSpecificRoute', { templateUrl: '/templates/admin.html', caseInsensitiveMatch: true, controller: 'adminController', resolve: { permission: function(authorizationService, $route) { return authorizationService.permissionCheck([roles.admin]); }, } }) .when('/adminSuperUserSpecificRoute', { templateUrl: '/templates/adminSuperUser.html', caseInsensitiveMatch: true, controller: 'adminSuperUserController', resolve: { permission: function(authorizationService, $route) { return authorizationService.permissionCheck([roles.admin,roles.superUser]); }, } }) }); 
-1
source

Source: https://habr.com/ru/post/975166/


All Articles