I'm quite new to Angular. I'm using token authentication in my Angular app. I use HTTP interceptor to check when my backend returns 401, and than do a login using a refresh token:
myapp.config(...)
...
$httpProvider.interceptors.push(['$q', '$injector', function($q, $injector) {
var sessionRecoverer = {
responseError: function(response) {
// Session has expired
if (response.status === 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
var $auth = $injector.get('$auth');
$auth.login({
refresh_token: ????,
grant_type: 'refresh_token',
event_client: 'client',
client_id: 'id'
});
// When the session recovered, make the same backend call again and chain the request
return deferred.promise.then(function() {
return $http(response.config);
});
}
return $q.reject(response);
}
};
return sessionRecoverer;
}]);
Now, the refresh_token comes from my login controller (which pulls it from the api backend). So the controller has to pass it somehow to the interceptor. The problem is that the interceptor is in the config block so there are no services, values, etc. - only providers. But the providers are not injectable into the controller. So is there a way to pass data from controller to app.config? If not, is there a workaround? Can the injector be anywhere else other than app.config?
Yes you can do it simply like this :
myapp.config(...)
...
$httpProvider.interceptors.push(['$q', '$injector', function($q, $injector, TokenFactory) {
var sessionRecoverer = {
responseError: function(response) {
// Session has expired
if (response.status === 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
var $auth = $injector.get('$auth');
$auth.login({
refresh_token: TokenFactory.getRefreshToken(),
grant_type: 'refresh_token',
event_client: 'client',
client_id: 'id'
});
// When the session recovered, make the same backend call again and chain the request
return deferred.promise.then(function() {
return $http(response.config);
});
}
return $q.reject(response);
}
};
return sessionRecoverer;
}]);
As you say, the block config can only inject providers, but the interceptor, itself, is a factory, thus you can inject other factories, for example, a factory called TokenFactory that should provide a method that return the refresh token when needed.
Edit
If the refresh_token is a thing that comes from the backend and you want to set a value within TokenFactory from your login controller, you can do something like this to implement your factory and your controller :
myapp.factory('TokenFactory',function(){
var currentRefreshToken;
return {
setRefreshToken: function(token){
currentRefreshToken = token;
},
getRefreshToken: function(){
return currentRefreshToken:
}
};
});
myapp.controller('MyLoginCtrl',function($scope,TokenFactory,$http){
$scope.login = function(){
$http.post('http://myapp.com/refreshtoken',$scope.credentials)
.then(TokenFactory.setRefreshToken)
.then(function(){ /* ... */})
.catch(function(err){ console.error(err) })
;
};
});
Data persistance
If you want to make your tokens to persist, you can write a factory that use the LocalStorage HTML5 API and use it in your TokenFactory:
myapp.factory('TokenFactory',function(LocalStorage){
// load the value from localstorage (hard disk) on app starts
var currentRefreshToken = LocalStorage.get('myapp.currentRefreshToken');
return {
setRefreshToken: function(token){
currentRefreshToken = token; // save value in RAM
LocalStorage.set('myapp.currentRefreshToken',token); // and sync the localstorage value
},
getRefreshToken: function(){
return currentRefreshToken; // quick access to the value from RAM
}
};
});
myapp.factory('LocalStorage',function($window) {
var localStorage = {};
localStorage.set = function(key, value) {
$window.localStorage[key] = value;
};
localStorage.get = function(key, defaultValue) {
return $window.localStorage[key] || defaultValue;
};
localStorage.setObject = function(key, value) {
$window.localStorage[key] = JSON.stringify(value);
};
localStorage.getObject = function(key) {
return (!$window.localStorage[key] || $window.localStorage[key] === undefined) ? {} : JSON.parse($window.localStorage[key]);
};
localStorage.setArray = function(key, array){
if (!array.length) {
console.debug(array);
$window.localStorage[key] = '[]';
} else{
this.setObject(key, array);
}
};
localStorage.getArray = function(key){
return (!$window.localStorage[key] || $window.localStorage[key] === undefined) ? [] : JSON.parse($window.localStorage[key]);
};
localStorage.exportAsFile = function(key, fileName){
var data = [$window.localStorage[key]] || ['{}'];
var blob = new Blob(data,{type:'application/json;charset=utf-8'});
$window.saveAs(blob,fileName);
};
return localStorage;
});
Related
I'm trying to authenticate user through token. if i login then token will be created and stored in local storage. whenever there is a change in route I'm hitting the api which is built in express js , gives me decoded user value. everything works without refresshing page. Once I refresh the page I'm not able to hit the API. in order to get decoded user value i suppose to click on login button which is there in header , which triggers the route change then again everything works fine. Please help me out .
.controller('mainController', function($rootScope, $location, $window ,Auth){
var vm = this;
$rootScope.loggedIn = Auth.isLoggedIn();
$rootScope.$on('$locationChangeStart', function(){
$rootScope.loggedIn = Auth.isLoggedIn();
Auth.getUser()
.then(function(data){
$rootScope.user = data.data;
});
});
vm.login = function(){
......
}
vm.logout = function(){
......
}
})
Service
.factory('Auth', function($http, $q, AuthToken){
var authFactory = {};
authFactory.login = function(username, password){
return $http.post('/api/login', {
username: username,
password: password
})
.success(function(data){
AuthToken.setToken(data.token);
return data;
});
};
authFactory.logout = function(){
AuthToken.setToken();
};
authFactory.isLoggedIn = function(){
if(AuthToken.getToken()){
return true;
} else {
return false;
}
};
authFactory.getUser = function(){
if(AuthToken.getToken()){
return $http.get('/api/me');
} else {
return $q.reject({ message: "User has no token"});
}
};
return authFactory;
})
factory for setting token and interceptor code
.factory('AuthToken', function($window){
var authTokenFactory = {};
authTokenFactory.getToken = function(){
return $window.localStorage.getItem('token');
};
authTokenFactory.setToken = function(token){
if(token){
$window.localStorage.setItem('token', token);
} else {
$window.localStorage.removeItem('token');
}
};
return authTokenFactory;
})
.factory('AuthInterceptor', function($q, $location, AuthToken){
var interceptorFactory = {};
interceptorFactory.request = function(config){
var token = AuthToken.getToken();
if(token){
config.headers['x-access-token'] = token;
}
return config;
};
interceptorFactory.responseError = function(response){
if(response.status == 403){
$location.path('/login');
}
return $q.reject(response);
};
return interceptorFactory;
});
It may be that you have to reset the $http default headers on refresh. Using cookies in my case, I make a call to the following function at the beginning of $on('$stateChangeStart'):
service.RefreshGlobalVars = function () {
if ($http.defaults.headers.common.RefreshToken == null) {
$http.defaults.headers.common.Authorization = "Bearer " + $cookieStore.get('_Token');
$http.defaults.headers.common.RefreshToken = $cookieStore.get('_RefreshToken');
}
};
edit- to clarify, since I haven't seen your setToken() function, your implementation may vary, but that's pretty much the gist of it.
I got answer, solved it by checking route change in the main app.js, inside run block.
MyApp.run(function ($rootScope, $location, Auth){
$rootScope.loggedIn = Auth.isLoggedIn();
$rootScope.$on('$locationChangeStart', function(){
$rootScope.loggedIn = Auth.isLoggedIn();
Auth.getUser()
.then(function(data){
$rootScope.user = data.data;
});
});
I'm learning angular, and I'm trying to use a service to store data from an HTTP request, and be able to access it later.
Problem:
Data object is empty every time I try to retrieve it, which causes it to make a new call. I'm using this in the context of a ui-router resolve(does this cause the service to re-instantiate and lose the data)?
Service:
evaApp.factory('userService', ['$http', '$q', function ($http, $q) {
var user = {};
return {
makeRequest : function(url, uid) {
var deferred = $q.defer();
if (!uid) { uid = ''; };
$http.get(url, { params : { userId : uid } }).then(function (res) {
deferred.resolve(res.data);
});
return deferred.promise;
},
getUser : function(userId) {
console.log(user); // user is always empty
if(!user || !user._id) {
user = this.makeRequest('/api/user/get', userId);
};
return user;
}
}
}]);
Addition:
Data storage is working using PSL's solution. Data retrieval is not: Link to question.
this.makeRequest returns a promise and it does not have a _.id property which is causing it to make the ajax call again (due the condition if(!user || !user._id) {). just return the promise itself from getUser and use it. Remember you are not assigning the user instead assigning a promise by doing user = this.makeRequest('/api/user/get', userId);
Instead just do:-
var user = {};
getUser : function(userId) {
return user[userId] || (user[userId] = this.makeRequest('/api/user/get', userId)
.catch(function(){ user = null })); //nullify in case of error for retry
}
and in make request just do:
makeRequest : function(url, uid) {
if (!uid) { uid = ''; };
return $http.get(url, { params : { userId : uid } }).then(function (res) {
return res.data;
});
},
and while making call from controller you would do:-
mySvc.getUser(userId).then(function(user){
myCtrlInstance.user = user;
});
Note: Avoid using deferred anti-pattern when you already have an operation that returns a promise.
You can make something like this:
evaApp.factory('userService', ['$http', '$q', function ($http, $q) {
var user = {};
return {
makeRequest : function(url, uid) {
var deferred = $q.defer();
if (!uid) { uid = ''; };
$http.get(url, { params : { userId : uid } }).then(function (res) {
user = res.data;
deferred.resolve(user);
});
return deferred.promise;
},
getUser : function(userId) {
console.log(user); // user is always empty
if(!user || !user._id) {
return this.makeRequest('/api/user/get', userId);
};
var deferred = $q.defer();
deferred.resolve(user);
return deferred.promise;
}
}
}]);
And then get the user details like this (the 1 is just for the example):
userService.getUser(1).then(
function( data ) {
console.log(data);
}
);
I'm trying to move this http request to a service and because I'm new to AngularJS I'm having some real issues. Can you help me figure out how to move not only the http request but the viewValue over from the controller to the service?
app.controller('poController',
function poController($scope, $http, poService) {
$scope.test = '';
$scope.fetchPartNumbers = function ($viewValue) {
return $http.get('/api/GetInventoryById/' + $viewValue
).then(function (res) {
var PartNumbers = [];
angular.forEach(res.data, function (item) {
PartNumbers.push(item.PartNumber);
});
return PartNumbers;
});
};
});
I can move the http request over, but I'm not sure how to push the $viewValue over or have it accessible in the service as well.
app.factory('poService', function () {
});
Just add it as a parameter:
app.factory('poService', function ($http) {
return {
fetchPartNumbers: function(value) {
return $http.get('/api/GetInventoryById/' + value).then(function(res) {
var PartNumbers = [];
angular.forEach(res.data, function (item) {
PartNumbers.push(item.PartNumber);
});
return PartNumbers;
});
}
}
});
And the controller
$scope.fetchPartNumbers = function ($viewValue) {
poService.fetchPartNumbers($viewValue).then(function(data) {
console.log(data)
});
}
I have a simple AngularJS app running in a Chrome Extension making use of the Storage API. Having an issue with the async nature of Storage; I've abstracted the storage away into a 'UserService' that sets and gets the data as a factory:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service);
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
});
The console.log() call above dumps out the data as expected. However, when I am including the UserService in other factories (I have an APIService that makes use of a user-specific API token), the UserService parameter is being flagged as 'undefined' in the code below:
app.factory('APIService',
function($resource, $http, UserService, AppSettings) {
var token = UserService.user.api.token;
...
});
I am sure I am not fully grasping the Angular promise pattern in terms of consuming resolved promises throughout the app.
Updated code:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user);
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
return service;
});
Edit/Additional Info:
Ok, getting close. Have refactored so that I am returning the object properly, but the issue now is that when the APIService gets created and tries to use the properties of the UserService object, they simply don't exist yet as they are only created after the async restore method is resolved. So it's not possible to access the UserService.user.api.token property, as it doesn't exist at that point, so the question is, how do I get that data in APIService when I need it if it is not available at that point? I'm trying to avoid having to put the entire contents of APIService into a callback that fires after a hypothetical new UserService.get() method that calls the callback on resolution of the promise. Any final guidance appreciated.
Your service is wrong. Please look at my fix:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user); // <--- return the user in here
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
return service; // <--- return the service to be used injected when injected
});
[EDIT]
answer to your new question: Dont access user directly. create a new function in your service like getUser() that returns a promise. In that function return the user if it is already retreived otherwise return the restore() function:
var service = {
user: null,
getUser: function() {
if (service.user)
{
var deferred = $q.defer();
deferred.resolve(service.user);
return deferred.promise;
}
else
return service.restore();
},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user); // <--- return the user in here
});
return deferred.promise;
}
};
You're not returning an object from your factory. So when you try to inject your UserService parameter, it gives undefined because you haven't returned anything from your UserService function.
If you return your service variable, I think you'll get the behavior you're looking for.
I've made an interceptor in my application that detects session loss (server sends an HTTP 419). In this case, I need to request a new session from the server, and then I would like to send the original request again automatically.
Maybe I could save the request in a request interceptor, and then send it again, but there might be a simpler solution.
Note that I have to use a specific webservice to create the session.
angular.module('myapp', [ 'ngResource' ]).factory(
'MyInterceptor',
function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
if(response.status == 419){
// session lost
// create new session server-side
// Session.query();
// then send current request again
// ???
}
return $q.reject(response);
});
};
}).config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('MyInterceptor');
});
Here is my solution using promises for those interested. Basically you need to request a new session, and wait for the response before sending a new request corresponding to the original request (using response.config). By returning the promise $http(response.config) you ensure that the response will be treated as if it was the original request.
(syntax may not be the best as I'm new to promises)
angular.module('myapp', [ 'ngResource' ]).factory(
'MyInterceptor',
function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
if(response.status == 419){
// session lost
var Session = $injector.get('Session');
var $http = $injector.get('$http');
// first create new session server-side
var defer = $q.defer();
var promiseSession = defer.promise;
Session.query({},function(){
defer.resolve();
}, function(){
// error
defer.reject();
});
// and chain request
var promiseUpdate = promiseSession.then(function(){
return $http(response.config);
});
return promiseUpdate;
}
return $q.reject(response);
});
};
}).config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('MyInterceptor');
});
The responseError method of httpInterceptor have to be like this:
responseError: function (response) {
// omit the retry if the request is made to a template or other url
if (response.config.apiCal === true) {
if (response.status === 419) {
var deferred = $q.defer();
// do something async: try to login.. rescue a token.. etc.
asyncFuncionToRecoverFrom419(funcion(){
// on success retry the http request
retryHttpRequest(response.config, deferred);
});
return deferred.promise;
} else {
// a template file...
return response;
}
}
}
And the magic happens here:
function retryHttpRequest(config, deferred){
function successCallback(response){
deferred.resolve(response);
}
function errorCallback(response){
deferred.reject(response);
}
var $http = $injector.get('$http');
$http(config).then(successCallback, errorCallback);
}
You're on the right path, you basically store the request in a queue and retry it after you've re-established the session.
Check out this popular module: angular http auth (https://github.com/witoldsz/angular-http-auth). In this module, they intercept 401 responses but you can model your solution off of this approach.
More or less the same solution, translated in typescript:
/// <reference path="../app.ts" />
/// <reference path="../../scripts/typings/angularjs/angular.d.ts" />
class AuthInterceptorService {
static serviceId: string = "authInterceptorService";
constructor(private $q: ng.IQService, private $location: ng.ILocationService, private $injector, private $log: ng.ILogService, private authStatusService) {}
// Attenzione. Per qualche strano motivo qui va usata la sintassi lambda perché se no ts sbrocca il this.
public request = (config: ng.IRequestConfig) => {
config.headers = config.headers || {};
var s: AuthStatus = this.authStatusService.status;
if (s.isAuth) {
config.headers.Authorization = 'Bearer ' + s.accessToken;
}
return config;
}
public responseError = (rejection: ng.IHttpPromiseCallbackArg<any>) => {
if (rejection.status === 401) {
var that = this;
this.$log.warn("[AuthInterceptorService.responseError()]: not authorized request [401]. Now I try now to refresh the token.");
var authService: AuthService = this.$injector.get("authService");
var $http: ng.IHttpService = this.$injector.get("$http");
var defer = this.$q.defer();
var promise: ng.IPromise<any> = defer.promise.then(() => $http(rejection.config));
authService
.refreshAccessToken()
.then((response) => {
that.$log.info("[AuthInterceptorService.responseError()]: token refreshed succesfully. Now I resend the original request.");
defer.resolve();
},
(err) => {
that.$log.warn("[AuthInterceptorService.responseError()]: token refresh failed. I need to logout, sorry...");
this.authStatusService.clear();
this.$location.path('/login');
});
return promise;
}
return this.$q.reject(rejection);
}
}
// Update the app variable name to be that of your module variable
app.factory(AuthInterceptorService.serviceId,
["$q", "$location", "$injector", "$log", "authStatusService", ($q, $location, $injector, $log, authStatusService) => {
return new AuthInterceptorService($q, $location, $injector, $log, authStatusService)
}]);
Hope this help.