AngularJs watch for change in variable inside a service - angularjs

I have a service named loginManager which stores objects called is_logged_in & api_token along with few others. My various controllers make ajax calls using $http using the api_token.
If the api_token is reset/expired on server, response is sent as auth_error, at this point I set is_logged_in = false
What i want to achieve is, whenever is_logged_in is changed, the service redirects to /login using $location.path('/login'), i.e. to say, I want to watch the object inside the service, and invoke callback on change from service itself.
I just want the service to take care of login and corresponding routing, without any controller worrying about weather user is logged in or not.

I believe Pankaj Pakar's answer could work but you should use angular's interceptors for that. They intercept all messages. You could add hook for response or responseError and when you recieve auth_error you do any action you like. For example $location.path('/login'), display error to user, etc.
If you want to separate logic you could inject your service with all code inside and just call some method on it.

I'd suggest you to put that watcher in run phase on the angular application which will be there at a single place, by which you could check the value is_logged_in flag of service & if user is not login then redirect him/her to login page directily.
Code
app.run(function($rootScope, loginManager, $location){
$rootScope.$watch(function(){
return loginManager.is_logged_in;
}, function(newValue){
if(angular.isDefine(newValue) && !newValue)
$location.path('/login');
//$state.go('login'); //if you are using ui.router
})
})
Edit
Really curious part of your question is, from where you are changing is_logged_in flag of your service as #JBNizet asked? If any code is there is JavaScript then you should directly redirect to login page from there.

I feel the need to answer something more, Mior is quite right, but his answer needs more meat.
Here I show you how I managed to handle ALL server XHR requests with response 401 unauthorized.
First of all you need a service:
'use strict';
angular.module('theModule')
.factory('interceptorService', ['$q', '$location', function ($q, $location) {
return {
response: function (response) {
return response || $q.when(response);
},
responseError: function (rejection) {
var returnTo = $location.path().replace(/^\/|\/$/g, '');
if (returnTo === 'login') {
return;
}
if (rejection.status === 401) {
console.log('Unauthorized');
$location.path('/login').search('returnTo', returnTo);
}
return $q.reject(rejection);
}
};
}]);
This will be used to intercept all XHR calls and to change the location every time a 401 error is found.
I've also added an improvement that is the "returnTo" parameter, you will be able to use it after login to return to the previous page.
To bind it to each request you have to call the config method, this is my main javascript.
'use strict';
/**
* #author Gianmarco Laggia
*
* Main module of the application an configurations.
*/
angular
.module('theModule', [])
.config(['$httpProvider', function ($httpProvider) {
//Http Interceptor to check auth failures for xhr requests
$httpProvider.interceptors.push('interceptorService');
}]);
This is pretty much what you need to intercept every request, working on the rejection.status you can also intercept events such as server down (status is -1), internal server error (500+), success status (in the response part, status 200+) etc.

Related

How to set a value in responseError so that the request can use it for every request in AngularJS interceptor

I have a question on AngularJS interceptor.
I have 2 sets of users: legacy user and new user.
For every legacy user, to continue they have to reset their password first. That means they have to be authenticated first and this API sends a status code of 420 and then the user is redirected to Password reset page. And until the user resets his password, no other API calls are honored.
For this I'm using AngularJS interceptor.
In the responseError function I check if the status code I receive is 420. If it is, I set a variable in $rootScope so that all the subsequent requests can be suppressed based on this flag in the request function of the interceptor. But this value is always undefined after the redirect.
I also tried creating a new Service and injecting it in the interceptor and calling the service method to set a variable from responseError. Then, when I read the variable from request function it was always undefined after the redirect. So, no luck here either.
When the request method is executed the user would have been redirected to a new page. It shouldn't have reset the value in either $rootScope or in the Service, am I right?
What am I missing here? Can someone point me in the right direction here?
angular.module('app')
.factory('httpInterceptor', ["$window", "$q", "$rootScope", "$injector", function ($window, $q, $rootScope, $injector) {
return {
request: function (config) {
// Read the value from the $rootScope. If it's true, cancel all the subsequent requests
var redirected = $rootScope.app.redirected;
console.log("request: redirected: " + redirected); // The value is always 'undefined' here
var canceller = $q.defer();
if (redirected) {
// Canceling request
canceller.resolve();
}
return config;
},
responseError: function (response) {
var statusCode = response.status;
var newLocation = response.headers('location');
if (statusCode === 420 && newLocation) { // Status code is 420. Redirect and set a flag in the $rootScope
$rootScope.app.redirected = true;
$window.location.href = newLocation;
}
return response || $q.when(response);
}
}
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
}]);
Finally, my efforts in solving this made me more knowledgeable today than I was yesterday. And, it feels so good to answer your own question :)
So, it looks like when used $window.location.href, it resets the $rootScope variable. As a fix, I replaced this line of code with $location.path(newLocation)
I switched back to using Service and I can see the value and it's not undefined

Use of AngularJS Factory for Authentication

New to AngularJS.. I want to build a simple Logon screen using a logon page, utilising a controller (I call it 'AccessCtrl') and service.
I want the controller to make use of a service (I called it 'AuthService'), which will perform the REST call that verifies the username/password, and, will also contain information about the success or failure of that logon attempt.
I want to receive information about the last login attempt (as returned in by the REST call). For now - just a string which I want to show on the logon screen, (eg 'password invalid' or 'account expired', or 'welcome'). I'm making this a property of the AuthService service which I hope to display.
My view, has a form with username, password and a submit button that calls the controller's login() method. I'm not including it here for brevity. I don't think thats where the problem lies.
To start with, I want to capture when the server is down/unavailable, etc, and also report this, using the same service. To start with - all calls will fail,(because I have an invalid url).
The controller is defined like this:
angular.module('loginApp')
.controller('AccessCtrl', ['$scope','$http','AuthService',
function ($scope,$http,AuthService,Config) {
this.login =function() {
var user={user:this.username,password:this.password,otherCredentials:this.otherCredentials} ;
AuthService.login(user);
this.message=AuthService.message;
};
}]);
My factory service is defined as follows:
.factory('AuthService', ['$http', function ($http) {
var authService = {};
var apiRootUrl="http://somesite.com/urany";
authService.login = function (credentials) {
authService.message="";
$http.post(apiRootUrl+'/login', credentials)
.then(
function (response) {
// populate OK message
authService.message="Welcome !";
},
function (response) {
// Currently - all calls fail, as the URI is invalid
// I want to trap this.
authService.message = "Server unavailable with response status = " + response.status ;
return authService;
},
function(response) {
// this is the notify callback function.
//
});
};
authService.isAuthenticated = function () {
// rerurn true or false if previously authenticated
return true;
};
return authService;
}])
;
The problem is that my invalid message ('Server unavailable with response status...') does not appear in my view.
In stepping through the code, the correct authService function is called, but, this does not seem to be populated into the controller/view.
I have a feeling this is a timing issue, where the controller that is executing
this.message=AuthService.message;
before the call actually comes back from the post - but I'm not sure that is the case or how to fix that.
any help/clues welcome.
thanks.
S
Have you tried something like this?
Angular Service Code:
//call the service
AuthService.login(user).then(function(response){//begin call back
//store the response data message object property returned from the service
$scope.message = response.message;
}//end call back

Change an existing $http request interceptor

In my angular app, I have a $http request interceptor called 'authInterceptor' that I create like this:
.factory('authInterceptor', function ($q, $window, EXT_API_BASE, $injector) {
return {
request: function (config) {
if (config.url.indexOf(EXT_API_BASE) !== -1){
var Auth = $injector.get('Auth');
if(Auth.user && Auth.user.token){
config.headers.Authorization = 'Web ' + Auth.user.token;
}
}
return config;
}
}});
It gets registered in a .config():
$httpProvider.interceptors.push('authInterceptor');
As you see, my Authorization headers are bound to a Auth.user.token value. This value is available when my user is signed in.
Then the headers are sent for any calls to my api.
The problem I am facing is... when the user signs out in my angular app, the Authorization headers are still being sent even though I deleted Auth.user.token already.
On a hard refresh of the page, the Authorization headers then get removed completely.
How can I make sure 'authInterceptor' registers the change in token value when my user signs out?
Answering my own question. Made a newbie mistake of changing Auth.user object like this on sigh out:
Auth.user = {}
That created a new object and the requestInterceptor was still referencing the old object.
The correct way to do it is:
delete Auth.user.token

How to get logged user with Angular?

Maybe I am missing something very trivial, but I can't find an answer.
I am implementing a webapp based on nodejs, express and angular for the client side.
I handle my signup and sessions through passport.js. Therefore, server-side, I can access the logged user through request.user.
Now, I have a logged user which goes on its profile page, displayed through an angular view.
Here is the issue: In order to show them user information now I am thinking to send an $http request to the server, which takes the user from request and sends it back to the client where it is displayed. However, this sounds weird to me.
So here is my question: Is there any way to access the user in the session thruogh angular? If so, what user information is actually stored in the client?
Thanks in advance, and apologies if this is something too trivial to be asked:/
You will need to create a service in Angular that requests the current user, and stores the result so you can retrieve it for use in your controllers. There is nothing built in to Angular for this; you will have to create it your self. However, it's fairly straight forward.
myapp // assume was previously defined using var myapp = angular.module('myapp', []);
.factory('principal', ['$http', '$q', function($http, $q) {
var _identity = undefined;
return {
// this function returns the current _identity if defined; otherwise, it retrieves it from the HTTP endpoint
identity: function(setIdentity) {
if (setIdentity) {
_identity = setIdentity;
return;
}
var deferred = $q.defer();
if (angular.isDefined(_identity)) {
deferred.resolve(_identity);
return deferred.promise;
}
$http.get('/path/to/endpoint')
.success(function(result) {
_identity = result;
deferred.resolve(_identity);
})
.error(function() {
_identity = undefined;
deferred.reject();
});
return deferred.promise;
}
};
}]);
This principal service has one method at the moment, identity(). This method returns a promise. If the identity has already been retrieved, it will resolve with that value immediately. If not, it will attempt to get it from an HTTP endpoint. If the HTTP call succeeds, it will memoize the result to _identity and resolve the promise. If the call fails, the promise will be rejected. identity has a secondary use. If you supply it with a single parameter, it will set that value as the identity and return nothing. This is useful if you already have the identity and want to set it right away, for example, after they successfully sign-in.
You could manage the sign-in page like this:
myapp.controller('SignInCtrl', ['$scope', 'principal', '$http', function($scope, principal, $http) {
// these two values will be bound to textboxes in the view
$scope.username = '';
$scope.password = '';
// this function will be called when the sign in form is submitted
$scope.submit = function() {
$http.post('/path/to/signin', {username: $scope.username, password: $scope.password })
.success(function(identity) {
// assumes /path/to/signin returns a JSON representation of the signed-in user upon successful authentication
// presumably the endpoint also sets a cookie representing an auth token or something of that nature. the browser will store this automatically for you
principal.identity(identity); // set the identity immediately
// do whatever else on successful login, like redirect to another route
});
}
}]);
And a controller somewhere else that needs the current identity could do something like this:
myapp.controller('MyCtrl', ['$scope', 'principal', function($scope, principal) {
// retrieve the identity. when it resolves, set user on the scope
principal.identity().then(function(identity) {
// after this, you can use user in your view or later in your controller
$scope.user = identity;
});
}]);
Now you have a way of storing the identity immediately after sign-in. I do make an assumption that your code that signs the user in sets a cookie to represent an auth token or whatever in your signin endpoint. The good thing about this is that if the user refreshes their browser, or the cookie is stored for a period of time, the user can just visit your app and the identity will resolve it using that token cookie automatically.
This plunk is a working demo of a more elaborate set up. Some of it may not apply to you (for example, it uses ui-router instead of regular routing), but it should be a reasonable reference point.

How to load data synchronously in AngularJS application

Now I know that because of the way javascript executes it is recommended that you run all remote requests as async instead of sync. While I agree with that 99% of the time, sometimes you do want to run remote request as a sync instead of a async. For example, loading session data is something I would want to do synchronically as I don't want any views to render until that data is loaded. This plunker shows the issue with loading session data asynchronically (NOTE: I am using $timeout to simulate what would happen with an async call):
http://plnkr.co/edit/bzE1XP23MkE5YKWxRYrn?p=preview
The data property does not load anything because the data is not available when it tries to get it and data2 does only because the data is available when it tries to get it. Now in this case I could just put the session variable on the scope and be done with it but that is not always the case.
Is there a better way to do sync remote calls in an angular application other than using jQuery's .ajax() method (trying to depend on jQuery as little as possible)?
If you want the session data to be loaded prior to a controller being loaded, you should included it as as resolve parameter (assuming you are using the $routeProvider).
For example:
angular.module('mymodule', ['ngResource'])
/* here's our session resource. we can call Session.get() to retrieve it. */
.factory('Session', ['$resource', function($resource) {
return $resource('/api/session.json');
}])
/* here's our controller + route definition. */
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/foo', {
controller: 'MyCtrl',
templateUrl: '/templates/foo.html',
/* the controller will not be loaded until the items
* below are all resolved! */
resolve: {
session: ['$q', 'Session', function($q, Session) {
var d = $q.defer();
Session.get(function(session) {
/* session returned successfully */
d.resolve(session);
}, function(err) {
/* session failed to load */
d.reject(err);
});
return d.promise;
}]
}
});
}])
.controller('MyCtrl', ['$scope', 'session', function($scope, session) {
/* 'session' here is the key we passed to resolve above.
* It will already be loaded and resolved before this function is called */
$scope.session = session;
}]);
Angular is hardcoded to make the requests async. To do it synchronously would take other code, whether custom or from some other library. Here is line 9269 from angular 1.0.7:
xhr.open(method, url, true);
The third param makes it asynchronous.
I would take a step back and think about how you are doing things. You could provide some loading indicator while your async request is going and easily control the loading of a view in the success callback so that it doesn't appear until the data is loaded.
A better solution is to add a response interceptor:
checkAuth = ($q, $location) ->
success = (response) ->
response
error = (response) ->
errorCode = response.status
$location.path '/login' if errorCode is 403 or errorCode is 401
# $q.reject response - no need because we are redirecting before any other promises in the chain will resolve (were breaking our future promises)
(promise) ->
promise.then success, error
$httpProvider.responseInterceptors.push checkAuth
And in your $routeProvider, or $stateProvider in my case:
.state 'user',
templateUrl: 'assets/views/user/partials/user.html'
resolve:
credentials: (checkLogIn) ->
checkLogIn.get().$promise
When checkLogIn.get()'s promise is rejected (the error handler is fired), assuming it's a 401 or 403 response (unauthenticated or unauthorized), the promise chain will be broken and the user will be "redirected" to /login.
With this method, any error calls will be channelled into the interceptor, instead of handling errors on a route-by-route basis.

Resources