Using $state.go inside $stateChangeStart causes Infinite loop - angularjs

I am using AngularJS ui-router. I am trying to implement protecting routes for unauthenticated user. I am checking if user is logged in on $stateChangeStart. If the user is not logged in then redirect to login state.
But when i am using $state.go("login") in stateChangeStart handler, the handler code goes in infinite loop and getting console error "RangeError: Maximum call stack size exceeded"
Below is my code:
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams) {
var allowedStates = ["signup","confirmaccount","resetPassword"];
if(!$window.localStorage.getItem('userInfo') && !(allowedStates.includes($state.current.name)))
{
$state.go("login");
}
}
);
And below is the screenshot of console error.

Prevent the default behavior and check for allowed state without using $state.current.name since toState is already a parameter to $stateChangeStart
Update
I think you need here a No State Change logic rather than redirecting to login always.
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams) {
var noChangeStates = ["login", "signup", "confirmaccount", "resetPassword"];
var noStateChange = noChangeStates.indexOf(toState.name) > -1;
if (noStateChange) {
return;
}
//Check for Allowed or Not Allowed logic here then redirect to Login
if (!$window.localStorage.getItem('userInfo')) {
event.preventDefault();
$state.go("login")
}
}
);
Please note, you should also add "login" to No state change

But when i am using $state.go("login") in stateChangeStart handler, the handler code goes in infinite loop and getting console error "RangeError: Maximum call stack size exceeded"
Looks like you always call $state.go("login");
You can check toState and fromState to avoid calling additional time $state.go("login");
Something like:
if(!$window.localStorage.getItem('userInfo')
&& !(allowedStates.includes($state.current.name))
&& fromState.name !== 'login'
){
event.preventDefault();
$state.go("login");
}

Using a stateChange event is not the best way to handle that. Actually, what it does:
You change state
Then you check for the authentication.
It would be better to check before changing the state. For this, you can use ui-router's resolve:
$stateProvider
.state('login', { // Login page: do not need an authentication
url: '/login',
templateUrl: 'login.html',
controller: 'loginCtrl',
})
.state('home', { // Authenticated users only (see resolve)
url: '/home',
templateUrl: 'home.html',
controller: 'homeCtrl',
resolve: { authenticate: authenticate }
});
function authenticate($q, user, $state, $timeout) {
if (user.isAuthenticated()) {
// Resolve the promise successfully
return $q.when()
} else {
// The next bit of code is asynchronously tricky.
$timeout(function() {
// This code runs after the authentication promise has been rejected.
// Go to the log-in page
$state.go('logInPage')
})
// Reject the authentication promise to prevent the state from loading
return $q.reject()
}
}
See also this answer.

Related

UI-Router doesn't get previous state on $stateChangeSuccess

I need to prevent users from going to certain states (dashboard and account) on the app if they have not completed all required enrollment steps. If a user tries to access those states by direct URL input, I will redirect them back to where they are. I am doing this simple check on my run block to redirect the user conditionally:
$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
let shouldPreventNavigationToAccountPages = fromState.name.includes('enroll') && toState.parent === 'layout';
if (shouldPreventNavigationToAccountPages) {
redirectUser();
}
}
So if a user comes from a "enroll" state (say "enroll.step-one") and tries to access a "layout" state they are not allowed to, they should be redirected. However, when I input the URL directly (no click on link), "fromState"returns the following object:
{name: "", url: "^", views: null, abstract: true}
I don't have access to the previous state and cannot perform the check on fromState.name.includes('enroll'). Is this a default behavior in UI-Router? Is there a way to get the previous state if user tries to access a page by inputing the URL directly on browser?
You are very close. All you were missing is:
$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
let shouldPreventNavigationToAccountPages = fromState.name.includes('enroll') && toState.parent === 'layout';
if (shouldPreventNavigationToAccountPages) {
//This
event.preventDefault();
redirectUser();
}
}
I need to prevent users from going to certain states (dashboard and account) on the app if they have not completed all required enrollment steps.
Prevent users from going to a state by rejecting a resolver.
myApp.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('dashboard', {
url: "/dashboard",
resolve: {
enrolled: function(enrollmentStatus) {
if (enrollmentStatus.enrolled) {
return "enrolled";
} else {
//throw to reject resolve
throw "not enrolled";
};
}
},
templateUrl: "partials/dashboard.html"
})
By tracking enrollment status with a service, state changes can be rejected without regard to the previous state.
UPDATE
The $stateChangeError event can be used to handle the rejection.
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
if (error && fromState.name=='') {
$state.go("enroll");
}
});
For more information, see UI Router $state API Reference -- $stateChangeError

Change the URL breaks the routing prevent

I have a requirement to prevent routing if it is first time login user, they have to stay in the setting page to reset password before do something else (or go to other pages), the code is 99% working now, I can change the url/ refresh page, it works fine but only one issue. Here is code:
.state('accountsetting.security', {
url: '/security/{isFirstTimeUser}',
templateUrl: 'settings/security/security.html',
params: {'isFirstTimeUser': null}
}) // this is where I define the route
// in the run block
.run(['$state','$rootScope',function($state,$rootScope) {
// var isFirstTimeUser = false;
// userinforservice.getUserInformation().then(function(data){
// isFirstTimeUser = data.isFirstTimeUser;
// });
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if(fromState.name!=="undefined" && toState.name=='firsttimeuser'){
$rootScope.isFirstTimeUser=true;
$state.go('accountsetting.security',{isFirstTimeUser:true});
event.preventDefault();
}else if((toParams.isFirstTimeUser || fromParams.isFirstTimeUser) && toState.name !='accountsetting.security'){
$state.go('accountsetting.security',{isFirstTimeUser:true});
event.preventDefault();
}
else{
return;
}
});
}]);
The url is like: https://localhost/app/#/account/security/true
As I mentioned, I can refresh the page or change the url like:https://localhost/app/#/account or https://localhost/app/#
they all work fine, but when I change the url like this:
https://localhost/app/ it will take me to the home page. I check console, in the statechangestart, I lost the isFirstTimeUser, it is undefind. any idea about it?
Thanks in advance.
You lose angular state when you go to the url of the root rather than the state url (i.e) #(hash) urls. The root url reloads the page wherein you lose memory of all javascript variables as they are all client side. Hence the variable is undefined.
State changes happen in a single instance of page load, the url changes give you a illusion as if a page load is happening
The issue causing this behaviour is described by Shintus answer.
A possible solution would be to make sure the event order is correctly resolved. I assume $stateChangeStart is fired before userinforservice.getUserInformation() is resolved. Instead of calling them in parallel you could query the returned promise inside your $stateChangeStart instead of the variable assigned at any undefined time.
.run(['$state','$rootScope',function($state,$rootScope) {
var storedUserPromise;
storedUserPromise = userinfoservice.getUserInformation();
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
storedUserPromise.then(function(data) {
if(data.isFirstTimeUser) {
//do redirection logic here
}
})
});
}]);
Storing the user promise allows you to only have the overhead of calling userinfoservice.getUserInformation() once. Afterwards any .then on the stored promise resolves instantly.
PS: you probably have a typo in userinfo>r<service ;)
You can intercept any route loading in your route definition with $routeProvider.when.resolve, check their status in the resolve block and redirect them or anything else you want to do.
Tutorial Example showing the below code snippet:
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
message: function(messageService){
return messageService.getMessage();
}
}
})

urlRouterProvider.otherwise conflicting with $stateChangeStart

I don't have a root / state. My root state is
$urlRouterProvider.otherwise('/dashboard');
and in $stateChangeStart if user is not logged in i redirect to login
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
var requireLogin = toState.data && toState.data.access!=undefined;
if (requireLogin && !Auth.isLoggedIn() {
$rootScope.redirectState = toState.name;//Auto redirect to the state
event.preventDefault();
$state.go('login');
return;
}
});
This works fine if I directly hit /dashboard but if i hit / it gets redirected to /dashboard which internally get redirected to /login till which is fine, but after that it gets redirected to /dashboard and /login again and again.
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
http://errors.angularjs.org/1.3.15/$rootScope/infdig?p0=10&p1=%5B%5D
at REGEX_STRING_REGEXP (angular.js:63)
at Scope.$get.Scope.$digest (angular.js:14346)
at Scope.$get.Scope.$apply (angular.js:14571)
at bootstrapApply (angular.js:1455)
at Object.invoke (angular.js:4203)
at doBootstrap (angular.js:1453)
at bootstrap (angular.js:1473)
at angularInit (angular.js:1367)
at HTMLDocument.<anonymous> (angular.js:26304)
at jQuery.Callbacks.fire (jquery.js:3099)
Any idea on how to solve this?
Solution here is to use one of these. I created plunkers for both, based on this Q & A: How can I fix 'Maximum call stack size exceeded' AngularJS
I. State 'dashboard' is public. Check it in this working plunker
.state('dashboard', {
url: "/dashboard",
templateUrl: 'tpl.html',
data: {
//access: true
},
})
II. Or redirect to 'login' - the other solution plunker
//$urlRouterProvider.otherwise('/dashboard');
$urlRouterProvider.otherwise('/login');
The point is, the otherwise should be really free of any limitations. It should be some public page ... if there is any... or directly the login. That's the point
In case that we need to
go to login if not authenticated
go to dashboard if authenticated
we can updated this event handler:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
var isGoingToLogin = toState.name === "login";
if (isGoingToLogin && Auth.isLoggedIn)
{
event.preventDefault();
$state.go('dashboard');
return;
}
if (isGoingToLogin || Auth.isLoggedIn)
{
return;
}
var requireLogin = toState.data && toState.data.access !== undefined;
if (requireLogin) {
$rootScope.redirectState = toState.name; //Auto redirect to the state
event.preventDefault();
$state.go('login');
return;
}
});
And again use two approaches:
I. State 'dashboard' is public. Check it in this working plunker
.state('dashboard', {
url: "/dashboard",
templateUrl: 'tpl.html',
data: {
//access: true
},
})
II. Or redirect to 'login' - the other solution plunker
//$urlRouterProvider.otherwise('/dashboard');
$urlRouterProvider.otherwise('/login');

Unable to access $state custom properties in $locationChangeStart with next and prev arguments

What i am trying to do
As mentioned in questions title, i want to check authorization and authentication of next routes so i have made custom properties on routes and trying to access them in $locationChageStart.
Problem i am facing is
the event , next , prev/current arguments are correct but custom properties are returning undefined
My code
myModule.run(function($rootScope,authService,$state){
$rootScope.$on("$locationChangeStart",function(e,next,prev){
console.log(e+","+next+","+prev); //correct
console.log(next.reqAuthentication); // returning undefined
console.log(prev.reqAuthentication); // returning undefined
});
})
and in Config routes are like
.state('profile.company', {
url: "/company",
templateUrl: 'partials/companyprofile.html',
controller: 'adminCtrl',
reqAuthentication:true,
authenticateRole:['a','b',"c"]
})
.state('profile.messages', {
url: "/messages",
templateUrl: 'partials/chat.html',
controller: 'adminCtrl',
reqAuthentication:true,
authenticateRole:['a','b',"b"]
})
Note: I am using ui routes so i think there is some problem i am mixing it up with ngRoute
You can use this:
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){ ... })
if you want to work with states.
More details you can find https://github.com/angular-ui/ui-router/wiki

How do I access the 'resolve' property of a UI-Router root state from $stateChangeStart?

I'm trying to implement a user auth check and access check system in my app, however I keep hitting roadblocks. I think I have it the correct way this time but I have one last hurdle.
A little background: I tried putting all of the code into the $rootScope.on($startChangeStart) and it worked, horribly... but it worked. The route was always redirected but due to the auth check on the backend it displayed the first request page for 1/2 a second and then the redirect page every time. Thus I tried 'pausing' page load by calling evt.preventDefault() right at the start of the $startChangeStart function, which worked, but trying to put the user back to the original route afterwards caused an infinite loop in the router.
So after more research and reading a lot of stack posts I'm certain that 'resolve:' is the proper place to put the auth check to ensure the page is not loading while it occurs, and then redirect the user if needed from the $startChangeStart. ($state and event are always undefined in my attempts to inject them into a resolve function) It seems like the winning combination.
My problem: I have the resolve on the root state in my app: 'main'
This was to avoid code redundancy, however I cannot determine how to access the root state's properties, and therefore the resolve result, from the $stateChangeStart function. The toState is the child state, while the fromState is either the previous state or an abstract state with the '^' route...
Do I have to put the resolve on every child state for this to work, or is there a way to access the root state from this point?
Basic app setup:
angular.module('App', ['ui.router', 'ui.bootstrap', 'ui.event', 'AngularGM', 'ngResource'])
.config(['$urlRouterProvider', '$stateProvider', function($urlRouterProvider, $stateProvider){
$urlRouterProvider
.when('/home', '/')
.when('', '/')
.when('/sign-up/joe', '/sign-up')
.otherwise('/');
$stateProvider
.state('main', {
url: '',
abstract: true,
templateUrl: 'views/main.html',
controller: 'MainCtrl',
resolve: {
checkAccess: ['accountService', function(accountService) {
accountService.checkAuth(function(){
accountService.checkAccess(function (access){
return access;
});
});
}]
}
})
.state('main.home', {
url: '',
abstract: true,
templateUrl: 'views/home.html',
controller: 'HomeCtrl'
})
.state('main.home.index', {
url: '/',
templateUrl: 'views/home/index.html'
});
.run(['$rootScope', '$state', '$stateParams', 'accountService', function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
console.dir(toState);
console.dir(toParams);
console.dir(fromState);
console.dir(fromParams);
if (!toState.checkAccess.allowed) {
event.preventDefault();
$state.transitionTo(toState.checkAccess.newState);
}
});
}]);
This is the output from the console.dir() calls on the two state objects:
Object
name: "main.home.index"
templateUrl: "views/home/index.html"
url: "/"
__proto__: Object
Object
controller: "PlacesCtrl"
name: "main.places.search"
templateUrl: "views/places.html"
url: "/places"
__proto__: Object
Update
Oops, forgot to mention AngularJS version is v1.2.0-rc.2
$state.current console.dir()
Object
abstract: true
name: ""
url: "^"
views: null
__proto__: Object
Yes, I believe you can access root state from the $stateChangeStart function.
When using pure AngularJS I normally use current.$$route
For example, using the following route
.when('/home', {
title:'Home',
bodyClass: 'meetings',
controler: 'HomeCtrl'
})
I can access the root state like so
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
if (current.$$route) {
$rootScope.title = current.$$route.title;
$rootScope.bodyClass = current.$$route.bodyClass;
}
});
Using ui-router it's just a bit different as it's called $state.current. And you can access all the properties associated to whatever route you hit (e.g: $state.current.url)
So on your code you could have something like this
.run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
console.log($state.current.url);
});
}]);
You need not to use resolve. Take a look at my solution:
app.run ($rootScope, $state, Auth, ngDialog) ->
$rootScope.$on '$stateChangeStart', (e, to) ->
if to.authRequired and not Auth.isAuthenticated()
e.preventDefault()
Auth.currentUser().then(
(user) ->
$state.go(to)
(failure) ->
$rootScope.storedState = to
$state.go('main')
ngDialog.closeAll()
ngDialog.open
template: 'modals/login.html'
controller: 'loginCtrl'
className: 'ngdialog-theme-default'
)
I use angular_devise and ngDialog but they are optional and you can implement it with your own user's service.
Is it possible to do the redirect from within your accountService? If you detect that the user fails your checkAuth or checkAccess functions, you could prevent the callback from executing and redirect the user to your error (or login) page.
Something else to consider is implementing some sort of variable/queue of states if you'd like to redirect someone to the login page to refresh their authorization/authentication and then return to the previous state.
If you initialize your state with a default, empty object on resolve, you'll be able to manipulate it within $stateChangeStart.
$stateProvider
.state 'home',
url: "/"
resolve: {}
...
$rootScope.$on '$stateChangeStart', (e, toState, toParams, fromState, fromParams) ->
toState.resolve.x = ->
$timeout ->
alert "done"
, 3000
See https://github.com/angular-ui/ui-router/issues/1165
This answer is very late but it can be useful.
Resolves of a state and $stateChangeStart event are executed at the same time. By the time you try to access resolved data in $stateChangeStart, it'll not be available but it'll be available when $stateChangeSuccess event fires.
If you use $stateChangeStart then you'll need to do checkAuth from two places $stateChangeStart event and main resolve. Since they have parallel execution, at least 2 network requests will be sent to server for the same data.
Instead use $stateChangeSuccess. Using this will ensure that your resolves are resolved and you can then check access. Also, instead of accessing resolved properties,access resolved data using angular service.

Resources