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.
Related
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
In my angularjs app I have route /#/tasks, which fetch tasks from /tasks url. If user manually goes to /tasks url, he gets json data. Should I prevent show json data to user? What is the best practice? For example, in backend I can check if request is ajax or not. If not - redirect to base url. Backend - laravel 5.
From the title of the post having the keywords 'prevent user', it seems that you need some sort of logic to define access privileges for a particular user.
You could stick to resolving the authorization logic before a user ever accesses a route.
For this, you can take advantage of the $routeProvider's resolve property of the .when() method.
According to the docs, the resolve property is ...
An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated.
If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired.
The resolve takes in functions (as injectable dependencies) each of which may return a promise, and if any of the promises get rejected, as quoted, an error is thrown and literally, neither the route nor the view is loaded.
For the authorization logic to work, you'll need to perform some API call to some backend function/service which either resolves that the user is permitted for the route, or rejects his/her request.
.when('/tasks', {
resolve: {
authorize: function(authService){
//some api call to get either acceptance (e.g. status code 200) or rejection (status code 500) for the user being authorized
return authService.authorize();
}
},
controller: function(){
//route controller logic
}
})
Alternatively, if you'd also want to perform something on failure of the promise, you could do so by simply chaining the promise with a catch() handler and throw an error to forcefully reject the promise that is returned from the catch() itself.
.when('/tasks', {
resolve: {
authorize: function($location, authService){
//some api call to get either acceptance or rejection for the user being
return authService.authorize()
.then(function(){
//some logic on success of the promise
})
.catch(function(){
$location.path('/auth/login/');
throw 'Authorization error';
});
}
},
controller: function(){
//route controller logic
}
})
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.
I'm trying to get current user coordinates and afterwards send them as a parameter of http request and parse response.
Workflow is next -> user opens view, (loader shown) - coordinates are retrieved (if not send request without coordinates) - send requests - parse and display response - hide loader.
What is the best way to achieve this?
I have tried to create two services, to retrieve coordinates using navigator.geolocation.getCurrentPosition, but I'm unable to return promise in which i would send http request using $http.get.
This workflow should be used in different controllers so I'm searching for a way which code will be most reusable. Thanks!
Update: this is a code sample of how I did resolve waiting for $http to complete before view is shown, but I have no idea how to use same approach with navigator.geolocation as it is not returning a promise.
.state('tab.explore.index', {
url: '',
templateUrl: 'templates/tab-explore.html',
controller: 'ExploreCtrl',
resolve: {
explore: function ($stateParams, Explore) {
console.log("rr");
return Explore.explore();
}
}
});
Service:
.factory('Explore', ["$http", "ApiEndPoint", "Resort", "ServiceTypes", function ($http, ApiEndPoint, Resort) {
return {
explore: function () {
console.log("loc");
**//somehow somewhere here should code wait to retrieve coordinates and send them as url parameter**
var url = ApiEndPoint().explore + Resort.id;
return $http.get(url);
}
}
}]);
controler:
.controller('ExploreCtrl',function ($scope, $stateParams,ServiceTypes,explore) {
console.log(explore.data.Services);
$scope.serviceTypes = ServiceTypes;
$scope.explore = explore.data;
})
We have a resource defined as follows:
var services = angular.module('services', ['ngResource']);
services.factory('Facility', function ($resource) {
var resource = $resource('/facility/:id.json', {id: '#id'}, update);
resource.restore = function (pathParams, success, error) {
$resource('/facility/:id/restore.json', {}, update).update(pathParams, {}, success, error);
};
return resource;
});
where update is defined as a custom action, because Angular does not support PUT by default, as follows:
var update = {update: {method: 'PUT'}};
Now the GET url fired by call to Facility.get is http://localhost:9091/facilities/1.json for e.g., which works normally. But we have Spring security in use and after a session timeout and subsequent redirect to login and login successful, we do a location.reload(), which reloads the edit page, but this GET url fails with JSON parse error "No content to map ... end of input". We have found that redefining another service factory like
services.factory('FacilityGet', function ($resource) {
return $resource('facilities/:id', {}, {});
}
and using it solves the problem. Using either of the update action or the "{id: '#id'}" params causes problems - only when we exclude both of them, the corresponding resource fires the correct GET request.
Please note that the GET fired from earlier resource failed only the very first time after login redirection, but worked fine later. Any help or pointers will be very useful.
Thanks,
Paddy