How to handle angular-ui-router and template loading errors? - angularjs

Using angular-ui-router, I have something like:
.state('page', {
url: '/page',
templateUrl: '/page.html'
})
This template URL may return a "401 Unauthorized". Is it possible to handle the http response when the router tries to load the url and handle it, so I can show some message or redirect the user?

You can register an interceptor for your application. The implementation of this interceptor
$httpProvider.responseInterceptors.push([
'$q',
'$location',
'$rootScope',
(function($q, $location, $rootScope) {
return {
responseError: function(response) {
if (response.status === 401) {
console.log(response.status + ' intercepted');
$location.path('/unauthorized/');
$q.reject(response);
}
return response;
}
};
});
]
After this you need to register /unauthorized in your states with a custom page template.

Related

Circular Dependency Error with Auth0

I'm learning authentication using an Angular and Auth0 video on Udemy.
I've gotten to the point where I'm handling 401 errors and I'm receiving the following error:
angular.js:66 Uncaught Error: [$injector:cdep] Circular dependency found: auth <- redirect <- $http <- auth
And here is my angular configuration:
angular.config(config);
function config($provide, authProvider,
$urlRouterProvider, $stateProvider, $httpProvider, jwtInterceptorProvider) {
authProvider.init({
domain: 'cmckinstry.auth0.com',
clientId: 'Rmdm7tgPIWv1e1P6sKrBDoW8zI4kuOEa'
});
jwtInterceptorProvider.tokenGetter = function(store) {
return store.get('id_token');
}
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'components/home/home.tpl.html'
})
.state('profile', {
url: '/profile',
templateUrl: 'components/profile/profile.tpl.html',
controller: 'profileController as user'
});
function redirect($q, $injector, auth, store, $location) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
auth.signout();
store.remove('profile');
store.remove('id_token');
$location.path('/home');
}
return $q.reject(rejection);
}
}
}
$provide.factory('redirect', redirect);
$httpProvider.interceptors.push('redirect');
$httpProvider.interceptors.push('jwtInterceptor');
}
So, taking out the auth injection from the redirect function. But, then the redirect doesn't work properly. I'm suspecting that this has something to do with the authProvider, but I just can't seem to figure it out.
Look at the error message:
Circular dependency found: auth <- redirect <- $http <- auth
As stated in the error message, the circular dependency is formed because the authProvider uses the httpProvider which then uses the redirectProvider which completes a circle with the authProvider.
To break the circle, don't inject auth as a dependency in the redirectProvider construction function. Use the $injector service to inject the auth service when it is needed.
$provide.factory('redirect', redirect);
$httpProvider.interceptors.push('redirect');
//function redirect($q, $injector, auth, store, $location) {
function redirect($q, $injector, store, $location) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
//INJECT HERE
var auth = $injector.get("auth");
auth.signout();
store.remove('profile');
store.remove('id_token');
$location.path('/home');
}
return $q.reject(rejection);
}
}
}

Load a view & controller after AUTH_REQUIRED error and route resolve has been rejected

In AngularJS (I'm a newbie) how could I 'resolve' a promise that has been rejected during routing?
My app requires all visitors to be authenticated either anonymously or by specifying their credentials -- I'm using Firebase and AngularFire.
Initially, when a visitor navigates to a route I check if they are authenticated with a resolve during routing using the $routeProvider.
If the visitor is not authenticated (the error === 'AUTH_REQUIRED') the promise is rejected. I then authenticate the user anonymously.
As the promise was rejected, the route's controller and view did not load.
Q: How can I load the route's controller and view after I have anonymously authenticated the visitor?
I imagine I could just somehow refresh/reload the route, but I would also imagine there is a better way.
So for an unauthenticated user who navigates to a route that requires authentication (almost all will):
Authentication is required; reject the promise
Authenticate the visitor anonymously
Anonymous authentication is successful; load the route's controller and view (and anything else) as would have happened, had the visitor already been authenticated
As I understand, I cannot resolve an already rejected promise.
I hope you can help; here is an example of what I am currently doing to try and achieve this:
var app = angular.module('vo2App', ['firebase', 'ngRoute']);
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeCtrl',
resolve: {
'currentAuth': ['Auth', function (Auth) {
return Auth.$requireAuth();
}]
},
templateUrl: './views/home.html'
})
}]);
app.run(['$rootScope', 'Auth', function ($rootScope, Auth) {
$rootScope.$on('$routeChangeError', function (event, next, prev, error) {
if (error === 'AUTH_REQUIRED') {
console.log('Unauthorized');
Auth.$authAnonymously().then(function (authData) {
console.log(authData);
}).catch(function(error) {
console.log(error);
});
}
});
}]);
app.controller('HomeCtrl', ['currentAuth', function (currentAuth) {
console.log(currentAuth);
}]);
app.factory('Auth', ['$firebaseAuth', function ($firebaseAuth) {
var ref = new Firebase('https://myvo2-mx.firebaseio.com');
return $firebaseAuth(ref);
}]);
Why doesn't Auth.$requireAuth(); call Auth.$authAnonymously() so that you can reject the promise only if both failed.
Basically,
var defer = $q.defer();
Auth.$requireAuth().then(defer.resolve,function(){
Auth.$authAnonymously().then(defer.resolve,defer.reject)
})
return defer.promise;

Redirecting to error page in Angularjs

I am new to AngularJs. I have a single page app with routes configured having a controller and a view. The view get loaded inside the <ng-view></ng-view> element of the index.html page. Inside the controller I am making a http call to get the data and binding the data to the $scope. For success scenarios this works fine but if there is an error how do I plug in another view instead of the default view configured inside the angular route. PLease let me know.
To implement common scenario for processing ajax errors you can implement custom request interceptor and redirect user to error page (or login page) according to error status:
myApp.factory('httpErrorResponseInterceptor', ['$q', '$location',
function($q, $location) {
return {
response: function(responseData) {
return responseData;
},
responseError: function error(response) {
switch (response.status) {
case 401:
$location.path('/login');
break;
case 404:
$location.path('/404');
break;
default:
$location.path('/error');
}
return $q.reject(response);
}
};
}
]);
//Http Intercpetor to check auth failures for xhr requests
myApp.config(['$httpProvider',
function($httpProvider) {
$httpProvider.interceptors.push('httpErrorResponseInterceptor');
}
]);
Plunker here
Use $location.url() to redirect to a 404.html when error is occured
$http.get(url,[params])
.success(function(data, status, headers, config){
// bind your data to scope
})
.error(function(data, status, headers, config) {
$location.url('/404');
});
Then configure your $routeProvider
$routeProvider
.when('/404', {
templateUrl: '404.html',
controller: 'Four04Controller'
})
you could use: https://github.com/angular-ui/ui-router
In case of error, you can trigger a "error" state.
I had the same problem some weeks ago and I have resolved in this way
If you use $stateProvider instead of $routeProvider you can do like this:
function routerConfig($stateProvider, $urlRouterProvider) {
$stateProvider
.state('404', {
url: '/404',
templateUrl: '404.html'
})
.state('home', {
url: '/',
templateUrl: 'home.html'
});
$urlRouterProvider.otherwise('/404');
}
Pay attention to $urlRouterProvider.otherwise(url), which is the function that gets called when the provider doesn't find the requested url, so it automatically redirect to the url provided in this function.

Angular template is showing before redirection

Im using Grails server side and Angular client side.
If the user is not allowed to access a page I redirect with Grails using response.sendError(404)
It is working but PrivateProfileController is calling before the redirection and it shows the template private.html for 1 seconds then redirecting to the template 404.html.
Is there a way to avoid displaying private.html before redirecting ?
Thank you
Grails controller's method
def privateProfile(){
if(NOT_AUTHORIZED){
return response.sendError(404)
}
}
Angular
app.config( ['$routeProvider', '$locationProvider',
'$tooltipProvider', '$compileProvider', '$httpProvider',
function($routeProvider, $locationProvider, $tooltipProvider,
$compileProvider, $httpProvider) {
$locationProvider.html5Mode(true) ;
$routeProvider
.when('/:username/private', {
templateUrl: '/js/angular/app/userprofile/templates/private.html',
controller: 'PrivateProfileController'
})
.when('/404', {
templateUrl: '/js/angular/app/error/404.html' ,
controller: 'HTTPErrorController'
})
.otherwise({
});
$httpProvider.responseInterceptors.push(
['$location', '$q', function ($location, $q) {
function success(response) {
return response;
}
function error(response) {
if (response.status === 404) {
$location.path('/404');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
return function (promise) {
return promise.then(success, error);
}
}]);
}
]).run(function ($rootScope) {
$rootScope.$on('$locationChangeStart',
function(evt, absNewUrl, absOldUrl) {
//console.log('Start : old', absOldUrl, 'new ',absNewUrl );
});
$rootScope.$on('$locationChangeSuccess',
function(evt, absNewUrl, absOldUrl) {
//console.log('Success : old', absOldUrl, 'new ', absNewUrl);
});
});;
app.controller("HTTPErrorController", function ($location) {
console.log("HTTPErrorController")
});
There are two components acting separately asynchronously:
rendering of the template based on the route
XHR request responding with the authorization status.
By the time the http response is intercepted (btw response interception is deprecated), the route completes its part by rendering content and bootstrapping the corresponding angular controller (PrivateProfileController). On receiving the response, the response gets intercepted and the routing is done to /404. There comes the latency of 1 sec. (The time it took to complete the call to Grails at server side plus interception)
What can be done is making the authorization call part of routeProvider's resolve for that particular route:
$routeProvider
.when('/:username/private', {
templateUrl: '/js/angular/app/userprofile/templates/private.html',
controller: 'PrivateProfileController',
resolve: {
authz: function($q){
//Create a promise
//Call the service (REST call to Grails)
//Get back 404
//Reject the promise if 404
//route to 404
//return promise
}
}
})
.when('/404', {
templateUrl: '/js/angular/app/error/404.html' ,
controller: 'HTTPErrorController'
})
.otherwise({
});
If routing to /404 is a problem inside resolve then, use $routeChangeError event which gets fired on rejected promise.
Refer this question answered by Misko himself and this post as well to see how resolve works.

AngularJS UI router handling 404s

I have an app with a service which wraps my API calls:
var ConcernService = {
...
get: function (items_url, objId) {
var defer = $q.defer();
$http({method: 'GET',
url: api_url + items_url + objId}).
success(function (data, status, headers, config) {
defer.resolve(data);
}).error(function (data, status, headers, config) {
console.log('ConcernService.get status',status);
defer.reject(status);
});
return defer.promise;
},
and I'm using UI-Router to transition between states:
concernsApp
.config( function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/404/");
$stateProvider.state('project', {
url: '/project/:projectId/',
resolve: {
project: function ($stateParams, ConcernService) {
return ConcernService.get('projects/', $stateParams.projectId);
},
},
views: {
...
}
});
I'm moving from using the normal AngularJS router and I'm having difficulty understanding how to implement 404s. I can see the ConcernService throwing the console.log status as rejected, but how do I catch this in the state router?
The otherwise() rule is only invoked when no other route matches. What you really want is to intercept the $stateChangeError event, which is what gets fired when something goes wrong in a state transition (for example, a resolve failing). You can read more about that in the state change event docs.
The simplest implementation for what you're trying to do would be something like this:
$rootScope.$on('$stateChangeError', function(event) {
$state.go('404');
});
Also, since $http itself is built on promises (which resolve resolves), your ConcernService method can be simplified down to a one-liner (I realize you expanded it for debugging purposes, but you could have just as easily chained it, just FYI):
var ConcernService = {
get: function (items_url, objId) {
return $http.get(api_url + items_url + objId);
}
}
I differ between two 404 states:
Server:
show 404 page depending on server response HTTP Code 404
important to define no URL, so that user stays on URL where the error happened
Client:
URL is not found by angular ui router (none of defined URLs)
Code for Angular UI-Router state:
$stateProvider
.state('404server', {
templateUrl: '/views/layouts/404.html'
})
.state('404client', {
url: '*path',
templateUrl: '/views/layouts/404.html'
});
Code in $httpProvider interceptor:
if(response.status === 404) {
$injector.get('$state').go('404server');
}
And why I used $injector instead of $state is explained here.
You can also try something like this and see if it works for you. You may need to adjust to your needs:
.state('otherwise', {
abstract: true,
templateUrl: 'views/404.html'
})
.state('otherwise.404', {
url: '*path',
templateUrl: 'views/404.html'
})
The $urlRouterProvider only works like a $watch to $location and if the actual URL matches one of the rules defined in the .config() function then it will redirect to the specified route.
Here's what I recommend, define "/404/" as a state:
$stateProvider.state('404', {
url:'/404/',
views:{
...
}
});
And inside the reject() function move to 404 state
if(status == '404'){
$state.transitionTo('404');
}
You will have to add ui-router as dependency of the project module and use the $state provider in your controller in order to be able to use $state.transitionTo()
Here's some info: https://github.com/angular-ui/ui-router/wiki/Quick-Reference#statetransitiontoto-toparams--options
I managed to handle 404 without using $urlRoutProvider since I'm only using states by testing $state.transistion:
angular.module("app", []).run(["$state", "$rootScope", function($state, $rootScope) => {
$rootScope.$on("$locationChangeSuccess", function() {
if (!$state.transition) {
$state.go("404");
}
});
}]);
$urlRouterProvider.otherwise('/page-not-found');
.state('error', {
url: "/page-not-found",
templateUrl: "templates/error.html",
controller: "errorController"
})
Will handle your page not found problem.
If you want to raise 404 found purposefully use the state or url. We have created a separate controller just if you want to perform any operations.

Resources