I'm trying to build Role-Permissions system that I want to initialize on root state resolve:
$stateProvider
.state('common', {
resolve:{
user: function(AclService, UserService) {
UserService.getCurrent().then((currentUser) => {
AclService.initialize(currentUser);
});
}
}
})
and check permissions each time on $stateChangeStart:
$rootScope.$on('$stateChangeStart', ($event, toState) => AclService.interceptStateChange($event, toState));
but I faced a problem that first $stateChangeStart was fired before resolve, so permissions were not initialized yet.
What would you recommend in such situation?
You could do that in your app's run function. Here is a trimmed down version of how I load auth data up front.
(function() {
"use strict";
angular
.module("myModule", [ //dependencies here...]);
angular
.module("myModule")
.run(run);
run.$inject = ["$rootScope", "$state", "authService"];
function run($rootScope, $state, authService) {
authService.fillAuthData(); //front load auth stuff here...
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
var isPublic = (toState.data && toState.data.isPublic && toState.data.isPublic === true);
var requiredRole = (toState.data && toState.data.requiredRole) ? toState.data.requiredRole : null;
var authorized = isPublic || authService.isUserInRole(requiredRole);
if (authService.authentication.isAuth || isPublic) {
//if the user doesn't have the requisite permission to view the page, redirect them to an unauthorized page
if (!authorized) {
event.preventDefault();
$state.go("unauthorized");
return;
}
} else {
event.preventDefault();
$state.go("login");
return;
}
});
}
})();
A state definition may look like this:
.state("someState", {
url: "/someState",
templateUrl: "my/folder/file.html",
data: {
pageTitle: "Some Page",
isPublic: false,
requiredRole: "Admin"
}
})
You shouldn't do some auth logic in state resolves. Better approach is to set listener for $stateChangeStart event in angular.run function:
angular.module('yourModule', [])
.run(['$rootScope', 'principal', '$state', function ($rootScope, principal, $state) {
var firstOpen = true;
$rootScope.$on('$stateChangeStart', function(event, toState, toParams) {
if (!principal.isAuthenticated() && firstOpen) {
firstOpen = false;
event.preventDefault();
principal.checkAuthentication().then(function() {
$state.go(toState, toParams);
});
} else if (principal.isAuthenticated() && toState.name === 'login') {
event.preventDefault();
// Do some stuff here, for example, redirect to main page
}
});
}
]);
Related
I have routes
.state('staff.dashboard', {
url: '/dashboard',
templateUrl: 'demo2.html',
})
.state('staff.users.institutions', {
url: '/institutions',
templateUrl: 'demo.html',
resolve: {
security: [
'$q', 'UserFactory', 'AuthenticationService',
function ($q, UserFactory, AuthenticationService) {
UserFactory.setData(AuthenticationService.getUserData());
if (!UserFactory.hasInstitutionsUsersAccess()) {
return $q.reject(App.errors[401]);
}
}
]
}
})
And on error I have
$rootScope.$on('$stateChangeError', function (e, toState, toParams, fromState, fromParams, error) {
if (error === App.errors[401]) {
$state.go('staff.dashboard');
}
});
So the scenerio is:
When I change URL /institutions before logging to app, it changes the URL to ?url=%2Fdashboard because of the security in Institutions state.
What I want is to keep the URL as ?url=%2Finstitutions after $q rejected in resolve.
Any help would be appreciable.
You can use your toState and/or fromState params :
$rootScope.$on('$stateChangeError', function (e, toState, toParams, fromState, fromParams, error) {
if (error === App.errors[401]) {
$state.go('staff.dashboard', {"url" : fromState.url});
}
});
Then you can access your url param in your dashboard :
app.controller('dashboardController', function($scope, $stateParams) {
var fromUrl = $stateParams.url;
});
I'm trying to use meteor angular js ui-router resolve to load information of one user selected from user list.
$stateProvider
.state('userprofile', {
url: '/user/:userId',
cache: false,
template: '<user-profile userinfo="$resolve.userinfo"></user-profile>',
controller: UserProfile,
controllerAs: name,
resolve: {
userinfo: function($stateParams) {
viewedUser = Meteor.users.findOne({
_id: $stateParams.userId
});
return viewedUser;
},
}
});
The problem is that, for the first time after from user list, user profile display correctly. However, page reload makes the userinfo becomes undefined.
I guest that from second time, the controller loaded already so that it display before resolve done?!
After a while searching, I tried $q and $timeout
resolve: {
userinfo: function($stateParams, $q, $timeout) {
deferred = $q.defer();
$timeout(function() {
deferred.resolve(Meteor.users.findOne({
_id: $stateParams.userId
}));
}, 1000);
return deferred.promise;
},
}
It works as I expected, user profile displayed every time I refresh the page.
But if I lower the delay to 500, it back to undefined when refreshed.
I not sure why in this case, longer delay works?
Thank you!
Here is the code that I use,
resolve: {
currentUser: ($q) => {
var deferred = $q.defer();
Meteor.autorun(function () {
if (!Meteor.loggingIn()) {
if (Meteor.user() == null) {
deferred.reject('AUTH_REQUIRED');
} else {
deferred.resolve(Meteor.user());
}
}
});
return deferred.promise;
}
}
This is from a tutorial by #urigo somewhere, which took me some time to find, but it works like a charm.
This code is handy to trap the case where authentication is required - put it at the top level in a .run method
function run($rootScope, $state) {
'ngInject';
$rootScope.$on('$stateChangeError',
(event, toState, toParams, fromState, fromParams, error) => {
console.log("$stateChangeError: "+error);
if (error === 'AUTH_REQUIRED') {
$state.go('login');
}
}
);
}
You can try this routes in resolve
if you use angular-meteor
resolve: {
'loginRequired': function ($meteor, $state) {
return $meteor.requireUser().then(function (user) {
if (user._id) {return true;}
}).catch(function () {
$state.go('login');
return false;
});
}
}
I am a newbee in angular and I would like to know how to make a AuthenticationService where I would check if user is authenticated or not. I have routes for which I want a user to be authenticated in order to be able to see them, and if they are not authenticated that they are redirected to login page. I am using satellizer for token based authentication.
This is my app.js
angular.module('coop', ['ionic', 'coop.controllers', 'coop.services', 'satellizer'])
.constant('ApiEndpoint', {
url: 'http://coop.app/api'
})
.run(function($ionicPlatform, $rootScope, $auth, $state, $location) {
// Check for login status when changing page URL
$rootScope.$on('$routeChangeStart', function (event, next) {
var currentRoute = next.$$route;
if (!currentRoute || currentRoute.requiresAuth && !AuthenticationService.authenticated) {
$location.path('/auth');
}
else if (!currentRoute || !currentRoute.requiresAuth && AuthenticationService.authenticated) {
$location.path('/front');
}
});
$rootScope.logout = function() {
$auth.logout().then(function() {
// Remove the authenticated user from local storage
localStorage.removeItem('user');
// Remove the current user info from rootscope
$rootScope.currentUser = null;
$state.go('main.auth');
});
}
$rootScope.token = localStorage.getItem('token');
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
cordova.plugins.Keyboard.disableScroll(true);
}
if (window.StatusBar) {
// org.apache.cordova.statusbar required
// StatusBar.styleDefault();
StatusBar.show();
StatusBar.overlaysWebView(false);
StatusBar.styleLightContent();
StatusBar.backgroundColorByHexString("#2a2e34");
}
});
})
.config(function($stateProvider, $urlRouterProvider, $authProvider, ApiEndpoint) {
$authProvider.loginUrl = ApiEndpoint.url + '/authenticate';
$stateProvider
.state('main', {
url: '/main',
abstract: true,
templateUrl: 'templates/main.html',
requiresAuth: true
})
.state('main.auth', {
url: '/auth',
views: {
'content': {
templateUrl: 'templates/login.html',
controller: 'AuthController',
requiresAuth: false
}
}
})
.state('main.front', {
url: '/front',
views: {
'content': {
templateUrl: 'templates/main-front.html',
controller: 'FrontPageController',
requiresAuth: true
}
}
})
.state('main.article', {
url: '/article/{id}',
views: {
'content': {
templateUrl: 'templates/main-article.html',
controller: 'ArticleController',
requiresAuth: true
}
}
});
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/main/front');
});
And my controllers:
angular.module('coop.controllers', [])
.controller('FrontPageController', function($scope, ArticleService, $state) {
ArticleService.all().then(function(data){
$scope.articles = data;
$scope.like = function(article){
article.like = article.like == 0 ? 1 : 0;
ArticleService.like(article.id, article.like)
};
})
})
.controller('ArticleController', function($scope, ArticleService, $stateParams, $ionicSlideBoxDelegate, $auth) {
ArticleService.get($stateParams.id).then(function(response) {
$scope.article = response;
$scope.commentsCount = response.comments.length;
$scope.articleText = response.text;
$scope.like = function(){
$scope.article.like = $scope.article.like == 0 ? 1 : 0;
ArticleService.like($scope.article.id, $scope.article.like)
};
$ionicSlideBoxDelegate.update();
})
})
.controller('AuthController', function($scope, $location, $stateParams, $ionicHistory, $http, $state, $auth, $rootScope) {
$scope.loginData = {}
$scope.loginError = false;
$scope.loginErrorText;
$scope.login = function() {
var credentials = {
email: $scope.loginData.email,
password: $scope.loginData.password
}
$auth.login(credentials).then(function(response) {
var token = JSON.stringify();
localStorage.setItem('token', response.data.token);
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('main.front');
}, function(){
$scope.loginError = true;
$scope.loginErrorText = error.data.error;
});
}
});
Updated code
I have changed the app.js as suggested:
// Check for login status when changing page URL
$rootScope.$on('$routeChangeStart', function (event, next) {
var currentRoute = next.$$route;
if (!currentRoute || currentRoute.requiresAuth && !$auth.isAuthenticated()) {
$location.path('/main/login');
}
else if (!currentRoute || !currentRoute.requiresAuth && $auth.isAuthenticated()) {
$location.path('/main/front');
}
});
And have added logout controller to delete user and token from localstorage, but I am still not being redirected to login page:
My controller:
.controller('AuthController', function($scope, $location, $stateParams, $ionicHistory, $http, $state, $auth, $rootScope) {
$scope.loginData = {}
$scope.loginError = false;
$scope.loginErrorText;
$scope.login = function() {
var credentials = {
email: $scope.loginData.email,
password: $scope.loginData.password
}
$auth.login(credentials).then(function(response) {
var token = JSON.stringify();
localStorage.setItem('token', response.data.token);
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('main.front');
}, function(){
$scope.loginError = true;
$scope.loginErrorText = error.data.error;
});
}
$scope.logout = function() {
$auth.logout().then(function() {
// Remove the authenticated user from local storage
localStorage.removeItem('user');
localStorage.removeItem('token');
// Remove the current user info from rootscope
$rootScope.currentUser = null;
$state.go('main.login');
});
}
});
If you are using satellizer, it already takes care of this for you.
Use the isAuthenticated() method of satelizer's $auth service instead of defining your own
$rootScope.$on('$routeChangeStart', function (event, next) {
var currentRoute = next.$$route;
if (!currentRoute || currentRoute.requiresAuth && !$auth.isAuthenticated()) {
$location.path('/auth');
}
else if (!currentRoute || !currentRoute.requiresAuth && $auth.isAuthenticated()) {
$location.path('/front');
}
});
Basically what $auth.isAuthenticated() does is checking if the user has a vaild jwt saved, and returns true or false.
The $routeChangeStart handler kicks in in every route change, checks if the route has requiresAuth set and if isAuthenticated returns true or false and acts accordingly.
If you want to do it on your own, here is a good tutorial on how to decode the token and check if it's valid:
https://thinkster.io/angularjs-jwt-auth
I've been working in a application that log a user. Once that the user is logged, the info of the user is stored in a service and a cookie is stored with the auth token.
Like this:
angular
.module('MyModule')
.service('AuthService', service);
service.$inject = ['$cookieStore', '$resource'];
function service($cookieStore, $resource){
var self = this,
user = null;
self.loginResource = $resource('my_path_to_login_resource');
self.login = login;
self.getUser = getUser;
self.reloadUser = reloadUser;
function login(_userCredentials){
var userResource = new self.loginResource(_userCredentials);
return userResource.$save()
.then(setUser);
}
function setUser(_userResponse){
user = _userResponse.toJSON();
}
function getUser(){
return user;
}
function reloadUser(_token){
return self.loginResource()
.get(_token)
.then(setUser);
}
}
Using ui-router when I need deal with the routes of the app I do this:
angular
.module('MyModule')
.run(runFn);
runFn.$inject = ['$state', '$rootScope', 'AuthService', '$cookieStore'];
function runFn($state, $rootScope, AuthService, $cookieStore) {
$rootScope.$on('$stateChangeSuccess', stateTransitioned);
function stateTransitioned(e, toState, toParams, fromState, fromParams) {
var pageRealoaded = fromState.name? false : true;
if(AuthService.getUser()){
//DEALING WITH STATES TRANSITIONS
}
else{
if($cookieStore.get('auth_token') && pageRealoaded){
//ENSURE THAT THE USER IS LOGGED WHEN THE STATE IS CHANGED
AuthService.reloadUser.then(function(){
$state.go(toState.name);
})
.catch($state.go.bind(null,'login'));
//I DON'T KNOW HOW AVOID THAT THE STATE IS LOADED UNTIL THE USER
//HAVE BEEN LOGGED
}
else{
$state.go('login');
}
}
}
}
When the page is reloaded, using the stored token, I try to waiting that user have been login, and then, if it's success, redirect to state toState.name, and if error, redirects to login.
My questions:
1. How to avoid that the state is loaded until the user have been login?
2. My architecture for dealing for this case are correct? Suggestions for better sctructure?
I would recommend you use the resolve functionallity of ui-router to to ensure that user is ALWAYS authenticated before entering a state.
See the ui-router wiki here:
https://github.com/angular-ui/ui-router/wiki#resolve
Something like this should prevent state entry before authentication.
.state('secure', {
resolve: {
user: function(AuthService, $state) {
// State will only be entered if this resolves.
return AuthService.resolveUser()
.catch(function(error) {
$state.go('login');
})
}
},
template: '<div>Secret Page</div>
})
I also threw together this pen with a working example:
http://codepen.io/marcus-r/pen/eZKbYV?editors=1010
Finally, for my first question, I create the following example that shows how I prevent that the state change until the user is logged successfully.
http://codepen.io/gpincheiraa/pen/zqLYZz
In this example, when i go to the "state3", i wait until the user is login, and then redirects to the state
angular
.module('exampleApp', ['ui.router']);
angular
.module('exampleApp')
.config(configFn);
configFn.$inject = ['$stateProvider', '$urlRouterProvider'];
function configFn($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('state1');
$stateProvider
.state('state1', {
url: '/state1',
controller: ['$state', function($state){
var vm = this;
vm.current = $state.current.name;
vm.goTwo = function(){$state.go('state2');};
}] ,
controllerAs: 'one',
template: '<h1>{{one.current}}</h1><button ng-click="one.goTwo()">Go to state 2</button>'
})
.state('state2', {
url: '/state2',
controller: ['$state', function($state){
var vm = this;
vm.current = $state.current.name;
vm.goThree = function(){$state.go('state3');};
}] ,
controllerAs: 'two',
template: '<h1>{{two.current}}</h1><button ng-click="two.goThree()">Go to state 3</button>'
})
.state('state3', {
url: '/state3',
controller: ['$state', function($state){
var vm = this;
vm.current = $state.current.name;
}] ,
controllerAs: 'three',
template: '<h1>{{three.current}}</h1>'
})
}
angular
.module('exampleApp')
.run(runFn);
runFn.$inject = ['$rootScope','$timeout','$state'];
function runFn($rootScope, $timeout, $state){
var logged = false;
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
console.log(toState.name);
if(toState.name === 'state3' && !logged){
//Fake like a user take a 1 second to login in server
$timeout(function(){
logged = true;
console.log('login ok');
$state.go(toState.name);
},1000)
console.log('while the user not loggin, prevent the state change');
event.preventDefault();
}
})
}
I have successfully implemented most of what is described here.
https://github.com/colthreepv/angular-login-example
My issue now is that the stateChangeStart event-handler in my login service does not seem to be registered yet when my grandfather resolve executes. The event handler never fires when I initially load the page, it only fires when I change state again after loading the first state. This makes me thing that my resolve is being executed before the stateChangeStart handler in my login-service has been registered. What can I do to make sure the event handlers has been registered when my root state resolve executes?
The global app route and the resolve:
.state('app', {
abstract: true,
templateUrl: 'vassets/partials/partial-nav.html',
resolve: {
'login': ['loginService','$q', '$http', function (loginService, $q, $http) {
loginService.pendingStateChange;
var roleDefined = $q.defer();
/**
* In case there is a pendingStateChange means the user requested a $state,
* but we don't know yet user's userRole.
*
* Calling resolvePendingState makes the loginService retrieve his userRole remotely.
*/
if (loginService.pendingStateChange) {
return loginService.resolvePendingState($http.get('/session'));
} else {
roleDefined.resolve();
}
return roleDefined.promise;
}]
}
})
My login-service looks like this (the handlers are setup by the managePermissions() call at the bottom of the service):
/*global define */
'use strict';
define(['angular'], function(angular) {
/* Services */
angular.module('myApp.services', [])
.provider('loginService', function () {
var userToken = localStorage.getItem('userToken'),
errorState = 'app.error',
logoutState = 'app.home';
this.$get = function ($rootScope, $http, $q, $state, AUTH_EVENTS) {
/**
* Low-level, private functions.
*/
var managePermissions = function () {
// Register routing function.
$rootScope.$on('$stateChangeStart', function (event, to, toParams, from, fromParams) {
if (wrappedService.userRole === null) {
wrappedService.doneLoading = false;
wrappedService.pendingStateChange = {
to: to,
toParams: toParams
};
return;
}
if (to.accessLevel === undefined || to.accessLevel.bitMask & wrappedService.userRole.bitMask) {
angular.noop(); // requested state can be transitioned to.
} else {
event.preventDefault();
$rootScope.$emit('$statePermissionError');
$state.go(errorState, { error: 'unauthorized' }, { location: false, inherit: false });
}
});
};
/**
* High level, public methods
*/
var wrappedService = {
loginHandler: function (data, status, headers, config) {
// update user
angular.extend(wrappedService.user, data.user);
wrappedService.isLogged = true;
wrappedService.userRole = data.user.roles[0].roleName;
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
$rootScope.currentUser = data.user;
return data.user;
},
loginUser: function (httpPromise) {
httpPromise.success(this.loginHandler);
},
resolvePendingState: function (httpPromise) {
var checkUser = $q.defer(),
self = this,
pendingState = self.pendingStateChange;
httpPromise.success(
function success(httpObj) {
if (!httpObj.user) {
getLoginData();
}
else {
self.loginHandler(httpObj);
}
}
);
httpPromise.then(
function success(httpObj) {
self.doneLoading = true;
if (pendingState.to.accessLevel === undefined || pendingState.to.accessLevel.bitMask & self.userRole.bitMask) {
checkUser.resolve();
} else {
checkUser.reject('unauthorized');
}
},
function reject(httpObj) {
checkUser.reject(httpObj.status.toString());
}
);
self.pendingStateChange = null;
return checkUser.promise;
},
/**
* Public properties
*/
userRole: null,
user: {},
isLogged: null,
pendingStateChange: null,
doneLoading: null
};
managePermissions();
return wrappedService;
};
})
});