Change the URL breaks the routing prevent - angularjs

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();
}
}
})

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

Modal Popups in AngularJS using angular-ui-router ($stateProvider) without updating the browser url

My criteria was:
Open a modal from anywhere in my app
Don't rewrite the url, leave it as is from before opening the modal
When pressing the back button on the browser don't remember modal states and reopen them
Don't include lots of options in the markup when ui-sref is being used
I'm using $stateProvider in my angularjs application to handle the routing - I love how easy it is to display a modal using ui-sref element attributes along with onEnter event in the .state function.
The issue I'm having is that I want to be able to display a modal over the top of any page in my application without it redirecting to the route for the modal popup.
Currently I'm using transitionTo to get back to the previous state before the modal was opened but it's messy and you can see the page changing behind the modal mask.
Does anyone know how to create true global modals using $stateProvider or will I have to run my own modal manager to deal with it. I already have this in place for Confirmation dialogues but it's a little messy as I have many ng-clicks throughout the markup and $scope.click = function in many controllers along with $on and $broadcast / $emit events - which I don't want (if I can help it).
UPDATE 15/10/15
When transitioning to the model states I also don't want the url to change / store the state in the browsers back history.
UPDATE 15/10/15 - 30 minutes after previous update
I managed to work it out - see answer below.
I worked it out.
I have the following state:
$stateProvider.state("default.mychannels.modalpopup", {
url: 'XXX',
data: { isModal: true },
onEnter: ['$stateParams', '$state', '$modal', function ($stateParams, $state, $modal) {
modal = $modal.open({
...
});
}],
onExit: function () {
modal.close();
}
});
As you can see, I'm setting a data property stating whether this state is a modal or not.
Then in the $stateChangeStart event I do the following:
$rootScope.$on('$stateChangeStart', function (event, to, toParams, from, fromParams) {
var isModal = false;
if (to.data != undefined) {
isModal = to.data.isModal == undefined ? false : to.data.isModal;
}
if (isModal) {
event.preventDefault();
$state.go(to, toParams, {
location: false, notify: false
});
} else {
var requiresAuth = to.data && to.data.requiresAuth;
var isAuthenticated = authenticationService.isLoggedIn();
if (requiresAuth) {
if (!isAuthenticated) {
event.preventDefault();
event.currentScope.loginConfirmedState = to.name;
event.currentScope.loginConfirmedStateParams = toParams;
// user is not logged in
$rootScope.$broadcast('event:auth-loginRequired');
}
} else {
$rootScope.$broadcast('event:auth-loginNotRequired');
}
}
$rootScope.previousState = from;
$rootScope.previousStateParams = fromParams;
});
The key part of this code snippet, apart from picking up the isModal data property, are the properties were are providing to the go function.
$state.go(to, toParams, {
location: false, notify: false
});
You need to set location to false within the $state.go options - this will not update the browser url (and therefore will not go into the browsers back history).
You also need to set notify to false within the $state.go options - this will prevent the $stateProvider being called again due to $state.go being called from within it. This will prevent an infinite loop.
Pretty simple, but I had a hard time finding a solution but eventually worked it out.

How to redirect in Angular if translate cookie is not set?

I want to redirect the user to a language page and not allow him to the index page if he hasn't chosen a language yet. I'm using the Angular Translate module. This module has cookie usage built-in with the following function:
$translateProvider.useCookieStorage();
This works. Now I would like to know if this cookie is set, and if not, redirect the user to the language page. Here's my idea how to handle this.
.factory('cookiecheck', function(){
if($translateProvider.CookieStorage.get) return true;
return false;
}
.run(function($rootScope, $state){
if(cookiecheck
{$location.path('/language');}
else
{$location.path('/home');}
This doesn't work. How would I best approach this? What is the correct syntax to determine if a CookieStorage exists and redirect?
You have a few syntax errors in your posted code, the factory isn't necessary as you can inject the $translate service into your run block.
.run(function($rootScope, $state, $location, $translate) {
var store = $translate.storage();
// if store is empty?
if(angular.equals(store, {}))){
$location.path('/language');
}
else {
$location.path('/home');
}
});
See ng-translate docs for cookie store
Also, since you won't know if and when the cookie will be expired or removed i think it is best to watch the route for changes and do your check then.
To do that hook into the $stateChangeStart event from ui router in your run block
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
// check then redirect
});
ui router docs
see this post for watching route changes

ui-router StateChange event go through even with redirection

I am developping an OAuth Provider application, using AngularJS and ui-router.
For each state change, I do the following check:
If the user is already logged in:
1.1 If the user is not an admin -> redirect him to the callBackUrl
1.2 If the user is an admin, do nothing
If the user is not logged in:
2.1 If the user tries to access an admin page -> redirect him back to login
2.2 If not, do nothing
my ui-router run method is the following:
.run(function ($rootScope, $state, $auth, accountService, $window, $stateParams) {
$rootScope.$on('$stateChangeStart', function (e, toState, toParams, fromState, fromParams) {
accountService.getUser({ token: $auth.getToken() }).$promise
.then(function (response) {
if (response.isAdmin === false) {
e.preventDefault();
$window.location.href = $stateParams.callBackUrl;
return;
}
})
.catch(function (response) {
if (toState.name.split(".")[0] === 'admin') {
e.preventDefault();
$state.go('root.login');
}
});
});
});
Everything is OK except for the part where I redirectthe user to the callback URL using $window
$window.location.href = $stateParams.callBackUrl;
This redirection takes 2-3 seconds, and in the meantime my user can see the page he is trying to access on my application. I thought the use of preventDefault() would solve that but it doesn't. Do you know how I could hold the $statechangeevent so that the user is redirected directly to the callback URL?
Thanks
I would put it this way:
The above approach allows user everything, until he is not really proven to be UN-authenticated. Why that? Because the code is calling service and evaluating all the stuff once the data are received. Meanwhile - we trust the user.
So, I'd suggest to change the approach
NEVER trust the user. He/she must to do the best to prove he/she is the right one, to get somewhere ... (well, kind of that...)
I described one possible way (with working example) here:
Confusing $locationChangeSuccess and $stateChangeStart
Just a piece of code to cite
Th first part of the $rootScope.$on('$stateChangeStart', ...:
// if already authenticated...
var isAuthenticated = userService.isAuthenticated();
// any public action is allowed
var isPublicAction = angular.isObject(toState.data)
&& toState.data.isPublic === true;
// here - user has already proved that he is the one
// or the target is public (e.g. login page)
// let him go, get out of this check
if (isPublicAction || isAuthenticated) {
return;
}
The second part, user is not trusted, he requires access to private stuff
// now - stop everything
// NO navigation
// we have to be sure who user is, to continue
// stop state change
event.preventDefault();
// async load user
userService
.getAuthObject()
.then(function (user) {
var isAuthenticated = user.isAuthenticated === true;
if (isAuthenticated) {
// let's continue, use is allowed
$state.go(toState, toParams)
return;
}
// log on / sign in...
$state.go("login");
})
Check that, in action, here
I know this was asked a while ago, but here is a possible solution:
Ui-router actually provides a fantastic way to solve this. By using a "resolve" within your $stateProvider, you can check that the user is authenticated before the controller of that particular state is even instantiated. Here is what ui-router docs say about resolve:
Resolve
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
https://github.com/angular-ui/ui-router/wiki - Section for resolve is almost half way down page
You can run accountService.getUser within the resolve to check for an authenticated user, and by doing so, would prevent someone who is not authed from seeing the view they are trying to route to.
The resolve is set up inside the $stateProvider, and may look something like this:
$stateProvider.state('myState', {
resolve: {
userAuth: function(accountService) {
return accountService.getUser();
}
}
)
If you notice in the above example, I set a property called userAuth within the resolve. This can now be injected into any controller or service and then you can check against it for authenticated users. Each state that needs to be a "protected" view can contain the resolve, and then the 2-3 second flash of the view won't occur, as the controller hasn't been instantiated, and the user is redirected to another state.

Resources