Tuesday, February 26, 2019

javascript - Resolve $http request before running app and switching to a route or state



I have written an app where I need to retrieve the currently logged in user's info when the application runs, before routing is handled. I use ui-router to support multiple/nested views and provide richer, stateful routing.




When a user logs in, they may store a cookie representing their auth token. I include that token with a call to a service to retrieve the user's info, which includes what groups they belong to. The resulting identity is then set in a service, where it can be retrieved and used in the rest of the application. More importantly, the router will use that identity to make sure they are logged in and belong to the appropriate group before transitioning them to the requested state.



I have code something like this:



app
.config(['$stateProvider', function($stateProvider) {

// two states; one is the protected main content, the other is the sign-in screen

$stateProvider

.state('main', {
url: '/',
data: {
roles: ['Customer', 'Staff', 'Admin']
},
views: {} // omitted
})
.state('account.signin', {
url: '/signin',
views: {} // omitted

});
}])
.run(['$rootScope', '$state', '$http', 'authority', 'principal', function($rootScope, $state, $http, authority, principal) {

$rootScope.$on('$stateChangeStart', function (event, toState) { // listen for when trying to transition states...
var isAuthenticated = principal.isAuthenticated(); // check if the user is logged in

if (!toState.data.roles || toState.data.roles.length == 0) return; // short circuit if the state has no role restrictions

if (!principal.isInAnyRole(toState.data.roles)) { // checks to see what roles the principal is a member of

event.preventDefault(); // role check failed, so...
if (isAuthenticated) $state.go('account.accessdenied'); // tell them they are accessing restricted feature
else $state.go('account.signin'); // or they simply aren't logged in yet
}
});

$http.get('/svc/account/identity') // now, looks up the current principal
.success(function(data) {
authority.authorize(data); // and then stores the principal in the service (which can be injected by requiring "principal" dependency, seen above)
}); // this does its job, but I need it to finish before responding to any routes/states

}]);


It all works as expected if I log in, navigate around, log out, etc. The issue is that if I refresh or drop on a URL while I am logged in, I get sent to the signin screen because the identity service call has not finished before the state changes. After that call completes, though, I could feasibly continue working as expected if there is a link or something to- for example- the main state, so I'm almost there.



I am aware that you can make states wait to resolve parameters before transitioning, but I'm not sure how to proceed.


Answer



OK, after much hair pulling, here is what I figured out.





  1. As you might expect, resolve is the appropriate place to initiate any async calls and ensure they complete before the state is transitioned to.

  2. You will want to make an abstract parent state for all states that ensures your resolve takes place, that way if someone refreshes the browser, your async resolution still happens and your authentication works properly. You can use the parent property on a state to make another state that would otherwise not be inherited by naming/dot notation. This helps in preventing your state names from becoming unmanageable.

  3. While you can inject whatever services you need into your resolve, you can't access the toState or toStateParams of the state it is trying to transition to. However, the $stateChangeStart event will happen before your resolve is resolved. So, you can copy toState and toStateParams from the event args to your $rootScope, and inject $rootScope into your resolve function. Now you can access the state and params it is trying to transition to.

  4. Once you have resolved your resource(s), you can use promises to do your authorization check, and if it fails, use $state.go() to send them to the login page, or do whatever you need to do. There is a caveat to that, of course.

  5. Once resolve is done in the parent state, ui-router won't resolve it again. That means your security check won't occur! Argh! The solution to this is to have a two-part check. Once in resolve as we've already discussed. The second time is in the $stateChangeStart event. The key here is to check and see if the resource(s) are resolved. If they are, do the same security check you did in resolve but in the event. if the resource(s) are not resolved, then the check in resolve will pick it up. To pull this off, you need to manage your resources within a service so you can appropriately manage state.



Some other misc. notes:





  • Don't bother trying to cram all of the authz logic into $stateChangeStart. While you can prevent the event and do your async resolution (which effectively stops the change until you are ready), and then try and resume the state change in your promise success handler, there are some issues preventing that from working properly.

  • You can't change states in the current state's onEnter method.



This plunk is a working example.


No comments:

Post a Comment

plot explanation - Why did Peaches' mom hang on the tree? - Movies & TV

In the middle of the movie Ice Age: Continental Drift Peaches' mom asked Peaches to go to sleep. Then, she hung on the tree. This parti...