angular-ui / ui-router

The de-facto solution to flexible routing with nested views in AngularJS
http://ui-router.github.io/
MIT License
13.54k stars 3k forks source link

UI-Router state not changing as expected... #1818

Closed Drammy closed 8 years ago

Drammy commented 9 years ago

I have an AngularJS app that uses UI-Router and has Authentication & Authorisation.

When changing $state I check authentication and authorisation and on failure I intend to redirect or request login.

$rootScope.$on('$stateChangeStart', function (event, toState) {
    var requiresAuth = toState.data.requiresAuth;
    if (requiresAuth) {
        var authorizedRoles = toState.data.authorizedRoles;
        if (!AuthService.isAuthorized(authorizedRoles)) {
            event.preventDefault();
            if (AuthService.isAuthenticated()) {
                // user is not allowed
                notifyError({ status: "Denied", statusText: "You don't have permission" });
            } else {
                // user is not logged in
                $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
            }
            $rootScope.$state.go('app.dashboard');
        }
    }
});

My problem is that when on state 'app.dashboard' and attempting to navigate to a state on which I don't have permission or am not authenticated I see the state change (breadcrumbs change on view) but then the $stateChangeStart code above kicks in and should send the state back to 'app.dashboard' and it does... I can debug and see the state is indeed changed back to app.dashboard but because the state already was 'app.dashboard' a reload doesn't take place therefore my navigation ui-sref-active directives don't update and the view (breadcrumbs) doesn't update.

<li data-ui-sref-active="active">
    <a data-ui-sref=" app.dashboard" title="Dashboard">
    <i class="fa fa-lg fa-fw fa-home"></i> <span class="menu-item-parent"> Dashboard</span>
    </a>
</li>

This all makes sense because AngularS is designed to prevent page reloads however I'm not sure how best to code around the problem. I've taken advice and code from this answer and it works but I'm a little concerned about issues this could introduce as I further develop the application.

.config(function($provide) {
    $provide.decorator('$state', function($delegate)
    {
        $delegate.go = function(to, params, options)
        {
            return $delegate.transitionTo(to, params, angular.extend(
            {
                reload: true,
                inherit: true,
                relative: $delegate.$current
            }, options));
        };
        return $delegate;
    });

Ideally, I'd prefer to achieve what I want without changing the behaviour of UI-Router's $state provider. Has anyone got any ideas?

acollard commented 9 years ago

@Drammy I recommend using a resolve to handle your auth. You won't need to edit any ui-router code or have any code in the events.

Here is a simple example.

$stateProvider.state({ 
    name: 'home', 
    url: '/home', 
    resolve: {
      loginRequired: ['$q','$state','$timeout', function($q, $state,$timeout){
       if(!isLoggedIn) // Or call some service
        {
          $timeout(function(){$state.go('login');});
          return $q.reject('User not logged in. Redirecting to login page.');
        }
        // Just let it pass through if we are logged in
      }]
    }});

The timeout is required because of this issue #1737.

Drammy commented 9 years ago

Thanks Andrew, I'm using a modal login so no redirection. Would you expect that to be problematic?

acollard commented 9 years ago

@Drammy The resolve will get called when you first start transitioning to the state. The transition will wait for any promises that are returned from these resolves before it completes the transition. I assume you want to wait for the user to login before they enter the state.

So in your case you may want to first check if the user is logged in then display your modal login dialog, then on complete of the modal dialog you could continue.

Something like this:

if(!isLoggedIn) // Or call some service
  {
    return authService.login() // This displays your login dialog
     .then(function(loginResult){
     if(!loginResult)
     {
       $timeout(function(){$state.go('accessDenied');});
       return $q.reject('User is not allowed here.');
     }
    });
  }
Drammy commented 9 years ago

I didn't appreciate the transition would wait for the promises to resolve.

Yes, in a nutshell this is what I'm wanting to do.

Thanks

eddiemonge commented 8 years ago

Closing due to inactivity