Search code examples
javascriptangularjsangular-routingangular-servicesdeferred-rendering

Defer creation of controllers/services on app run angularjs


every time the route change, I check if is set a variable with the current logged user details, if this variable is not set, I simply redirect to the login page.

Now I'm trying to achieve some kind of "remember me" functionality, so every time the route changes, if the variable user doesn't exist instead of redirecting to the login page, I check the local storage for an "authtoken", if is set I call a check function passing the authtoken to the server, that returns the user and the app will works the same way as after the manual login (that returns the same user as the check function).

I'm pretty sure this is not the best way to do that.

If I reload the page, first thing I run the check function that sends the authtoken to the server and wait for a response, if the user exists that value is assigned to a variable in the rootscope.

I have different services that use the variable in the rootscope, for example

angular.module('myApp')
    .service('AdminService', function AdminService($rootScope, $http, StorageService) {
    var authtoken = StorageService.get('authtoken');
    var clientId = $rootScope.loggedUser.id;

and of course when the check function runs it waits for the response, but the service is being instantiated and $rootScope.loggedUser.id does not exists.

How can I tell the app to wait until the check function receive the response?

This is my code

....
}).run(function($rootScope, $location, AuthService, StorageService) {

var intended = $location.path();  

AuthService.check().success(function(data) {
    if(data.user) {
       $rootScope.loggedUser = data.user;
       $location.path(intended);
    } else $location.path('login');
});
$rootScope.$on('$routeChangeStart', function() {

  if($rootScope.loggedUser) {
  ....

For example if the user bookmarks the page "myapp.com/#/admin/users", I don't want to redirect to the login, if I have in local storage the authtoken, but this causes the controller to be instantiated, that uses the service, that needs the $rootScope.loggedUser.id that is not yet populated.

And I want to run the function check only when the page (re)loads (not every time the user change route).


Solution

  • I would advise to re-examine your design if you need a service call to check your auth token. Auth tokens typically have expiration time stored in them, so you could just check whether you are within the expiration period without calling the server. You are not compromising security, since auth tokens are signed by the server, and validated when you make server calls to do anything useful. So, under normal circumstances, no separate check call is needed.

    But, if you insist, this use case is best handled with the resolve property of the route. This means, however, that every route that cares about the user's logged-in state would have to have a resolve property defined. This does not mean that you have to call the service on each route change.

    Instead of using $rootScope.loggedUser to store the user, have it be cached by the AuthService and injected via the resolve parameter.

    So, you could do the following:

    $routeProvider
       .when("some/secure/route", {
              controller: "SecureCtrl",
              resolve: {
                 user: AuthService.checkUser
              }
            });
    

    And in your AuthService:

    ...
    checkUser: function(){
      var deferred = $q.defer();
    
      if (cachedUser){
         deferred.resolve(cachedUser);
      } else {
    
         AuthService.check().success(
            function(data){
               // cachedUseris defined at AuthService's scope
               cachedUser = data.user;
               deferred.resolve(data.user);
            });
      }
    
      return deferred.promise;
    }
    

    Then, in your controllers:

    .controller("SecureCtrl", function(user){
       $scope.userId = user.id;
    }