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();
}
});
Related
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)
A little back story.
I have public pages, not restricted
I have private pages, restricted that require login.
I am using Firebase Authentication for the login.
I have my $routeProvider set up in my .config() of my main .module() My restricted paths look like the following.
.when('/admin', {
controller: 'adminCtrl as admin',
templateUrl: 'pages/admin/dashboard.tpl.html',
resolve: {
"currentAuth": ["$firebaseAuth", function($firebaseAuth) {
//$requireAuth returns a promise if authenticated, rejects if not
var ref = new Firebase(fbUrl);
var authObj = $firebaseAuth(ref);
return authObj.$requireAuth();
}]
}
})
My question is about the resolve specifically, I am assuming it is working correctly because I am not seeing the templateUrl on my browser. (I know this for a fact, I removed the return and placed return true and my templateUrl appeared)
So right now, this promise in the resolve is getting rejected. Since I am not a logged in user.
My question is, how would I either a. send $routeProvider to my .otherwise route or b. place a redirect in the resolve if the promise is rejected.
Basically, If the user is not logged in, I want them sent back to the home page. Right now the browser is just showing a blank page.
After some more research (I realize now I needed to dig more, sorry won't happen again.)
I have found a solution by adding .run() to my application.
.run(function($rootScope, $location) {
$rootScope.$on("$routeChangeError", function(event, current, previous, eventObj) {
console.log(eventObj);
if (eventObj === 'AUTH_REQUIRED') {
$location.path("/");
}
});
})
This grabs my routeChangeError and redirects when I have the error of AUTH_REQUIRED.
Simple.
A redirect to the login page should happen if a user has not authenticated yet. $state.go("login"); should accomplish this, but it doesn't do anything.
// in app.js
$stateProvider
.state('login', {
template: 'login.html'
url: '/login',
controller: 'AuthenticationCtrl',
})
.state('special', {
resolve: { loginRequired: checkAuthenticated },
template: 'special.html',
url: '/special',
controller: 'SpecialCtrl',
})
var checkAuthenticated = function($state, AuthenticationService, $q, $timeout) {
var deferred = $q.defer();
if(AuthenticationService.isAuthenticated()) {
deferred.resolve();
} else {
deferred.reject();
alert('You are being redirected to login page');
$state.go("login"); // redirect to login page -- but this does nothing.
}
}
Here, if a user has not authenticated and tries to access a route that requires authentication, I see my alert pop up. But nothing happens afterwards; ui-router doesn't redirect to my login page.
I also tried out $location.path('/login') instead of $state.go("login"), but this also does not do anything.
I checked the documentation it says for go():
String Absolute State Name or Relative State Path
Which I have, so I must be missing something that's causing this problem, but I'm not sure what.
I've experienced issues with state go in conjunction with an alert, I solved this by using a $timeout set to 100ms or so, eg: delay your state.go and hopefully this will solve your issue.
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?
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