There is a hidden page in my angular app, so only special id user can access it. To implement it I send HTTP get request to the server (because only the server knows that special id) when state changes to that hidden state.
And if server responses to me with an error message then I prevent state changing process, in other case user goes to that special page.
Here is the code I am using:
angular
.run(["$rootScope", "$location", "$state", "mySvc", function ($rootScope, $location, $state, mySvc) {
var id = mySvc.getId();
$rootScope.$on( '$stateChangeStart', function(event, toState , toParams, fromState, fromParams) {
if(toState.name === "specialstate") {
mySvc.check(id)
.then(function(result) {
if(result.error !== 0) {
event.preventDefault();
}
}, function(error) {
event.preventDefault();
});
}
});
}]);
Function in service:
function check(id) {
var deferred = $q.defer();
$http({
url: "/api/url",
method: "GET",
headers: {'Content-Type': 'application/json'}
}).
then(function (result) {
console.log(result);
if(result.data.error === 0) {
deferred.resolve(result.data);
}
else {
deferred.reject(result.data);
}
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
};
Everything is right, except one thing: the state is being changed without waiting for request result. I think I should use resolve, but I do not know how to use it properly.
Thanks.
ui-router supports a resolve object that defines aset of data that should be resolved (from server, for instance), before the state transition is allowed.
.state('hidden', {
url: '/hidden',
...
resolve: {
data: function(mySvc) {
var id = mySvc.getId();
return mySvc.check(id).then(function(result) {
if (result.error !== 0) {
throw new Error("not allowed");
}
});
}
}
});
The data object defined would be available to your controller in that hidden state, and only in case it returns a 2xx status code and there's an error property equal to 0.
If data is resolved, then a state change occurs and a controller is instantiated. In case of rejection, none of this takes place.
As a personal note, I find that a great device to handle authorization.
Related
We have an application that requires users to be logged in. Once logged in the user interacts with web api. Our issue is when the authentication expires we want to show a Login page, for the user to log back in without being redirected. This way they will not lose there work. Our issue is as follows:
Inside our Controller an request is made
$http.get("/api/Document/GetDocumentsManage").success(function (response) {
$scope.Documents = response;
});
On the server we identify that the user is no longer authenticated and we reject the call. We then use an interceptor to catch the error and handle it to show a popup modal.
$httpProvider.interceptors.push(function ($q, $injector) {
return {
'responseError': function (rejection) {
var response = rejection;
var defer = $q.defer();
if (rejection.status == 401 || rejection.status == 400) {
var modal = $injector.get("$mdDialog");
modal.show({
parent: angular.element("body"),
targetEvent: window.event,
templateUrl: "/directives/LoginPage/Login.html",
controller: "loginController",
onRemoving: function () {
var $http = $injector.get("$http");
$http(rejection.config);
}
});
}
}
};
});
With this code we can successfully re-authenticate without the user navigating away from the page, and then once authenticated again we can execute the original request. Our issue is the resubmitted request is not bound to the original .success callback of our request. Therefore in this example $scope.Documents does not get set to the response. Is there anyway we can rerun any request that failed and continue execution?
You are definitely on the right track! You just need a few minor changes to ensure that the result makes in back to your controller:
$httpProvider.interceptors.push(function ($q, $injector) {
return {
'responseError': function (rejection) {
var response = rejection;
var defer = $q.defer();
var $http = $injector.get("$http"); //moved this for readability
var modal = $injector.get("$mdDialog"); //moved this for readability
if (rejection.status == 401 || rejection.status == 400) {
modal.show({
parent: angular.element("body"),
targetEvent: window.event,
templateUrl: "/directives/LoginPage/Login.html",
controller: "loginController",
onRemoving: function () {
// resolve the deferred
deferred.resolve();
}
});
// return the promise object
return deferred.promise.then(function() {
// return the new request promise
return $http(rejection.config);
});
}
return $q.reject(rejection);
}
};
});
Take a look at the is blog example where they are doing the same thing you are trying to do: http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/#sessionrecovererresponseerrorinterceptor
Can you tell me what is the correct way to redirect to another page if $http.post returns a specific error code.
Just to add context, I want to redirect to another page if the user is not logged in or authorized to use the API.
function getData(filter) {
var deferred = $q.defer();
var data = JSON.stringify(filter);
$http.post('/myapp/api/getData', data)
.success(function (data, status, headers, config) {
deferred.resolve(data);
})
.error(function (error) {
deferred.reject(error);
});
return deferred.promise;
}
You could do a redirect to the page using $window.location.href, based on the error condition you have.
var app = angular.module("sampleApp", []);
app.controller("sampleController", [
"$scope",
'$window',
'sampleService',
function($scope, $window, sampleService) {
sampleService.getData().then(function(result) {}, function(error) {
if (error.statusCode === 400) {
alert("Error");
$window.location.href = "http://stackoverflow.com"
}
});
}
]);
app.service("sampleService", function() {
this.getData = function() {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
reject({
statusCode: 400
});
}, 1000);
});
return promise;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-App="sampleApp">
<div ng-controller="sampleController">
</div>
</div>
The best way to catch global AuthenticationErrorin angular is with interceptor.
This way you can monitor all request that are sent from angular and check for AuthenticationError.
$provide.factory('AuthErrorInterceptor', function($q, $location) {
return {
'responseError': function(rejection) {
//check for auth error
$location.path('/login');
return $q.reject(rejection);
}
};
});
Example :
$http.post('/myapp/api/getData', data)
.then(function (data) {
if(data.ErrorCode==1)
{
$window.location.href="controllerName/actionName";
}
})
Use a interceptor service in order to centralize all of your rejection request in the same service.
module.config(['$httpProvider', ($httpProvider: ng.IHttpProvider) => {
$httpProvider.interceptors.push('errorService');
}]);
module.factory('errorService', ['$location', function($location) {
var errorService = {
responseError: function(rejection) {
if (rejection === '401') {
$location.path('/login');
}
}
};
return errorService;
}]);
The $http.post is misguiding.
So far the best answer is #Kliment's. Interceptors are the best way to manage what comes before and after http requests.
However, if your end goal is to prevent access to a page, you have to at least use a routing plugin (ngRoute, ui-router) because with the promise idea there will always be a delay between the http request and the response.
Depending on server response time you'll still see the page display for about a second or so.
With ui-router you simply configure a resolve method for each state you want to protect. It could look like this:
.state('protected',
{
url : '/protected_page',
templateUrl : 'secret.html',
resolve: {
loggedin: loggedin
}
})
loggedin refers to a function you define that contains your $http.post call (or better yet a service)
function loggedin($timeout, $q, $location, loginService) {
loginService.then(function(data) {
if(data.status == 401) {
//$timeout(function() { $location.path('/login'); });
return $q.reject();
} else {
return $q.when();
}
});
}
Here this particular service returns a 401 status but you can return anything.
The state will not be resolved (and the page not displayed) until it's accepted or rejected.
You can redirect directly from there if you want, although it's not very elegant.
ui-router gives you another option with default redirection:
if (tokenIsValid())
$urlRouterProvider.otherwise("/home");
else
$urlRouterProvider.otherwise("/login");
With otherwise you tell ui-router to go to certain urls if no state exists for a particular request or if a resolve has been rejected.
On another subject, your http request is badly written.
.success and .error are deprecated and you don't need to create a promise ($q) over an $http request that itself already returns a promise.
You have a good example in the documentation linked above.
You can redirect to page on unauthorized access of a user based on the status code which you can return from your API call.
$http({
method: "POST",
url: 'Api/login',
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}
}).success(function (data,status) {
if(status==200){
alert('Successfully logged in');
$location.path('/dashboard'); //You can use this if you are defined your states.
}
}).error(function (data,status) {
if(status==403||status==403){ //Based on what error code you are returning from API
$location.path('/login');// if states are defined else
$window.location.href = "https://www.google.com";
}
});
First of all Nice Question , In this scenario You Can use $location , $state If it is external url You can use $window.location.href ... I would recommend $location and it is the best way ...
Please See the link for further Using $window or $location to Redirect in AngularJS
function getData(filter) {
var deferred = $q.defer();
var data = JSON.stringify(filter);
$http.post('/myapp/api/getData', data)
.success(function (data, status, headers, config) {
deferred.resolve(data);
if(data.errorcode==9999) // Define Your Error Code in Server
{
$location.path('/login'); // You Can Set Your Own Router
} })
.error(function (error) {
$location.path('/login'); // You Can Set Your Own Router
deferred.reject(error);
});
return deferred.promise;
}
Preferably use $location or $state ...
I've made an AuthService that should help me take care of all authentication matters like logging in, registering, getting user data from server etc.
I am looking for a solution that runs only on login, on page refresh and when triggered to refresh get the user data from the service and make it available on the controllers that i include the service to. I would like to have something like vm.user = AuthService.getUserData() and this returns the session variable from the service. Something like this:
function getUserData(){
if (session) {
return session.user;
}
return null;
}
On $rootScope.$on('$stateChangeStart' i have :
AuthService.loadSessionData();
Which translates to:
function loadSessionData() {
$http({
url: API.URL+'auth/session-data',
method: 'GET'
})
.success(function(response)
{
session = response;
})
.error(function(err){
console.log(err);
});
};
One of the issues here are that i have to set a timeout on AuthService.getUserData() because when this executes, the call that retrieves the session data from the server is not finished yet, but this is a bad practice.
Here is the complete service code http://pastebin.com/QpHrKJmb
How about using resolve? If I understood correctly you wish to have this data in your controller anyway.
You can add this to your state definitions:
.state('bla', {
template: '...'
controller: 'BlaCtrl',
resolve: {
user: ['AuthService', function(AuthService) {
return AuthService.loadSessionData();
}
}
}
also, alter your loadSessionData function: (make sure to inject $q to AuthService)
function loadSessionData() {
return $q(function(resolve, reject) {
$http({
url: API.URL + 'auth/session-data',
method: 'GET'
})
.success(function(response)
{
if (response) {
resolve(response);
} else {
reject();
}
})
.error(function(err){
reject(err);
});
})
}
Lastly, add the user object from the resolve function to you controller:
app.contoller('BlaCtrl', ['$scope', 'user', function($scope, user) {
$scope.user = user;
}]);
What does this accomplish?
In case the user does not have a valid session or an error occurs, the state change is rejected and the event $stateChangeError is thrown. You can listen (like you did for $stateChangeStart for that event and respond with a modal, redirect to an error page, or whatever.
You only pull the session for states that needs it - not for every state change.
The state is not resolved until the user data is resolved as well (either by resolve or reject).
You should call loadSessionData() in getUserData()
function getUserData(){
loadSessionData()
.success(function(response)
{
if (response) {
return response.user;
}
})
return null;
}
and
function loadSessionData() {
return $http.get(API.URL+'auth/session-data');
};
I am sure this is a configuration error, but not sure how to get round it correctly with angular.
I have my states setup like so
$stateProvider
.state('app', {
url: '',
abstract: true,
templateProvider: ['$templateCache', function ($templateCache) {
return $templateCache.get('app/main/main.html');
}],
controller: 'MainController',
resolve: {
Pages: ['PageFactory', function (PageFactory) {
return PageFactory.getAll();
}]
}
})
.state('app.home', {
url: '/home',
views: {
'content#app': {
templateProvider: ['$templateCache', function ($templateCache) {
return $templateCache.get('app/page/page.html');
}],
controller: 'PageController'
}
},
resolve: {
Page: ['$stateParams', 'PageFactory', function ($stateParams, PageFactory) {
return PageFactory.get($stateParams.id);
}],
ModuleData: function () {
return {};
},
Form: function () {
return {};
}
}
});
Now my problem is that if PageFactory.get($stateParams.id) fails, then the page is reloaded, and reloaded and reloaded.
Here is a sample from PageFactory, which as you can see returns a promise
angular.module('app').factory('PageFactory', ['$http', '$q', function ($http, $q) {
var urlBase = 'api/page';
var factory = {};
factory.getAll = function () {
var $defer = $q.defer();
$http.get(urlBase)
.success(function (data) {
$defer.resolve(data);
})
.error(function (error) {
$defer.reject(error);
});
return $defer.promise;
};
factory.get = function (id) {
var $defer = $q.defer();
$http.get(urlBase + '/' + id)
.success(function (data) {
$defer.resolve(data);
})
.error(function (error) {
$defer.reject(error);
});
return $defer.promise;
};
}]);
It is possible to restrict the number of times the resolve is attempted, or should i have set this up differently in the first place?
I noticed this when moving the site from 1 place to another and needed to specify a base href in my index.html page. Because the $http was trying to connect to a url that didnt exist, it just kept on trying and trying and trying, which if someone left it would harn our web servers performance.
My state app is my route state (abstract state) and the user defaults to app.home uponing entering the site. I guess this is why it just keeps retrying the resolve?
We also had infinite loops when there were errors in resolves, so we ended up having more logic in the $stateChangeError event handler. Since we have this, we had no more troubles with loops.
See how we check where we were trying to go to and if we failed while going to a home state, we do not try again and redirect to a simple error state instead.
Here our example, this is setup in our main module's run method:
$rootScope.$on("$stateChangeError", function (event, toState, toParams, fromState, fromParams, error) {
console.log('Error on StateChange from: "' + (fromState && fromState.name) + '" to: "'+ toState.name + '", err:' + error.message + ", code: " + error.status);
if(error.status === 401) { // Unauthorized
$state.go('signin.content');
} else if (error.status === 503) {
// the backend is down for maintenance, we stay on the page
// a message is shown to the user automatically by the error interceptor
event.preventDefault();
} else {
$rootScope.$emit('clientmsg:error', error);
console.log('Stack: ' + error.stack);
// check if we tried to go to a home state, then we cannot redirect again to the same
// homestate, because that would lead to a loop
if (toState.name === 'home') {
return $state.go('error');
} else {
return $state.go('home');
}
}
});
The common way to use resolve with promises is to use $q.defer()
Pages: ['PageFactory', '$q', function (PageFactory, $q) {
var deffered = $q.defer();
PageFactory.getAll()
.success(function(data) {
deffered.resolve(data);
})
.error(function(data) {
deffered.reject();
})
return deferred.promise;
}]
This will reject the the state change if it fails, or you can do whatever when it fails. And it passes the data through if it succeeds.
Also see this post about the same thing.
Angular ui-router get asynchronous data with resolve
I have a Service which wraps my API calls in Angular:
var ConcernService = {
list: function (items_url) {
var defer = $q.defer();
$http({method: 'GET',
url: api_url + items_url})
.success(function (data, status, headers, config) {
defer.resolve(data, status);
})
.error(function (data, status, headers, config) {
defer.reject(data, status);
});
return defer.promise;
},
Then my app config, with UI-Router:
.config(function($stateProvider){
$stateProvider
.state('default', {
url: '/',
resolve: {
tasks: function ($stateParams, ConcernService) {
return ConcernService.list('tasks/').then(
function (tasks) { return tasks; },
function (reason) { return []; }
);
},
...
}
}
});
This is the most basic configuration I could get away with, which basically just returns an empty object if a 403, 404 etc is encountered and I can handle that in the view, template.
My question is, what is the best approach for getting the other detail to the view/ template, such as the rejection reason and status. Should it be returned in the tasks object, or separately?
Well, first of all, your first bit of code has the deferred anti pattern, let's fix that:
list: function (items_url) {
return $http.get(api_url + items_url); // $http already returns a promise
},
Also note, that deferred rejections and fulfillments are single value, so your multiple return values don't really work here.
Now, let's look at the router. First of all, your first fulfillment handler is redundant.
.state('default', {
url: '/',
resolve: {
tasks: function ($stateParams, ConcernService) {
return ConcernService.list('tasks/'); // result.data contains the data here
},
...
}
Now, the problem we have here is what happens in case of a rejection?
Well, listen to $stateChangeError:
$rootScope.$on('$stateChangeError',
function(event, toState, toParams, fromState, fromParams, error){ ... })
Here, you can listen to change state failures from rejections.