2

I'm writing an angularjs client for a token based restful API. The tokens in the API expire every hour so every time the token is expired in my client there should be a refresh token action.

The controller which handles the API call results looks like this:

angular.module('apitestApp')
  .controller('MainCtrl', ['$rootScope', '$scope', 'httpService', function ($rootScope, $scope, httpService) {
    $scope.messages = [];
    var url = $rootScope.domainPath + $rootScope.apiPath + 'messages.json';
    httpService.getRequest(url, {}).then(
      function (data){
          $scope.messages = data;
      }
    );
  }]);

I have a service that makes the API calls using angularjs $resource

angular.module('apitestApp')
  .service('httpService', ['$rootScope', '$resource', '$localStorage', function ($rootScope, $resource, $localStorage) {
    this.getRequest = function (url, params){
        var res = $resource(url, params, {
            query: {
                method: 'GET',
                isArray: true,
                headers: { 'Authorization': 'Bearer ' +  $localStorage.token.access_token }
            }
        });
        return res.query().$promise;
    };
    this.refreshToken = function (){
        var url = $rootScope.domainPath + this.authPath;
        var request = $resource(url);
        return request.get({
            client_id: this.clientId,
            client_secret: this.secret,
            grant_type: 'refresh_token',
            refresh_token: $localStorage.token.refresh_token
            },
            function (data){
                $localStorage.token = data;
            }
        ).$promise;
    };
  }]);

And finally an interceptor that handles all unauthorized requests (401), refresh the access token and retries the failed request.

angular.module('apitestApp')
 .factory('apiInterceptor', ['$q', '$injector', function ($q, $injector){
    //Handling error codes
    return {
        response : function (response){
            return response || $q.when(response);
        },
        responseError: function (rejection){
            switch(rejection.status){
                case 400:
                    console.log("Bad request");
                    break;
                case 401:
                    var config = rejection.config;
                    var deferred = $q.defer();
                    var httpService = $injector.get('httpService');
                    httpService.refreshToken().then(deferred.resolve, deferred.reject);
                    return deferred.promise.then(function (){
                        return httpService.getRequest(config.url, config.params);
                    });
                    //break;
                case 500:
                    console.log("Internal server error");
                    break;
                default:
                    console.log("Another error");
                    break;
            }
            return $q.reject(rejection);
        }
    };
  }]);

When the access token is valid, getRequest() method in my service successfully returns a promise, this is the same I want the interceptor to return but is not. In case the access token has expired the interceptor catches a 401 error, then updates the access token and finally makes the same request, the problem is that my controller doesn't get any response of it.

How can I perform a refresh token action and return the expected data on the behalf of the user? What am I doing wrong in the interceptor?

Jorge Zapata
  • 2,316
  • 1
  • 30
  • 57

1 Answers1

2

You're going to want to remove the $rootScope provider from the controller, that is not best practices for Angular as the controller has it's own scope inside of $rootScope. Services and Factories are okay to put on the $rootScope as it does not create it's own scope and that is where they will listen for their own events.

Also, it's best practice to put any asynchronous activity/HTTP calls into the services/factories. Just remember "skinny controllers, fat services".

Maybe try using an async handler that uses a sort of publish/subscribe design. Now, if it fails, it will call to update the stored value of messages once the getRequest function has completed async, triggering an update to the scope digest for any controller subscribed to the method:

Controller

angular.module('apitestApp')
  .controller('MainCtrl', ['$scope', 'httpService', function ($scope, httpService) {
    $scope.messages = [];
    httpService.setPath();
    httpService.onMessageReady($scope, function (messagesData) {
      $scope.messages = messagesData;
    });
  }]);

Service

angular.module('apitestApp')
  .service('httpService', ['$rootScope', '$resource', '$localStorage', function ($rootScope, $resource, $localStorage) {
    var self = this;
    this.messages = undefined;

    this.setPath = function () {
      self.getRequest($rootScope.domainPath + $rootScope.apiPath + 'messages.json', {});
    };

    this.getRequest = function (url, params) {
        var res = $resource(url, params, {
            query: {
                method: 'GET',
                isArray: true,
                headers: { 'Authorization': 'Bearer ' +  $localStorage.token.access_token }
            }
        });
        return res.query().$promise.then(function (data) {
          if (data) {
            self.messages = data;
            $rootScope.$broadcast('messagesReady');
          }
        });
    };

    this.refreshToken = function (){
        var url = $rootScope.domainPath + this.authPath;
        var request = $resource(url);
        return request.get({
            client_id: this.clientId,
            client_secret: this.secret,
            grant_type: 'refresh_token',
            refresh_token: $localStorage.token.refresh_token
            },
            function (data){
                $localStorage.token = data;
            }
        ).$promise;
    };

    this.onMessageReady = function (scope, callback) {
      callback(this.messages);
      scope.$on('messagesReady', function () {
        callback(this.messages);
      });
    };
  }]);
TommyMac
  • 299
  • 1
  • 6
  • thanks for your answer. You're definitely right about keeping thin controllers and keeping out rootScope from it. However I was expecting getRequest() to be kind of abstract method since there all of my API resource need authorization, that's why I was returning a promise. What is wrong about returning a promise in my interceptor? – Jorge Zapata May 18 '15 at 23:15
  • You have to return the promise from your interceptor in order to properly chain it back through the service. If all of your API requests need authorization, then do all of that authorization through the interceptor and dedicate this as the only module that depends on 'httpService'. Any request you have will route through your interceptor, so technically, this solution accomplishes what you are looking to do and is already pretty "abstract". _Side note, you might want some type of login as the token is refreshed every time without user interaction._ – TommyMac May 20 '15 at 19:04
  • A link to look at referring to Javascript abstract methods and classes: http://stackoverflow.com/questions/7477453/best-practices-for-abstract-functions-in-javascript – TommyMac May 20 '15 at 19:06