2

I have authentication set up in such a way that I want to prevent any route/state from loading until I know that the user is authorized to access the page. If they are, then the requested page should load, if not, then it should go to the login page.

My config function:

$stateProvider

    .state('login', {
        url: '/login',
        templateUrl : 'login.html',
        controller  : 'loginController',
        data: {
            authorizedRoles: [USER_ROLES.guest]
        }
    })

    .state('home', {
        url: '/',
        templateUrl : 'home.html',
        controller  : 'homeController',
        data: {
            authorizedRoles: [USER_ROLES.admin]
        }
    })

    .state('admin', {
        url: '/admin',
        templateUrl : 'admin.html',
        controller  : 'adminController',
        data: {
            authorizedRoles: [USER_ROLES.admin]
        }
    });

$urlRouterProvider.otherwise('/login');

$locationProvider.html5Mode(true);

My run function:

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

    event.preventDefault();

    function checkAuthorization() {
        if(!AuthService.isAuthorized(authRole)) {
            $state.go('login');
        } else {
            $state.go(next.name);
        }
    }

    if(AuthService.getRoleId() === undefined) {
        // We'll have to send request to server to see if user is logged in
        AuthService.checkLogin().then(function(response) {
            checkAuthorization();
        });
    } else {
        checkAuthorization();
    }
})

If I keep the event.preventDefault() in my run function, then the app will be stuck in a loop always going to the requested state. If I remove the event.preventDefault() statement then the app will load the view (which will be visible for a second) before realizing the user should not be allowed to view it (and then go to the correct state).

How can I solve this problem?

chipit24
  • 6,509
  • 7
  • 47
  • 67

2 Answers2

2

You should use resolve and make request to the server to see if user is logged in the resolve

https://github.com/angular-ui/ui-router/wiki#resolve

.state('whatever',{
...
 promiseObj:  function($http){
            // $http returns a promise for the url data
            return $http({method: 'GET', url: '/someUrl'}).$promise;
         },
...
}

OR

if you have make a call in the controller, make the call in resolve in state, in which your api should response with a 401 if user is not login in and redirect to the log in screen if you have an intercept service.

Valter
  • 2,859
  • 5
  • 30
  • 51
  • I have looked into this and it seems like the best solution, however, I have not heard of setting up an intercept. Can you add an example of how I can implement what you have explained? – chipit24 Jan 25 '15 at 20:38
  • @cornflakes24 take a look at this https://docs.angularjs.org/api/ng/service/$http#interceptors i have a switch in the responseError that check rejection.status and depending on the status it redirect to somewhere. 400 -> log in 401 -> log in all other to the error page. – Valter Jan 25 '15 at 21:19
  • @cornflakes24 If you using token authentication. Interceptors is a great way to add the token to every request to the server. – Valter Jan 25 '15 at 21:20
  • Thanks! It seems even with resolve set up, it starts executing code from the state's controller. I can get it so it redirects correctly to the login page using interceptors, but not after executing some code from the controller that I don't want executed :/ – chipit24 Jan 25 '15 at 23:22
  • @cornflakes24 on the call to the server you have to add .$promise so state waits to the the call to complete then the run the controller code – Valter Jan 25 '15 at 23:27
  • I had the following: `resolve: { user: function(AuthService) { return AuthService.getUser(); } }` Is this not correct? Where would I include `$promise`? – chipit24 Jan 25 '15 at 23:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/69573/discussion-between-123456789-and-cornflakes24). – Valter Jan 25 '15 at 23:43
0

There is detailed explanation how to do this kind of resolve/wait stuff in this Q & A with working plunker.

An extracted version of the Auth service is:

.factory('userService', function ($timeout, $q) {

    var user = undefined;

    return {
        // async way how to load user from Server API
        getAuthObject: function () {
            var deferred = $q.defer();

            // later we can use this quick way -
            // - once user is already loaded
            if (user) {
                return $q.when(user);
            }

            // server fake call, in action would be $http
            $timeout(function () {
                // server returned UN authenticated user
                user = {isAuthenticated: false };
                // here resolved after 500ms
                deferred.resolve(user)
            }, 500)

            return deferred.promise;
        },

        // sync, quick way how to check IS authenticated...
        isAuthenticated: function () {
            return user !== undefined
                && user.isAuthenticated;
        }
    };    
})

where the most important parts are

  1. var user = undefined; - "global" variable which is
    • containing user and can answer if he has rights
    • does not contain user (yet) and the answer is "not authorized" then
  2. returned service function: isAuthenticated

That should solve the issue. check more details here

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335