For some reason after adding:
.state('root', {
abstract: true,
url: '',
views: {
'layout': {
templateUrl: '../views/layout.html'
},
'header#root': {
templateUrl: '../views/shared/header.html'
},
'footer#root': {
templateUrl: '../views/shared/footer.html'
}
}
})
it stopped redirecting if the user is not logged in.
here is the full code:
angular
.module('authApp', ['ui.router', 'ui.bootstrap', 'ngAnimate', 'satellizer', 'smart-table'])
.config(function ($stateProvider, $urlRouterProvider, $authProvider, $httpProvider, $provide) {
function redirectWhenLoggedOut($q, $injector) {
return {
responseError: function (rejection) {
// Need to use $injector.get to bring in $state or else we get
// a circular dependency error
var $state = $injector.get('$state');
// Instead of checking for a status code of 400 which might be used
// for other reasons in Laravel, we check for the specific rejection
// reasons to tell us if we need to redirect to the login state
var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
// Loop through each rejection reason and redirect to the login
// state if one is encountered
angular.forEach(rejectionReasons, function (value, key) {
if (rejection.data.error === value) {
// If we get a rejection corresponding to one of the reasons
// in our array, we know we need to authenticate the user so
// we can remove the current user from local storage
localStorage.removeItem('user');
// Send the user to the auth state so they can login
$state.go('root.login');
}
});
return $q.reject(rejection);
}
}
}
// Setup for the $httpInterceptor
$provide.factory('redirectWhenLoggedOut', redirectWhenLoggedOut);
// Push the new factory onto the $http interceptor array
$httpProvider.interceptors.push('redirectWhenLoggedOut');
$authProvider.loginUrl = '/api/authenticate';
$urlRouterProvider.otherwise('/login');
$stateProvider
.state('root', {
abstract: true,
url: '',
views: {
'layout': {
templateUrl: '../views/layout.html'
},
'header#root': {
templateUrl: '../views/shared/header.html'
},
'footer#root': {
templateUrl: '../views/shared/footer.html'
}
}
})
.state('root.login', {
url: '/login',
templateUrl: '../views/authView.html',
controller: 'AuthController as auth'
})
.state('root.account', {
url: '/account',
templateUrl: '../views/account.html',
controller: 'UserController as user'
})
.state('root.dashboard', {
url: '/dashboard',
templateUrl: '../views/dashboard.html',
controller: 'UserController as user'
})
.state('root.report', {
url: '/report',
templateUrl: '../views/account.html',
controller: 'UserController as user'
});
})
.run(function ($rootScope, $state, $location) {
// $stateChangeStart is fired whenever the state changes. We can use some parameters
// such as toState to hook into details about the state as it is changing
$rootScope.$on('$stateChangeStart', function (event, toState) {
// Grab the user from local storage and parse it to an object
var user = JSON.parse(localStorage.getItem('user'));
// If there is any user data in local storage then the user is quite
// likely authenticated. If their token is expired, or if they are
// otherwise not actually authenticated, they will be redirected to
// the auth state because of the rejected request anyway
if (user) {
// The user's authenticated state gets flipped to
// true so we can now show parts of the UI that rely
// on the user being logged in
$rootScope.authenticated = true;
// Putting the user's data on $rootScope allows
// us to access it anywhere across the app. Here
// we are grabbing what is in local storage
$rootScope.currentUser = user;
// If the user is logged in and we hit the auth route we don't need
// to stay there and can send the user to the main state
if (toState.name === "root.login") {
// Preventing the default behavior allows us to use $state.go
// to change states
event.preventDefault();
// go to the "main" state which in our case is dashboard
$state.go('root.dashboard');
}
}
});
});
i've added :
else{
// go to the "login" state
$state.go("root.login");
event.preventDefault();
}
in
if (user) {}
Related
I'm trying to check permissions on routes in an AngularJS application. Routes are handled by angular-ui-router.
My routes and permissions-per-route are defined as this:
angular.module('my_app')
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('root', {
abstract: true,
templateUrl: 'content.html',
resolve: {
user_info: function (UserService) {
return UserService.user_info();
}
}
})
.state('root.some_page', {
url: '/',
templateUrl: 'some_page.html',
controller: 'home',
data: {
roles: ['some_right', 'some_other_right]
}
});
}
I then check for permissions as this:
[...].run(function ($rootScope, $state, UserService) {
$rootScope.$on('$stateChangeStart',
function (event, toState) {
if (!!toState.data) {
var required_roles = toState.data.roles;
if (!UserService.has_permission_in(required_roles)) {
event.preventDefault();
$state.go('root.access_forbidden');
}
}
});
});
All good for now. My issue is resolving user permissions. There are provided by a backend and fetched using $http.get(...). Since the $http service returns a promise, it don't always have the result when the $stateChangeStart is event triggered.
It's "good" when the application is "warm", but if I land on a protected page, it fails (the data are not ready). I have no way to "wait" for the promise.
What would be a "good" way of doing access control on routes?
In your protected state's definition:
.state('root.some_page', {
url: '/',
templateUrl: 'some_page.html',
controller: 'home',
resolve: {
access: function($http, $state){ //Note: you can inject your own services/factories too
var requiredPermissions = ['some_right', 'some_other_right'];
return $http.get(...).then(function(response){
var permissions = response.data.permissions;
var granted = permissions.reduce((prev,curr) => requiredPermissions.indexOf(curr) > -1 || prev, false);
if(!granted)
$state.go('publicState');
return permissions;
});
}
}
})
Resolve waits for promises to settle before moving to a new state.
Better:
function checkPermissions(requiredPermissions){
return function ($http, $state){ //Note: you can inject your own services/factories too
return $http.get(...).then(function(response){
var permissions = response.data.permissions;
var granted = permissions.reduce((prev,curr) => requiredPermissions.indexOf(curr) > -1 || prev, false);
if(!granted)
$state.go('publicState');
return permissions;
});
}
}
//In state definition
.state('root.some_page', {
url: '/',
templateUrl: 'some_page.html',
controller: 'home',
resolve: {
access: checkPermissions(['first','second'])
}
})
I'm trying to redirect the user to the login page if the user is not logged in. The only use case that I can't get to work is if the user points the browser to a URL that requires authentication.
If the user goes directly to http://.../index.html#/dashboard (requires authentication), the browser will attempt to load that page.
If the user first goes to http://.../index.html#/login and then replaces 'login' with 'dashboard', the app will correctly not allow that and redirect to the login page.
When the user goes directly to 'dashboard', I can SEE that the code hits the redirection logic, but it seems to just ignore it.
WAT?
Here's my ui-router config and code:
angular.module('app', [
'ngRoute',
'ngCookies',
'ui.router',
'datatables',
'datatables.bootstrap',
'angularMoment'
])
.config(config)
.run(run);
config.$inject = ['$stateProvider', '$urlRouterProvider', '$httpProvider'];
function config($stateProvider, $urlRouterProvider, $httpProvider) {
$stateProvider
.state('login', {
url: '/login?ac',
controller: 'LoginController as vm',
templateUrl: 'app/login/login.view.html'
})
.state('content', {
url: '/',
abstract: true,
views: {
// the main template will be placed here (relatively named)
'#': {
templateUrl: 'app/content-template/container-with-nav.partial.html'
},
'navigation#content': {
templateUrl: 'app/common/views/master-navigation.partial.html'
}
}
})
.state('content.dashboard', {
url: 'dashboard',
views: {
'': {
templateUrl: 'app/dashboard/dashboard.partial.html'
},
'glance#content.dashboard': {
templateUrl: 'app/dashboard/dashboard-overview.partial.html',
controller: 'DashGlanceController as vm'
},
'cases#content.dashboard': {
templateUrl: 'app/dashboard/dashboard-cases.partial.html',
controller: 'DashCasesController as vm'
}
}
})
.state('content.casedetails', {
url: 'case/:caseId',
views: {
'': {
templateUrl: 'app/pages/cases/case.main.partial.html',
controller: 'CaseController as vm'
},
'casedetails#content.casedetails': {
templateUrl: 'app/pages/cases/case.details.partial.html'
},
'citation#content.casedetails': {
templateUrl: 'app/pages/cases/case.citation.partial.html'
},
'payout#content.casedetails': {
templateUrl: 'app/pages/cases/case.payout.partial.html'
},
'conversation#content.casedetails': {
templateUrl: 'app/pages/cases/case.conversation.partial.html'
},
'actionpending#content.casedetails': {
templateUrl: 'app/pages/cases/case.action-pending.partial.html'
}
}
})
;
$urlRouterProvider.otherwise('/login');
$httpProvider.defaults.withCredentials = true;
}
run.$inject = ['$state', '$rootScope', '$location', '$cookies'];
function run($state, $rootScope, $location, $cookies) {
// keep user logged in after page refresh
$rootScope.globals = ($cookies.get('globals')) ? JSON.parse($cookies.get('globals')) : null;
console.log('globals (should be set if cookie was present): ', $rootScope.globals);
if ($rootScope.globals) {
console.log('Found a cookie for the logged in user: ', $rootScope.globals.currentUser);
}
$rootScope.$on('$locationChangeStart', function (event, next, current) {
var restrictedPage = $.inArray($location.path(), ['/login', '/register']) === -1;
var isUserLoggedIn = ($rootScope.globals) && ($rootScope.globals.currentUser) ? true : false;
console.log('restricted page: ', restrictedPage, ', logged in: ', isUserLoggedIn);
// redirect to login page if not logged in and trying to access a restricted page
if (restrictedPage && !isUserLoggedIn) {
// This actually gets entered!
console.log('Page is restricted and user is not logged in - redirecting to login from listener.');
$state.go('login', {ac: ''});
//$location.path('/login');
}
});
}
It will be because the config is redirecting the user to the new state before your run function has ran when going directly to the restricted state.
The reason is works when they go to login first is because you have already set your event listener and it doesn't get unbound when moving to the new state.
Basically, if you change the listener to the end of state change rather than the start event, it probably will work as the state change will not have ended.
$rootScope.$on('$locationChangeSuccess', function (event, next, current) {
I am using AngularJS with Meteor and wanted to redirect users with unverified emails to the sign in page. I have created a sign in view in /client/routes.js:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url:'/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
Note that there are other states I am not listing for brevity sake.
Now, I want to redirect users to this sign in page if their emails have not been verified. How do I modify the example below from UI-Router FAQs to meet my needs? Other solutions not using the example below are acceptable to me as long as they address the issue at hand.
Example: Uses the data object on the state config to define a rule
function that will run logic against the user (here using an example
service called $currentUser). The $stateChangeStart handler catches
all state transition and performs this rule check before allowing the
transition, potentially blocking it and/or redirecting to a different
state.
app.config(function($stateProvider) {
$stateProvider.state('privatePage', {
data: {
rule: function(user) {
// ...
}
});
});
app.run(function($rootScope, $state, $currentUser) {
$rootScope.$on('$stateChangeStart', function(e, to) {
if (!angular.isFunction(to.data.rule)) return;
var result = to.data.rule($currentUser);
if (result && result.to) {
e.preventDefault();
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(result.to, result.params, {notify: false});
}
});
});
The example from the FAQ attempts to create a general way to add a rule to any page. Let's keep it simple:
app.run(function($rootScope, $state, UserService) {
$rootScope.$on('$stateChangeStart', function(event, toState) {
// don't check auth on login routes
if (["signin"].indexOf(toState.name) === -1) {
if (UserService.doesNotHaveVerifiedEmail()) {
event.preventDefault();
$state.go('signin');
return;
}
}
}
});
Anytime a state is loaded and it's not the signin state, you check if the user is verified (depends on your application, here I am injecting a UserService which I assume has knowledge about the user's status) and if not, you prevent that state change and redirect them to the signin page.
You can use the resolve functionality provided by angular-ui-router to check email verification of current user before the state is resolved. Here's how the code will look like:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
var isVerified = ['User', '$state', '$q',
function(User, $state, $q) {
var d = $q.defer();
var loginPromise = User.getVerificationStatus();
loginPromise.then(
function(response) {
d.resolve(response);
},
function(error) {
$state.go('login');
});
return d.promise;
}
];
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url: '/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
.state('home', {
url: '/home',
views: {
main: {
templateUrl: 'client/views/profile/home.tpl'
}
},
resolve: {
verified: isVerified
}
});
}
]);
Here home state checks for the verification before resolving. I have injected a service User which will arrange the information whether user is verified or not.
You can add resolve property to only those states where you want to check verification status. In this way this is better than checking on $stateChangeStart event which will be fired every time state changes irrespective of whether this check is needed or not.
Here's the link to the documentation.
I would like to have two home pages, the first would be for users who have not logged in and the second for users that are logged in.
This is my current set up:
.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
$urlRouterProvider
.otherwise('/');
$locationProvider.html5Mode(true);
$httpProvider.interceptors.push('authInterceptor');
})
.factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
return {
// Add authorization token to headers
request: function (config) {
config.headers = config.headers || {};
if ($cookieStore.get('token')) {
config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
}
return config;
},
// Intercept 401s and redirect you to login
responseError: function(response) {
if(response.status === 401) {
$location.path('/login');
// remove any stale tokens
$cookieStore.remove('token');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
};
})
.run(function ($rootScope, $location, Auth) {
// Redirect to login if route requires auth and you're not logged in
$rootScope.$on('$stateChangeStart', function (event, next) {
if (next.authenticate && !Auth.isLoggedIn()) {
$location.path('/login');
}
});
});
.config(function ($stateProvider) {
$stateProvider
.state('main', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainCtrl',
title: 'Home',
mainClass: 'home',
headerSearch: true
});
});
How could I reconfigure this so I could do something like the following:
.config(function ($stateProvider) {
$stateProvider
.state('welcome', {
url: '/',
templateUrl: 'app/welcome/welcome.html',
controller: 'WelcomeCtrl',
title: 'Welcome',
mainClass: 'welcome',
isLoggedIn: false
});
$stateProvider
.state('main', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainCtrl',
title: 'Home',
mainClass: 'home',
isLoggedIn: true
});
});
Just wanted to show, how we can manage authentication driven access to states. Based on this answer and its plunker, we can enrich each state (which should be accessible only for authenticated users) with a data setting, explained here: Attach Custom Data to State Objects (cite:)
You can attach custom data to the state object (we recommend using a data property to avoid conflicts)...
So let's have some states with public access:
// SEE no explicit data defined
.state('public',{
url : '/public',
template : '<div>public</div>',
})
// the log-on screen
.state('login',{
url : '/login',
templateUrl : 'tpl.login.html',
controller : 'UserCtrl',
})
...
And some with private access:
// DATA is used - saying I need authentication
.state('some',{
url : '/some',
template : '<div>some</div>',
data : {requiresLogin : true }, // HERE
})
.state('other',{
url : '/other',
template : '<div>other</div>',
data : {requiresLogin : true }, // HERE
})
And this could be hooked on on the state change:
.run(['$rootScope', '$state', 'User', function($rootScope, $state, User)
{
$rootScope.$on('$stateChangeStart'
, function(event, toState, toParams, fromState, fromParams) {
var isAuthenticationRequired = toState.data
&& toState.data.requiresLogin
&& !User.isLoggedIn
;
if(isAuthenticationRequired)
{
event.preventDefault();
$state.go('login');
}
});
}])
See all that in action here
There is similar Q & A were I try to show the concept of redirecting Authenticated and Not Authenticated user:
Angular UI Router: nested states for home to differentiate logged in and logged out
maybe that could help to get some idea, how we can use ui-router, and its event '$stateChangeStart' to hook on our decision manager - and its forced redirecting...
the code should be something like this
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) { //, fromParams
console.log(toState.name + "," + fromState.name);
if(fromState.name === "") {
if (Auth.isLoggedIn()) {
$state.go('welcome');
event.preventDefault();
} else {
$state.go('home');
event.preventDefault();
} else {
if (toState.authenticate && !Auth.isLoggedIn()) {
$toState.go('login');
event.preventDefault();
}
}
}
so if user entering the application, then if he is logged in take him to welcome else take him to home.
once he is inside, then if he hits some route which needs auth.. then redirect him to login page..
sorry if i did not understood you requirement fully...
.config(function ($stateProvider,$rootScope) {
$stateProvider
.state('welcome', {
url: '/',
templateUrl: 'app/welcome/welcome.html',
controller: 'WelcomeCtrl',
onEnter: function() {
if (userIsLoggedIn()) {
$stateProvider.go('home');
}
});
});
I had problem like this and I solved it like this
.run(function ($rootScope, $location, AuthService) {
// start showing PRELOADER because we doing async call and we dont know when it will be resolved/rej
AuthService.checkLoginStatus().then(
(resp) => {
// fire logged in user event
$rootScope.$broadcast('userLogged',resp);
$location.path(YourHomePageUrl);
},
(err)=> {
// Check that the user is not authorized redirect to login page
$location.path(loginURL);
}
}).finally(
()=> {
// finnaly Set a watch on the $routeChangeStart
/// Hide PRELOADER
$rootScope.$on('$routeChangeStart',
function (evt, next, curr) {
if (!AuthService.isLoggedIn()) {
$location.path(art7.API.loginURL);
}
});
}
)
});
and im not using interceptor for handling 401 not authorized errors, thats my solution
My ui-router configuration is this:
$stateProvider
.state('app', {
url: '/app',
abstract: true,
templateUrl: 'sidemenu/sidemenu.html',
controller: 'SideMenuCtrl'
})
.state('login', {
url: '/login',
templateUrl: 'login/login.html',
controller: 'LoginCtrl'
})
.state('app.dashboard', {
url: '/dashboard',
views: {
menuContent: {
templateUrl: 'dashboard/dashboard.html',
controller: 'DashboardCtrl'
}
}
})
[More states here]
$urlRouterProvider.otherwise('/app/dashboard');
When user navigates to /app/dashboard I would like to check if they are authorized, and then:
redirect to /login if they are not authorized, or
allow to see the dashboard if they are authorized
This seems to do the trick: (based on this example)
$rootScope.$on('$stateChangeSuccess', function(event) {
var path = $location.path();
if (path === '/login') {
return;
}
// Halt state change from even starting
event.preventDefault();
Auth.getCurrentUser().then(function(currentUser) {
if (currentUser === null) {
$state.go('login');
} else {
// Continue with the update and state transition
$urlRouter.sync();
}
});
});
The only problem with this solution is that, if a non-authorized user navigates to the dashboard, the dashboard template is visible first. Only when the authorization response is coming back it changes to the login template.
Is there a way not to show the dashboard while we authorize the user?
I also tried to change $stateChangeSuccess to $stateChangeStart, but it didn't really work (routing seems to be completely broken then).
What about using a resolve-block?
.state('app.dashboard', {
url: '/dashboard',
views: {
menuContent: {
templateUrl: 'dashboard/dashboard.html',
controller: 'DashboardCtrl',
resolve: {
login: function($q, Auth) {
var deferred = $q.defer();
Auth.getCurrentUser().then(function(currentUser) {
if (currentUser == null) {
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}
}
}
}
})
The state-change won't happed untill the promise is resolved so the template should not be showing. Rejecting the promise will raise a $stateChangeError according to this answer so you would need to handle that error and redirect to the login-page.
You could use resolve on your routes and reject the promise when the user is not logged in, so the controller will never be reached.