urlRouterProvider.otherwise conflicting with $stateChangeStart - angularjs

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');

Related

Using $state.go inside $stateChangeStart causes Infinite loop

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.

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

Ionic ui-router onEnter check throw errors at 2nd time around

So I have this very strange error: I want to check if a user is login when enter a state and redirect them back to SignIn page if they are not. So in my config I have:
.state('home', {
cache: false,
abstract: true,
url: "/home",
templateUrl: "app/home/home.html",
onEnter: function($state, MyFirebaseService) {
// check session
var userId = MyFirebaseService.LoginUserId();
if (!userId) {
$state.go('auth.signin')
};
}
})
So I type in http://localhost:8100/#/home/courses to go into courses page without login, everything work perfectly. User got redirect back to auth.signin view. But when I type in the address bar again http://localhost:8100/#/home/courses, it throw 4 errors:
TypeError: Cannot read property '#' of null
TypeError: Cannot read property 'auth-signin#auth' of null
TypeError: Cannot read property 'auth-signup#auth' of null
My signin and signup are in an abstract view call auth. Why is that and how to fix it?
I personnaly perform those actions of authenticatio ncontrol on .run( with event catcher :
.run([
'$rootScope','authService','$q',
function($rootScope, authService,q) {
$rootScope.$on('$stateChangeStart', function(event, next, current) {
// YOUR CONTROL & REDIRECT
});
}]);
If you add some authorisation role control for instance on ui-router, for instance:
.state('home', {
url: "/home",
templateUrl: "includes/pages/homePage.html",
resolve : {
authorizedRoles : function(){ return [ USER_ROLES['su'],
USER_ROLES['user'],
USER_ROLES['admin'],
USER_ROLES['skyadmin'],
USER_ROLES['skyuser'],
USER_ROLES['skysu']
] }
}
})
You can easily check into $stateChangeStart with var authorizedRoles = next.resolve.authorizedRoles(); and compare them with your user roles

Route changes but view doesn't ui-router

I'm trying to redirect unauthorized people when they first enter my website via a url;
eg: example.com/#/order should be redirected to example.com/#/auth, this includes when they first visit the webpage and also navigating inbetween states.
Currently I have an abstract parent state of /order and /auth which have resolves that check for authentication and redirect otherwise. I also have a watch on the $stateChangeStart event to do the same thing.
The code for when you initially load the page works correctly, it will redirect if you visit /order/restaurant without being logged in, however if I'm on the url /auth/login I can change my url to /order/resturant and it will redirect me successful but the view will not update. I will still be able to see the /order/resturant page but the resolve and page changes were hit. Why does this happen? I've attempted to use $rootScope.$apply() without success as well.
My code is as follows for the parent states:
// Authentication Urls
.state('auth', {
url: '/auth',
templateUrl: 'modules/auth/auth.html',
abstract: true
})
// Order Urls
.state('order', {
url: '/order',
templateUrl: 'modules/order/order.html',
abstract: true
})
and my code to watch the stateChange
.run(['$rootScope', '$location', 'Auth', function($rootScope, $state, Auth) {
$rootScope.$on('$stateChangeStart', function(event, toState) {
var stateName = toState.name
console.log('State start')
if (!stateName.match(/auth/) && !Auth.isLoggedIn) {
console.log('User is not visiting auth and isn\'t logged in, redirecting....')
$state.go('auth.login')
} else if (Auth.isLoggedIn && stateName.match(/auth/)) {
console.log('User is logged in and is on the auth page, redirecting....')
$state.go('order.resturant')
}
})
}])
Looking at the documentation here (http://github.com/angular-ui/ui-router/wiki#state-change-events) you should cancel the navigation by calling event.preventDefault() before performing your new transition.

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