7

I'm trying to create an SPA where you have to be logged in to access almost everything. So naturally, the default screen you see is the login screen. However, after a user has logged in, no matter what the ui-sref is, ui-router redirects to the login page (even when the user is authenticated). Here is my ui-router code:

(function () {
'use strict';
angular
    .module('app', ['ui.router', 'satellizer'])
    .config(function ($stateProvider, $urlRouterProvider, $authProvider, $httpProvider, $provide) {
  
        $httpProvider.interceptors.push(['$q', '$injector', function($q, $injector){
            return {
                responseError: function (rejection) {
                    var $state = $injector.get('$state');
                    var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
                    angular.forEach(rejectionReasons, function (value, key) {
                        if (rejection.data.error === value) {
                            localStorage.removeItem('user');
                            $state.go('auth');
                        }
                    });

                    return $q.reject(rejection);
                },
                response: function(response) {
                    var authorization = response.headers('authorization');
                    if(authorization !== null) {
                        authorization = authorization.substr(7).trim();
                        //console.log(authorization);
                        var $auth = $injector.get('$auth');
                        $auth.setToken(authorization);
                    }
                    return response;
                }
            }
        }]);

        $authProvider.loginUrl = 'mingdaograder/api/authenticate';

        $stateProvider
            .state('users', {
                url: '/users',
                templateUrl: 'views/userView.html',
                controller: 'UserController as user'
            })
            .state('subjects', {
                url: '/users/:user_id/subjects',
                templateUrl: 'views/subjectsView.html',
                controller: 'SubjectsCtrl as subjectsCtrl'
            })
            .state('subject', {
                url: '/users/:user_id/subjects/:subject_id',
                templateUrl: 'views/subjectView.html',
                controller: 'SubjectCtrl as subjectCtrl'
            })
            .state('auth', {
                url: '/auth',
                templateUrl: 'views/authView.html',
                controller: 'AuthController as auth'
            });
            //.state('otherwise', {
            //    url: '*path',
            //    templateUrl: 'views/authView.html',
            //    controller: 'AuthController as auth'
            //});

            //$urlRouterProvider.otherwise('/auth');
            $urlRouterProvider.otherwise(function($injector, $location) {
                console.log("Could not find " + $location);
                $location.path('/auth');
            });
    })
    .run(function ($rootScope, $state, $log) {
        $rootScope.$on('$stateChangeStart', function (event, toState) {
                console.log(toState.name);
            var user = JSON.parse(localStorage.getItem('user'));
            if (user) {
                $rootScope.authenticated = true;
                $rootScope.currentUser = user;
            }
        }
        );
    }
);
})();

Anytime I try to use $state.go(any state name here) or even type the address into the address bar, I am always redirected to the auth state. On the console the message is "Could not find http://localhost/#/" for every single route. I can type in http://localhost/#/users/5/subjects and I get the same message.

Here is one of my controllers doing a redirect:

(function () {
    'use strict';

    angular
        .module('app')
        .controller('AuthController', AuthController);

    function AuthController($auth, $state, $http, $rootScope, $log) {
        var vm = this;

        vm.loginError = false;
        vm.loginErrorText;

        vm.login = function () {
            var credentials = {
                username: vm.username,
                password: vm.password
            };

            $auth.login(credentials).then(function () {
                return $http.get('api/authenticate/user');
            }, function (error) {
                vm.loginError = true;
                vm.loginErrorText = error.data.error;
            }).then(function (response) {
                var user = JSON.stringify(response.data.user);
                localStorage.setItem('user', user);
                $rootScope.authenticated = true;
                $rootScope.currentUser = response.data.user;

                //$log.info('From AuthCtrl: ' + $rootScope.currentUser.id);
                $state.go('subjects', {user_id:$rootScope.currentUser.id});
            });
        }
    }
})();

Any ideas what I'm doing wrong? Thanks a lot for your time.

Update: Ok, I haven't found a way to fix it but I think I may have found a possible cause. It seems to only happen for the routes with parameters. For example, if I go to the users state, whose path is /users, there is no redirect. However, if I go to the subjects state, whose path is /users/:user_id/subjects, it does redirect. It's like the Url matching service can't recognize that /users/5/subjects matches /users/:user_id/subjects, so redirects. Any ideas how to work around this?

John Slegers
  • 45,213
  • 22
  • 199
  • 169
Brent Parker
  • 709
  • 1
  • 5
  • 19
  • otherwise needs to be the *last* statement in the chain, since the first acceptable match will be selected. – Claies Aug 08 '15 at 16:38
  • did you use `ui-view` and where is `abstract` state ? – gaurav bhavsar Aug 08 '15 at 16:42
  • @guarav bhavsar Yes, I use ui-view in the HTML. Do I need an abstract state? I'm pretty new to angular and angular-ui-router. – Brent Parker Aug 08 '15 at 16:58
  • @BrentParker a lot of approaches for authorization use a parent state (like "auth") to enforce the login requirement. All other states which require login, should be a child of this state (eg. "auth.users" and "auth.subjects"). Typically, you might also make the parent state be an abstract state, b/c it's not a state that you can actually visit (this decision is up to you/your app). – Sunil D. Aug 08 '15 at 17:01
  • @Sunil D. I'm enforcing the authorization via the server using JWT. What I mean is, any information that is retrieved from the server is retrieved via $http.get('https://myserver/api/some/resource') with an Authorization header. If you don't pass the correct JWT, an error is returned from the server and no sensitive data is sent. In javascript, I've added an interceptor to $httpProviders that checks the response. If there's an error, you go back to the login page (auth state). Why use /auth/users instead of just /users when you have to be logged in for everything? – Brent Parker Aug 08 '15 at 17:09
  • @BrentParker one reason to is so your SPA can have states that don't require authentication (like "login" or "signup"). These states can be siblings to the "main" state that requires authentication. All child states of main can inherit the login requirement (and any `data` or `resolves` attached to the "main" state. UI-Router is super flexible, this is just one approach... I do this in conjunction w/the JWT and http interceptor. – Sunil D. Aug 08 '15 at 20:47
  • Correction: I use the JWT and UI Router's `$stateChangeStart` and `$stateChangeError` events (I use the interceptor too, but only to insert the JWT for server requests). In the state change start check for JWT and go to "login" if not. In the error handler, check for 401 "unauthorized" response, and go to "login". – Sunil D. Aug 08 '15 at 20:56

4 Answers4

9

I found I didn't have a '/' at the beginning of my initial state url. Every time I navigated to the state, the missing '/' seemed to push it into the stateProvider.otherwise.

state1: 'opportunity'
state1Url : '/opportunity/' <== added initial forward slash to make it work.

state2: 'opportunity.create'
state2Url : 'create/'
sarin
  • 5,227
  • 3
  • 34
  • 63
1

The first path to be recognised will be the selected as the current location. This means that the order of your route definitions is crucially important. In your case you only have a single catch-all otherwise route definition and since all routes match this then all routes are directed to your login page ignoring any other route definitions you may have, including all your stateProvider state definitions.

One way to fix this is to remove the urlRouterProvider route definition altogether and instead use the *path syntax provided by ui-router to create an alternative otherwise state (which must be defined last for the same reasons given above).

Therefore your code might look something like this:

$stateProvider
    .state('auth', {
        url: '/auth',
        templateUrl: 'views/authView.html',
        controller: 'AuthController as auth'
    })
    .state('users', {
        url: '/users',
        templateUrl: 'views/userView.html',
        controller: 'UserController as user'
    })
    .state('subjects', {
        url: '/users/:user_id/subjects',
        templateUrl: 'views/subjectsView.html',
        controller: 'SubjectsCtrl as subjectsCtrl'
    })
    .state('subject', {
        url: '/users/:user_id/subjects/:subject_id',
        templateUrl: 'views/subjectView.html',
        controller: 'SubjectCtrl as subjectCtrl'
    })
    .state("otherwise", {
        url: "*path",
        templateUrl: 'views/authView.html',
        controller: 'AuthController as auth'
    });
biofractal
  • 18,963
  • 12
  • 70
  • 116
  • This cannot be true. In one of my apps I declare the "otherwise" route first, and it works just fine. A couple of reasons for this: 1) Note that you're using two different objects ($stateProvider and $urlProvider), you're not adding to or removing from the state hierarchy when you register the "otherwise" route. 2) UI Router states can be registered in any order. This is so you can put state definitions in Angular modules. – Sunil D. Aug 08 '15 at 16:58
  • Sorry, I tried this and it didn't change anything. It's still redirecting exactly as it was before. Any other ideas? – Brent Parker Aug 08 '15 at 16:58
  • @biofractal - Tried adding the *path code, too. It is still redirecting everything but now instead of having /#/auth in the address bar it just has /#/ – Brent Parker Aug 08 '15 at 17:31
0

From experience, this is either due to the / missing at either the beginning or the end of the url route property definition.

Make sure for parent routes to add the initial forward slash to your routes.

 .state('checkers', {
                url: '/checkers/',
                templateUrl: 'checkers.html',
                controller: 'CheckersController',
                title: 'Checker',
            })
devakone
  • 104
  • 4
-1

(function () {
'use strict';
angular
    .module('app', ['ui.router', 'satellizer'])
    .config(function ($stateProvider, $urlRouterProvider, $authProvider, $httpProvider, $provide) {
  
        $httpProvider.interceptors.push(['$q', '$injector', function($q, $injector){
            return {
                responseError: function (rejection) {
                    var $state = $injector.get('$state');
                    var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
                    angular.forEach(rejectionReasons, function (value, key) {
                        if (rejection.data.error === value) {
                            localStorage.removeItem('user');
                            $state.go('auth');
                        }
                    });

                    return $q.reject(rejection);
                },
                response: function(response) {
                    var authorization = response.headers('authorization');
                    if(authorization !== null) {
                        authorization = authorization.substr(7).trim();
                        //console.log(authorization);
                        var $auth = $injector.get('$auth');
                        $auth.setToken(authorization);
                    }
                    return response;
                }
            }
        }]);

        $authProvider.loginUrl = 'mingdaograder/api/authenticate';

        $stateProvider
            .state('users', {
                url: '/users',
                templateUrl: 'views/userView.html',
                controller: 'UserController as user'
            })
            .state('subjects', {
                url: '/users/:user_id/subjects',
                templateUrl: 'views/subjectsView.html',
                controller: 'SubjectsCtrl as subjectsCtrl'
            })
            .state('subject', {
                url: '/users/:user_id/subjects/:subject_id',
                templateUrl: 'views/subjectView.html',
                controller: 'SubjectCtrl as subjectCtrl'
            })
            .state('auth', {
                url: '/auth',
                templateUrl: 'views/authView.html',
                controller: 'AuthController as auth'
            });
            //.state('otherwise', {
            //    url: '*path',
            //    templateUrl: 'views/authView.html',
            //    controller: 'AuthController as auth'
            //});

            //$urlRouterProvider.otherwise('/auth');
            $urlRouterProvider.otherwise(function($injector, $location) {
                console.log("Could not find " + $location);
                $location.path('/auth');
            });
    })
    .run(function ($rootScope, $state, $log) {
        $rootScope.$on('$stateChangeStart', function (event, toState) {
                console.log(toState.name);
            var user = JSON.parse(localStorage.getItem('user'));
            if (user) {
                $rootScope.authenticated = true;
                $rootScope.currentUser = user;
            }
        }
        );
    }
);
})();
  • 5
    Please provide some description of your code and how it addresses the poster's question rather than simply posting a giant block of code – Suever Apr 07 '16 at 02:24