UI-Router state not changing as intended - javascript

I have an AngularJS app that uses UI-Router and has Authentication & Authorisation.
When changing $state I check authentication and authorisation and on failure I intend to redirect or request login.
$rootScope.$on('$stateChangeStart', function (event, toState) {
var requiresAuth = toState.data.requiresAuth;
if (requiresAuth) {
var authorizedRoles = toState.data.authorizedRoles;
if (!AuthService.isAuthorized(authorizedRoles)) {
event.preventDefault();
if (AuthService.isAuthenticated()) {
// user is not allowed
notifyError({ status: "Denied", statusText: "You don't have permission" });
} else {
// user is not logged in
$rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
}
$rootScope.$state.go('app.dashboard');
}
}
});
My problem is that when on state 'app.dashboard' and attempting to navigate to a state on which I don't have permission or am not authenticated I see the state change (breadcrumbs change on view) but then the $stateChangeStart code above kicks in and should send the state back to 'app.dashboard' and it does...
I can debug and see the state is indeed changed back to app.dashboard but because the state already was 'app.dashboard' a reload doesn't take place therefore my navigation ui-sref-active directives don't update and the view (breadcrumbs) doesn't update.
<li data-ui-sref-active="active">
<a data-ui-sref=" app.dashboard" title="Dashboard">
<i class="fa fa-lg fa-fw fa-home"></i> <span class="menu-item-parent"> Dashboard</span>
</a>
</li>
This all makes sense because AngularS is designed to prevent page reloads however I'm not sure how best to code around the problem. I've taken advice and code from this answer and it works but I'm a little concerned about issues this could introduce as I further develop the application.
.config(function($provide) {
$provide.decorator('$state', function($delegate)
{
$delegate.go = function(to, params, options)
{
return $delegate.transitionTo(to, params, angular.extend(
{
reload: true,
inherit: true,
relative: $delegate.$current
}, options));
};
return $delegate;
});
Ideally, I'd prefer to achieve what I want without changing the behaviour of UI-Router's $state provider. Has anyone got any ideas?

Related

Angular JS Authentication maintaince

I'm looking for the best architectural solution.
I have following html:
<body ng-controller="individualFootprintController as $ctrl">
<div ng-hide="$ctrl.authenticated">
<h1>Login</h1>
With Corporate Account: click here
</div>
And controller:
function individualFootprintController($http) {
var self = this;
$http.get("/is_auth").success(function () {
self.authenticated = true;
})
.error(function() {
self.authenticated = false;
}
);
}
Questions:
1) Is this controller appropriate place for having this logic?
2) I want to have actual "is_authenticated" value. How can make this happen, if I want to fire request only once
Presumably authentication with the backend requires an actual token of some kind. I.e. you don't just set a true/false flag somewhere and call it authentication, but to be able to communicate with the backend you need to include a username/password/cookie/token in the request or the request will be denied.
A controller is a bad place to store such a thing, since controllers aren't permanent. Or at least you shouldn't make them permanent to the extent possible. Also, storing the token in a controller doesn't allow anything else access to it.
Whether you are logged in or not should be based on whether you have a valid authentication token or not.
This token must be stored in a canonical place, best suited for that is a service.
Other services get the token from there and also decide whether the app is currently "logged in" based on whether a token is available.
A rough sketch of how that should be structured:
app.service('AuthService', function () {
this.token = null;
});
app.service('FooService', function (AuthService, $http) {
$http.get(..., AuthService.token, ...)
});
app.controller('LoginStatusController', function (AuthService) {
Object.defineProperty(this, 'isLoggedIn', {
get: function () { return AuthService.token != null; }
});
});
<div ng-controller="LoginStatusController as ctrl">
<div ng-hide="ctrl.isLoggedIn">
When you actually log in and obtain a token, you set AuthService.token and it will be available to all other services and controllers. If and when the token becomes invalid or unset, all services and controllers lose their authenticated status.
What I usually do is the following :
Using ui-router
Take advantage of the resolve hook (which resolves some args and inject them into controller), and define my routes as subroutes of a main one which checks auth on each route change
scripts/services/user.js
angular.module('yourmodule')
.service('userSrv', function ($http, $q) {
var srv = {};
srv.getAuthenticatedUser = function() {
return $http.get("/authenticated_user");
};
return srv;
});
scripts/routes.js
angular
.module('yourmodule')
.config(function ($stateProvider) {
$stateProvider
.state('authenticated', {
abstract: true,
template: '<ui-view />',
controller: 'AuthenticatedCtrl',
resolve: {
signedInUser: function(userSrv, $q) {
return userSrv.getAuthenticatedUser()
.then(function(null, function() {
//Catch any auth error, likely 403
//And transform it to null "signedInUser"
//This is the place to handle error (log, display flash)
return $q.resolve(null);
});
}
}
})
.state('authenticated.myspace', {
url: '/myspace',
templateUrl: 'views/myspace.html'
});
});
Take advantage of $scope inheritance inside your view
scripts/controllers/authenticated.js
angular.module('yourmodule')
.controller('AuthenticatedCtrl', function (signedInUser, $scope, $state) {
//Here you set current user to the scope
$scope.signedInUser= signedInUser;
$scope.logout = function() {
//this is where you would put your logout code.
//$state.go('login');
};
});
views/myspace.html
<!-- here you call parent state controller $scope property -->
<div ng-hide="signedInUser">
<h1>Login</h1>
With Corporate Account: click here
</div>
1) I want to have actual "is_authenticated" value. How can make this happen, if I want to fire request only once
My solution asks for authenticated user on each route change. This seems strange but this is actualy viable and fast. The query should not be > 30ms it's a very small SELECT under the hood. THOUGH, asking "am I authenticated" and "get the authenticated user" are pretty the same thing, except one return a boolean, and the other return the user. I suggest, like I just show you, to handle "am I authenticated" question by requesting the authenticated user, then booleaning it with "if(user)" (null value handling).
2) Is this controller appropriate place for having this logic?
Yes and no. As you can see, the controller is the very place to "set the user to the scope thing", but scope inheritance allow you to not repeat it for each route. Though, http api logic should be ported to a service, and routing event ("get the authenticated user for this page, please" is a routing event IMHO) should be set in a separate file.
NB: if you need complete route "protection" (like redirection on non-authenticated, ask another question and I'll be glad to answer it)

Avoid direct URL access in AngularJS

I have a web app which uses ui-router for routing. There is a user management module where there are certain roles given to a user (role x, y and z). When a user with role x logs in he will be restricted to certain states of the ui-router e.g. 'dashboard'.
When a user logs in I have saved the user's role in a cookie variable (using $cookie). I also used ng-hide to hide the Dashboard navigation sidebar when user with role x is logged in like this:
HTML:
<li ui-sref-active="active">
<a ui-sref="dashboard" ng-hide="roleIsX()">
<i class="fa fa-laptop"></i> <span class="nav-label">Dashboard</span>
</a>
</li>
Controller:
$scope.roleIsX = function() {
if ($cookies.get('loggedInUserRole') === "x")
return true;
else
return false;
}
This works okay for now but the problem is when any user logs in he is able to directly navigate to a URL by writing it in the address bar. Is there an easy way I could solve this issue keeping in view my situation?
I eventually used resolve of ui-router as #Shivi suggested. Following is the code if anyone is still looking up:
app.config(['$urlRouterProvider', '$stateProvider', '$locationProvider',
function($urlRouterProvider, $stateProvider, $locationProvider){
...
.state('user-management', {
...
...
resolve: {
roleAuthenticate: function($q, UserService, $state, $timeout) {
if (UserService.roleIsX()) {
// Resolve the promise successfully
return $q.when()
} else {
$timeout(function() {
// This code runs after the authentication promise has been rejected.
// Go to the dashboard page
$state.go('dashboard')
})
// Reject the authentication promise to prevent the state from loading
return $q.reject()
}
}
}
})
This code checks if the roleIsX then continue to the state else open dashboard state.
Helpful Reference: angular ui-router login authentication - Answer by #MK Safi
listen to the $locationChangeStart event, and prevent it if needed:
$scope.$on("$locationChangeStart", function(event, next, current) {
if (!$scope.roleIsX()) {
event.preventDefault();
}
});

"Re" resolve resources with angular ui router without reload

I am using the following code to resolve a resource when the main state is loaded. Is it possible to re - resolve the resource without reloading the page? I am avoiding reload so that the user experience is not affected.
$stateProvider
.state('main', {
url: '/',
templateUrl: 'publicApp/main.html',
controller: 'MainCtrl as mainCtrl',
resolve: {
userData: ["UserApi", function (UserApi) {
return UserApi.getUserData().$promise;
}]
}
})
.controller('MainCtrl', function (userData) {
console.log(userData.something);
})
Since is a public site, the user can visit any page without logging in but when the user logs in the page must customize based on the user data.
EDIT
I am using a modal for login so the state doesn't reload after logging in, I was thinking of throwing an event on $rootScope and then add listeners in controllers to load them again. But this doesn't look good, so I am looking for a better way
I currently have two options:
Reload page - will effect the user experience, so its the last option
Throw an event for the login modal and catch it in other controllers
Any better ideas?
Try using state reload once promise is resolved
$state.reload();

preventing users from navigating to some urls after login - angular-ui-router

currently i'm developing a sample admin application using angularjs, in my application. i have used ui-router instead of ngRoute to define the urls. on my config function states are defined as below.
sampleModule.config(['$stateProvider','$urlRouterProvider',function ($stateProvider,$urlRouterProvider) {
$urlRouterProvider.otherwise('login');
$stateProvider
.state('login',{
url:'/login',
templateUrl: 'views/html/login.html',
controller: 'authCtrl'
}).state('main', {
url:'/main',
templateUrl: 'views/html/main.html',
controller: 'adminViewCtrl'
});
}]);
in the run state of the application i'm redirecting the users to their respective view as follows.
sampleModule.run(function ($rootScope, AuthService, $state) {
$rootScope.$on("$stateChangeStart", function (event) {
if (AuthService.isAuthenticated()) {
event.preventDefault();
$state.go('main');
} else {
event.preventDefault();
$state.go('login');
}
});
});
but the problem i'm facing is application giving following exception and crashes some times when URL changes.
RangeError: Maximum call stack size exceeded
I put a debug point to run function of the application and
and noticed that error coming because content inside if (AuthService.isAuthenticated()) { condition executing in a infinite loop.
below is the image for the errors in chrome developer tools
i'm confused about the things happening here, i Googled for few hours and was not able to come up with a straight answer, i even tried putting event.preventDefault() as some suggestions i saw , but it didn't helped to resolve the problem.
i'm wondering what error i'm doing here? is there any better way to restrict user access to the log-in page,or some other parts of the application after logging in to the application?
The problem here is that you are constantly redirecting user to same state. Here is what happens: user goes to some state, $stateChangeStart is triggered, you redirect user again to some other state, and again $stateChangeStart is triggered, you check authentication and redirect again, and that continues until RangeError appears. What you should probably do is only redirect user when he is unauthorized, if he is authorized, let him see the page he is looking for.
sampleModule.run(function ($rootScope, AuthService, $state) {
$rootScope.$on("$stateChangeStart", function (event, toState) {
if (!AuthService.isAuthenticated() && toState.name !== 'login') {
$state.go('login');
} else if(AuthService.isAuthenticated() && toState.name === 'login') {
$state.go('main');
}
});
});
Here you check if user is authorized and if navigation state is not login (since he already got redirected) and later check if it's login page he is wanting for and redirecting to main

AngularJS [UI-Router] urlRouteProvider.when() versus resolve for routing to child states

In my app I have a state called 'dashboard' with multiple child states
.state('dashboard', {
url: '/dashboard',
abstract: true,
// resolve async objects before controller instantiation.
resolve: {
productResolve: function ($state, authService, cacheService, productService) {
var cache = cacheService.getCache();
if (cache.length === 0){
return productService.getProductsAndCommoditiesAsync(authService.getCredentials().roleID)
.then(function(productData){
cacheService.setCache(productData);
},
function(errorMessage){
$state.go('error',{errorMessage:errorMessage});
});
}
}
},
templateUrl: '/static/app/partials/dashboard.html',
controller: 'dashboardCtrl',
})
// parent state: dashboard
.state('dashboard.splash', {
templateUrl: '/static/app/partials/dashboard.splash.html',
controller: 'dashboard.splashCtrl'
})
// parent state: dashboard
.state('dashboard.podSelector', {
// params array can be used instead of url to populate $stateParams
params: ['productIndex', 'commodityIndex'],
templateUrl: '/static/app/partials/dashboard.pod-selector.html',
controller: 'dashboard.podSelectorCtrl'
Which child state the user will be redirected to depends on his/her session cookie, and I was curious where the appropriate place was to do the redirecting.
One option I can think of are either to set the parent dashboard state to be not abstract so it is able to be navigated to, then check the session from within the dashboard parent resolve promise, and do a $state.go to a child depending on the result. I ideally would like to keep the dashboard parent state abstract as its useless on its own, but if it always will redirect to a child state then I guess its not important if it is.
The alternative I was thinking of is to handle it with a $urlRouterProvider.when(), something like:
$urlRouterProvider.when('/dashboard', function($state, sessionService){
if (sessionService.getSession() === undefined) {
$state.go('dashboard.splash');
}
});
This seems cleaner and a more logical place to put it, but say if I'm transitioning to the dashboard state from a login state, if I redirect in the resolve I can transition from the login page with a $state.go('dashboard') while if I use $urlRouterProvider I need to do a $location.path('/dashboard'), which I don't know how much transitioning to states with location paths versus $state.go is frowned upon.
Any advice would be appreciated.
Which child state the user will be redirected to depends on his/her session cookie, and I was curious where the appropriate place was to do the redirecting.
In general, if you want to drive navigation based on app state or session state, I would suggest you use a service to intercept the $stateChangeStart event and send them where they should be.
So, in your app's module.run callback, you could say:
$rootScope.$on('$stateChangeStart', function(event, toState) {
if (toState.name === 'dashboard') {
event.preventDefault();
switch (myCondition) {
case 1:
$state.go('dashboard.splash');
break;
case 2:
$state.go('dashboard.podSelector');
break;
default:
$state.go('somewhereElse');
break;
}
}
});
This way, you don't let the transition take costly time and energy by going to a state that you never intend to stay on, and you don't rely on $urlRouter.when.

Categories

Resources