1

I have a Spring+AngularJS RestFul application with user identification capabilites.

As soon as the user is logged in I store this data in the following manner:

$window.sessionStorage.user

Because I want to have this sessionStorage in other tabs I have implemented in my main controller the solution proposed here: https://stackoverflow.com/a/32766809/1281500

Also in this controller I listen to $stateChangeStart events to check wether the user is logged in or not. If the user is not logged in, I redirect him to the corresponding login page.

My problem comes when I open a new tab in the explorer. As you'll see in the code below, I get the $window.sessionStorage.user variable from the old tab to the new tab like this:

else if (event.key == 'sessionStorage' && !$window.sessionStorage.length) {
        // another tab sent data <- get it
        var data = JSON.parse(event.newValue);
        for (var key in data) {
            $window.sessionStorage.setItem(key, data[key]);
        }

But the code of the $stateChangeStart is executed BEFORE the snippet above so the $window.sessionStorage.user is not available yet and the user is always redirected to the login page even when he's already logged in (at least in the original tab)

$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
        // if the user tries to access a restricted page an is not logged in, he is redirected to the login view
        var restrictedPage = $.inArray(toState.name, ['login', 'sign-up']) === -1;
        // "$window.sessionStorage.user" IS ALWAYS "UNDEFINED" just after a new tab is opened
        if (restrictedPage && $window.sessionStorage.user === undefined) {
            // Go to login page
        }
    });

The full controller code is below. How can I have user credentials available in the $stateChangeStart block?

(function() {
  'use strict';

  angular
    .module('app.core')
    .controller('CoreController', CoreController);

  CoreController.$inject = ['$q', '$scope', '$rootScope', '$state', '$location', '$cookies', 'LoginService', '$window'];

  function CoreController($q, $scope, $rootScope, $state, $location, $cookies, LoginService, $window) {
    var vm = this;

    var sessionStorage_transfer = function(event) {
      if(!event) { event = $window.event; } // ie suq
      if(!event.newValue) return;          // do nothing if no value to work with
      if (event.key == 'getSessionStorage') {
        // another tab asked for the sessionStorage -> send it
        $window.localStorage.setItem('sessionStorage', JSON.stringify(sessionStorage));
        // the other tab should now have it, so we're done with it.
        $window.localStorage.removeItem('sessionStorage'); // <- could do short timeout as well.
      } else if (event.key == 'sessionStorage' && !$window.sessionStorage.length) {
        // another tab sent data <- get it
        var data = JSON.parse(event.newValue);
        for (var key in data) {
            $window.sessionStorage.setItem(key, data[key]);
        }
      }
    };

    // listen for changes to localStorage
    if($window.addEventListener) {
        $window.addEventListener("storage", sessionStorage_transfer);
    } else {
        $window.attachEvent("onstorage", sessionStorage_transfer);
    };

    // ask other tabs for session storage (this is ONLY to trigger event)
    if (!$window.sessionStorage.length) {
        $window.localStorage.setItem('getSessionStorage', 'user');
        $window.localStorage.removeItem('getSessionStorage', 'user');
    };

    $rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
        // if the user tries to access a restricted page an is not logged in, he is redirected to the login view
        var restrictedPage = $.inArray(toState.name, ['login', 'sign-up']) === -1;
        if (restrictedPage && $window.sessionStorage.user === undefined) {
            // Go to login page
        }
    });

  }
})();

UPDATE

My application is stateless so it works in the following way:

  • The first time the user access to the application I check the username and password are correct (call to Spring Rest service) and at that point I generate a token for the user. This token and user data are both stored in the sessionStorage like $window.sessionStorage.authToken and $window.sessionStorage.authToken. I manage this from another AngularJS Controller (LoginController):
function login(valid) {
        if(!valid) return;

        LoginService.authenticate(vm.user)
        .then(
                function(user) {
                    var authToken = user.token;
                    $window.sessionStorage.authToken = authToken;
                    vm.authError = false;
                    if (vm.rememberMe) {
                        var now = new Date();
                        var expireDate = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
                        $cookies.put(AUTH_TOKEN_COOKIE, authToken, {'expires': expireDate});
                    }
                    LoginService.getUser()
                    .then(
                        function(user) {
                            $window.sessionStorage.user = user
                            $state.go('installations');
                        }
                    );
                },
                function(errAuthentication) {
                    vm.authError = true;
                    $window.sessionStorage.user = null;
                }
        );
    }
  • From now on I send this token with every request to the RESTful application and I have a filter in Spring that checks the token with the user credentials is correct. In case this token doesn't exist or is not valid anymore, Spring Security throws the corresponding Exception and I catch this Exception to redirect to the login page (I don't think this code is important for the current question but I can post it if necessary).

So basically my user only lives in the current sessionStorage object, I don't have a service to check if the user is logged in or not, I just check the variables I stored during the first log in process.

I hope this helps to clarify the process I little bit more.

Thank you very much.

Community
  • 1
  • 1
rocotocloc
  • 418
  • 6
  • 19

1 Answers1

0

It's really difficult to guarantee that your controller will run before other modules.

If you want to execute this check before everything else you need to do that on a run module.

angular
.module('myApp')
.run(runBlock)
.run(['mySessionChecker', '$rootScope', function (mySessionChecker, $rootScope ) {

  $rootScope.$on("$locationChangeStart", function () {
      //Service that checks if the user is logged or not
      mySessionChecker.checkSession().then( function (auth) {
       //Auth tells you if the user is logged in or not
       console.log('User is '+auth);
       doSomething();
      })
  });

}]);

function runBlock($log) {
    $log.log('Angular's running');
}

Run Module will bootstrap immediately after App Module.

Then, for instance, you can emit an event with $rootScope or using Postal.js and do something with that information.

I would create a Parent Controller which intercepts that event and I would extend the controllers where you need to do the check to understand if the user is logged or not.

Take a look at this Q/A.

Hope I've been helpful.

Community
  • 1
  • 1
AndreaM16
  • 3,917
  • 4
  • 35
  • 74
  • Thank you for your reply. Please check my UPDATE since I don't exactly get the solution proposed. – rocotocloc Sep 09 '16 at 11:13
  • I read the update, but I don't understand correctly what's your problem. If you just want to do these controls earlier you should do them inside a `run module`. You are probably not able to execute your snippet before `stateChangeStart` because you are doing these controls inside a controller, which is bootstrapped and executed after `run.config`. – AndreaM16 Sep 09 '16 at 11:41
  • I understand your point but I don't see how to implement this in my case. In your snippet above you have put inside the run block the check of the user but I cannot put mine there because I depend on another event (the one that takes the `sessionStorage` from the other tab) to make this verification. I am starting with AngularJS so could you detail a little bit more how to get the data transferred from the new opening tab BEFORE the `$rootScope.$on("$locationChangeStart")` event? – rocotocloc Sep 09 '16 at 13:32
  • Okay, If you need to to that check based on an event it is completely different. My answer is useful if you need to do that check everytime you change/reload a state. – AndreaM16 Sep 09 '16 at 14:52
  • My problem only comes when I open a new tab, because I want to keep user credentials in the new tab that are stored in the `sessionStorage` object. As my application is going to be an SPA maybe I'll just keep it like that and the user will have to logged in again if he goes to a new tab. At this moment I don't see any other method to share this data between different tabs (I don't like the idea of using cookies or localStorage for this purpose). Thanks again for your comments. – rocotocloc Sep 09 '16 at 15:09
  • 1
    Ahh, you mean that when you open a new tab it does not preserve `$localStorage`'s content? I fixed this kind of issue using `$cookies`. I first check `$localStorage` then, if it's empty, I check `$cookies`. Take a look at `Angular-JWT`. – AndreaM16 Sep 09 '16 at 15:12
  • No, I mean that maybe it's not good to use `localStorage` to store the user token because it's permanently saved. I already knew about ´Angular-JWT´ but I didn't use it because I have implemented the solution described in this project: https://github.com/philipsorst/angular-rest-springsecurity, which I think it's a similar approach but kind of handmade. I think I'll give a try to ´Angular-JWT´ since it seems to be more professional and will solve my current problem because it uses ´localStorage´. Thanks again! – rocotocloc Sep 09 '16 at 15:36