I'm not really sure why this is happening. I'm trying to listen to each state change in my Angular application and push the user back to login if they're not Auth. I'm storing their encrypted sessionID in a cookie, checking if that cookie is undefined and the redirecting the user as such.. or at least that's what I'm trying to do. I will be using the toState and toParams arguments down the road for user role's etc. so you can ignore those as part of this task for now.
Any feedback would be helpful. Thanks in advance!
.run(($rootScope, $state, $cookies) => {
$rootScope.$on('$stateChangeStart', (evt, toState, toParams) => {
if(!$cookies.get('SessionID')){
evt.preventDefault();
$state.go('login');
}
})
})
Since you're doing this on every state change, you've created an infinite loop. you might pretty this up a bit, but this should get you there:
.run(($rootScope, $state, $cookies) => {
$rootScope.$on('$stateChangeStart', (evt, toState, toParams) => {
if(!$cookies.get('SessionID'))
{
if (toState.name !== "login") {
evt.preventDefault();
$state.go('login');
}
}
})
})
The above will get you started, but as you move forward you'll find that there are other states that you want to allow access to without cookies or authentication as well. The best way to handle this is probably with a custom property that you define on each state. So when you define your states you would do something like:
.state('home', {url: '/home', templateUrl: 'views/home.html', authRequired: false})
.state('someSecureUrl', {url: '/someSecure.url', templateUrl: 'views/someSecureUrl.html', authRequired: true})
then modify the code above to be something like:
.run(($rootScope, $state, $cookies) => {
$rootScope.$on('$stateChangeStart', (evt, toState, toParams) => {
if(!$cookies.get('SessionID') && toState.authRequired)
{
evt.preventDefault();
$state.go('login');
}
})
})
You can also look at doing this using a resolve function on your states.
Related
.state('newProduct', {
url: '/products/new',
templateUrl: 'app/products/templates/product-new.html',
controller: 'ProductNewCtrl',
authenticate: 'cook,admin'
})
I'm trying to add different client routes based on role authentifications but if I try to add another role such as cook for example it won't trigger the page defined by the url for both of them. It will work separately tho if that makes more sense
authenticate: 'cook',
authenticate: 'admin'
is this a syntax error?
In your .state block, 'authenticate' is simply a data holder
You will need to do a manual check on the value of authenticate to handle permissions.
For example:
myApp.config(function($stateProvider, $urlRouterProvider){
$stateProvider
.state("newProduct", {
url: '/products/new',
templateUrl: 'app/products/templates/product-new.html',
controller: 'ProductNewCtrl',
authenticate: true
})
});
Where you require to check permissions, you you will need access the $state u want using the $state object.
for example in your controller inject the $state object and use:
if($state.get('newProduct').authenticate){ //if(true) in this case)
//do what u want
}
If you want to check permissions everytime u change state/screen heres an example for that too:
angular.module("myApp").run(function ($rootScope, $state, AuthService) {
$rootScope.$on("$stateChangeStart", function(event, toState,
toParams,fromState, fromParams){
if (toState.authenticate){
// User is authenticated
// do what u want
}
});
});
found the fix thanks to Alon indexOf it got me thinking :)
.run(function($rootScope, $state, Auth) {
// Redirect to login if route requires auth and the user is not logged in
// also if the user role doesn't match with the one in `next.authenticate`
$rootScope.$on('$stateChangeStart', function(event, next) {
if (next.authenticate) {
var loggedIn = Auth.isLoggedIn(function(role) {
if (role && next.authenticate.indexOf(role[0]) !== -1) {
console.log('works')
return; // logged in and roles matches
}
event.preventDefault();
if(role) {
// logged in but not have the privileges (roles mismatch)
console.log(next.authenticate.indexOf(role[0]));
$state.go('onlyAdmin');
} else {
// not logged in
$state.go('login');
}
});
}
});
});
I'm trying to create a service to check if a certain route needs a user to be logged in to access the page. I have a working code but I want to place the $scope.$on('routeChangeStart) function inside the service. I want to place it in a service because I want to use it in multiple controllers. How do I go about this?
Current code:
profileInfoCtrl.js
angular.module('lmsApp', ['ngRoute'])
.controller('profileInfoCtrl', ['$scope', '$location', ' 'pageAuth', function($scope, $location, pageAuth){
//I want to include this in canAccess function
$scope.$on('$routeChangeStart', function(event, next) {
pageAuth.canAccess(event, next);
});
}]);
pageAuth.js
angular.module('lmsApp')
.service('pageAuth', ['$location', function ($location) {
this.canAccess = function(event, next) {
var user = firebase.auth().currentUser;
//requireAuth is a custom route property
if (next.$$route.requireAuth && user == null ) {
event.preventDefault(); //prevents route change
alert("You must be logged in to access page!");
}
else {
console.log("allowed");
}
}
}]);
routes.js
angular.module('lmsApp')
.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider){
$routeProvider
.when('/admin', {
templateUrl: 'view/admin.html',
css: 'style/admin.css',
controller: 'adminCtrl',
requireAuth: true //custom property to prevent unauthenticated users
})
.otherwise({
redirectTo: '/'
});
}]);
By using $routeChangeStart, you are listening to a broadcast sent by $routeProvider on every change of the route. I don't think you need to call it in multiple places ( controllers ), just to check this.
In your service:
angular.module('lmsApp')
.service('pageAuth', ['$location', function ($location) {
var canAccess = function(event,next,current){
var user = firebase.auth().currentUser;
//requireAuth is a custom route property
if (next.$$route.requireAuth && user == null ) {
event.preventDefault(); //prevents route change
alert("You must be logged in to access page!");
}
else {
console.log("allowed");
}
}
$rootScope.$on('$routeChangeStart',canAccess);
}]);
And then inject your service in the .run() part of your application. This will ensure the check will be done automatically ( by the broadcast as mentioned earlier ).
In you config part :
angular.module('lmsApp')
.run(function runApp(pageAuth){
//rest of your stuff
});
You would add an event handler in the Service to $rootScope instead of $scope.
Also it would be much better if you would add the $routeChangeSuccess in the ".run" section so all the pages can be monitored from one point rather than adding it in every controller
You need to listen $rootScope instead of $scope.
And I think it's better to call that listener on the init of wrapped service, for instance (Services are singletons, so it will be run once you will inject it to any controller).
angular.module('lmsApp')
.service('stateService', ['$rootScope', function ($rootScope) {
$rootScope.$on('$locationChangeStart', (event, next, current) => {
// do your magic
});
}]);
I have a angular webapp that is using a pre-produced theme/framework called fuse (http://withinpixels.com/themes/fuse), this theme already has an app structure and code written to make the creation of apps easier.
We added some pages (which include sidemenu items), the problem is, when you tap one of the links in the sidebar, the whole page seems to be reloaded or at least a animate-slide-up is played 2 times on the index main div, I traced down one part of the problem to the configuration module of the page:
$stateProvider.state('app.pages_dashboard', {
url : '/dashboard',
views : {
'main#' : {
templateUrl: 'app/core/layouts/vertical-navigation.html',
controller : 'MainController as vm'
},
'content#app.pages_dashboard': {
templateUrl: 'app/main/dashboard/dashboard.html',
controller : 'DashboardController as vm'
},
'navigation#app.pages_dashboard': {
templateUrl: 'app/navigation/layouts/vertical-navigation/navigation.html',
controller : 'NavigationController as vm'
},
'toolbar#app.pages_dashboard': {
templateUrl: 'app/toolbar/layouts/vertical-navigation/toolbar.html',
controller : 'ToolbarController as vm'
},
},
bodyClass: 'login',
needAuth: true,
onStateChangeStart: function(event, state, auth, api) {
console.log('onStateChangeStart on DASHBOARD');
api.getUserCard.save({}, {}, function (response){
if (!response.result) {
state.go('app.pages_claimcard');
}
});
}
});
and the configuration module of the app
angular
.module('fuse')
.run(runBlock);
/** #ngInject */
function runBlock($rootScope, $timeout, $state, $auth, api)
{
console.log('INDEX.RUN loaded');
// Activate loading indicator
var stateChangeStartEvent = $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams)
{
// console.log('started change event');
// console.log(toState);
// console.log(fromState);
// check if authentication needed
if (toState.needAuth) {
// redirect to login page
if (!$auth.isAuthenticated()) {
event.preventDefault();
$state.go('app.pages_auth_login');
}
}
if (toState.onStateChangeStart) {
// THIS CAUSES ONE OF THE RELOADS
// toState.onStateChangeStart(event, $state, $auth, api);
}
$rootScope.loadingProgress = true;
});
// De-activate loading indicator
var stateChangeSuccessEvent = $rootScope.$on('$stateChangeSuccess', function ()
{
$timeout(function ()
{
$rootScope.loadingProgress = false;
});
});
// Store state in the root scope for easy access
$rootScope.state = $state;
// Cleanup
$rootScope.$on('$destroy', function ()
{
stateChangeStartEvent();
stateChangeSuccessEvent();
});
}
as you can see I commented the the toState OnStateChangeStart function, and that got rid of one the 'reloads' of the application, so basically have 2 questions:
Why does the onStateChangeStart function on the toState state causes the page to reload?
I have no idea what might be causing the other page reload, any ideas?
I found the problem, my states were defined as:
'app.pages.dashboard'
however there was never a declaration for
'app.pages'
so UI-router was trying its best to sort this mess, anyways, always remember to properly declare your states and everything should be fine.
According to documentation, you can use a custom data and listen for stateChanges to prevent accessing the state based on some condition. This works without a trouble but my use case is, when I'm entering a nested state I want all of the descendant states rules be enforced.
In other word, if I'm starting from green state and I want to end up being in blue state, I want the login rule gets evaluated. Currently, only color rule is applied. Is there any way to do this?
I understand that the data is overwritten on child state, and that's the reason that I only see the color log. But what is the way to extend data and not overwriting it?
here is the code:
var example = angular.module("example", ['ui.router']);
example.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("parent", {
url: "/parent",
templateUrl: "templates/parent.html",
data: {
rule: function () {
console.log("login rule checked");
return true;
}
}
})
.state("parent.child", {
url: "/child",
templateUrl: "templates/child.html",
data: {
rule: function () {
console.log("color rule checked");
return true;
}
}
});
});
example.run(function ($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if (!toState.data || !angular.isFunction(toState.data.rule)) return;
var result = toState.data.rule();
if (result) {
event.preventDefault();
$state.go(toState, result.params, {notify: false});
} else {
event.preventDefault();
}
})
});
I'm not sure if this is exactly what you want, but doing this:
$state.go(toState, result.params, {notify: false, reload:true});
will ensure that the entire state is reloaded including all parent states. It's not exactly what you want since it may reload more than you want if state R is not the top of the tree. However, this should work in the example you are showing.
For more information, see the ui-router API.
I have the following question... or situation. I have states defined in my AngularJS app, like so...
$stateProvider
.state('myApp', {
abstract: true,
template: '<ui-view/>'
})
.state('myApp.stateOne', {
url: 'state1',
templateUrl: '/an/views/state-1.html',
controller: 'StateOneCtrl'
})
.state('myApp.stateTwo', {
url: 'state2',
templateUrl: '/an/views/state-2.html'
controller: 'StateTwoCtrl'
})
.state('myApp.stateThree', {
url: 'state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl'
})
There are more states and I have changed the naming for this example, but suppose I need to check if the user is allowed to see / load 'mayApp.stateThree'. I can determine this by asking the backend. I have a service (in this example called IsAllowedService) to deal with this requests / provide the access and normally I would write the logic to do the check in the .run() block in my app.js file for example:
.run(['IsAllowedService', '$state', function (IsAllowedService, $state) {
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) {
// check if we are going to sfm.addContacts and if we are allowed to...
if (toState.name === 'myApp.stateThree') {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
}
});
}
});
}]);
This works well but doesn't wait until we get the result from the service so 'mayApp.stateThree' is loaded then we a redirected if necessary. So we get a quick flash of the page before we are redirected. I could put the same code into the 'StateThreeCtrl' but I still get the flash / FOUC. Would it be possible to resolve this when defining the states, I know this won't work but something like this...
.state('myApp.stateThree', {
url: '/an/state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl',
resolve: {
isAllowed : function () {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
return resp;
})
}
}
I realise that I wouldn't be able to inject the service (or even the $http service) but is it possible for me to somehow pause the loading of the view / controller of 'mayApp.stateThree' until I get the result from IsAllowedService.checkIfIsAllowed(). Any advice on how to structure my app / code would be appreciated. I have used ng-cloak in my HTML view but this did nothing!
Actually you're doing it almost right in the application's run block. Except you are not preventing anything. You can achieve that by adding:
event.preventDefault(); //Prevent from going to the page
Furthermore, you can add custom data to your $states , which will allow you to verify those conditions with your criteria. e.g.:
$stateProvider.state('home', {
controller: 'HomeController as home',
url: '/home',
templateUrl: 'home.html',
data: { roles: [ROLES.ANONYMOUS] }}); //This can be any condition
$stateProvider.state('user', {
controller: 'UserController as user',
url: '/user',
templateUrl: 'user.html',
data: { roles: [ROLES.ADMIN, ROLES.USER] }});
You can retrieve this custom data in the $stateChangeStart event:
$rootScope.$on('$stateChangeStart', function (event, next) {
if (!yourService.isAuthorized(next.data.roles)) {
event.preventDefault(); //Prevent from going to the page -> no flickering
$state.go('403'); //Or whatever is desired.
}
});
You see the flickering because you're using a Promise and the first page only gets redirected when the promise is furfilled. You can stop the flickering by preventing the default action, authorize and continue your flow as you desire when the promise resolves.
if (toState.name === 'myApp.stateThree') {
event.preventDefault(); //preventing the request.
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
} else { //he actually is allowed to go to state three.
$state.go('myApp.stateThree');
}
}, function() { //in case the server has no answer
$state.go('myApp.stateOne'); //you probably want to prevent it too
} );
In my opinion, if these conditions do not change during runtime, i.e. user role based, you can retrieve them upon user verification so you don't need a promise to begin with. Hope this helps.
I made a similar post before and added a working plunker.