AngularJS: Promise in app.run() stuck in loop forever - javascript

I'm using app.run() in my AngularJS app to check whether a user is logged in before displaying the site to block access to various sites for non-registered users. I tried doing it with a promise because before, whenever I reloaded the page the isLoggedIn function would return false the getStatus hasn't returned the answer from the server yet.
Now using the promise, the site just calls itself in a loop forever, I guess because the process just repeats itself when the promise is resolved. Where am I going wrong and how could I fix this? Thanks in advance, help is much appreciated!
This is my code in app.js:
app.run(function($rootScope, $state, authService){
$rootScope.$on('$stateChangeStart', function(event, next, nextParams, from, fromParams){
event.preventDefault();
authService.getUserStatus().then(function(){
console.log(authService.isLoggedIn());
if(next.access.restricted && !authService.isLoggedIn()){
$state.go('index', {}, { reload: true });
} else {
$state.go(next, {}, { reload: true });
}
});
});
});
Here's the service authService.js:
(function(){
var app = angular.module('labelcms');
app.factory('authService', ['$q', '$timeout', '$http', function($q, $timeout, $http){
var user = null;
var isLoggedIn = function(){
if(user){
return true;
} else {
return false;
}
};
var getUserStatus = function(){
var deferred = $q.defer();
$http.get('/api/user/status')
.success(function(data){
if(data.status){
user = data.status;
deferred.resolve();
} else {
user = false;
deferred.resolve();
}
})
.error(function(data){
console.log('Error: ' + data);
user = false;
deferred.resolve();
});
return deferred.promise;
};
return ({
isLoggedIn: isLoggedIn,
getUserStatus: getUserStatus,
login: login,
logout: logout,
signup: signup
});
}]);
})();

It loops because every time you execute $state.go(next, {}, { reload: true }); it will hit your $rootScope.$on again.
I would check if we actually are on restricted route before you go into your security service.
app.run(function($rootScope, $state, authService){
$rootScope.$on('$stateChangeStart', function(event, next, nextParams, from, fromParams){
if(!next.access.restricted) return;
authService.getUserStatus().then(function(){
console.log(authService.isLoggedIn());
if(!authService.isLoggedIn()){
$state.go('index', {}, { reload: true });
});
});
});

Related

Use local storage to store AngularJS data

I am currently using $rootScope to store user information and whether or not the user is logged in. I have tried using $window.localStorage, but with no success. My goal is to have items in my navbar appear through an ng-show once a user is logged on, have their username appear in the navbar, individual user profile view, all users view, etc. I need a persistent login. I have the navbar working with $rootscope, but whenever I try and transition over to $window.localStorage, it fails. Here is the code using $rootScope:
mainModule
angular.module('mainModule', [
'ui.router',
...
])
.config(configFunction)
.run(['$rootScope', '$state', 'Auth', function($rootScope, $state, Auth) {
$rootScope.$on('$stateChangeStart', function(event, next) {
if (next.requireAuth && !Auth.getAuthStatus()) {
console.log('DENY');
event.preventDefault();
$state.go('login');
} else if (Auth.getAuthStatus() || !Auth.getAuthStatus()) {
console.log('ALLOW');
}
});
}]);
Auth Factory
angular.module('authModule').factory('Auth', ['$http', '$state', function authFactory($http, $state) {
var factory = {};
var loggedIn = false;
var userData = {};
factory.getAuthStatus = function() {
$http.get('/api/v1/auth')
.success(function(data) {
if (data.status == true) {
loggedIn = true;
} else {
loggedIn = false;
}
})
.error(function(error) {
console.log(error);
loggedIn = false;
});
return loggedIn;
}
return factory;
}]);
Login Controller
function SigninController($scope, $rootScope, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
$rootScope.loggedIn = true;
$rootScope.userData = data;
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
Nav Controller
function NavbarController($scope, Auth) {
$scope.loggedIn = Auth.getAuthStatus();
}
EDIT EDIT EDIT
Here is how I am using local storage. These are the only things that changed.
Login Controller
function SigninController($scope, $window, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
$window.localStorage.setItem('userData', angular.toJson(data));
$window.localStorage.setItem('loggedIn', true);
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
Auth Factory
angular
.module('authModule')
.factory('Auth', ['$http', '$window', '$state', function authFactory($http, $window, $state) {
var factory = {};
factory.getAuthStatus = function() {
$http.get('/api/v1/auth')
.success(function(data) {
if (data.status == true) {
$window.localStorage.setItem('loggedIn', true);
} else {
$window.localStorage.setItem('loggedIn', false);
}
})
.error(function(error) {
console.log(error);
$window.localStorage.setItem('loggedIn', false);
});
return $window.localStorage.getItem('loggedIn');
}
return factory;
}]);
I see a potential problem with your use of localStorage.getItem('loggedIn').
Because localStorage only stores strings, what you get back is actually a stringified version of the boolean that you put in. If the string 'false' gets returned, your check of !Auth.getAuthStatus() in main module for example will always evaluate to boolean false because any non-empty string in JavaScript is "truthy".
i.e. !'false' === false (the same as !true === false)
You can get over this by using JSON.parse on the value in localStorage. e.g. JSON.parse(localStorage.getItem('loggedIn')) would parse the string 'false' to the Boolean false.
Simply replace $window.localStorage with window.localStorage and you should be fine.
For example:
function SigninController($scope, $window, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
window.localStorage.setItem('userData', angular.toJson(data));
window.localStorage.setItem('loggedIn', true);
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
This being said, storing authenticated status in localStorage (or sessionStorage) is not a good path to go down. Both key/value pairs can be read in the developer pane and then altered (aka spoofed) via the console. A better solution is to return a unique value (GUID) after a successful login and store it in a cookie (set to expire in a short amount of time, like 20 minutes) that can be read on the server and verified there. You can and should use $cookie for this. Your user login state should be controlled server-side, never client-side. The client should always have to prove that it is authenticated.
To persist login, create a service that handles your visitor and let that service handle the login/logout and provide the proof of being logged in. That proof of being logged in should always be a private value that is held internally by the service and not accessible outside of it.
(function () {
'use strict';
var visitorModelService = ['$http', function ($http) {
var loggedIn = false,
visitorModel = {
login:function(){
//do login stuff with $http here
//set loggedIn to true upon success
},
loggedIn:function(){
return loggedIn;
},
logout:function(){
//do logout stuff with $http here
//no matter what, set loggedIn to false
}
};
return visitorModel;
}];
var module = angular.module('models.VisitorModel', []);
module.factory('VisitorModel', visitorModelService);
}());
Doing this, you can simply check for visitor.loggedIn in your ng-show and have everything centralized. Such as:
<a ng-click='visitor.logout' ng-show='visitor.loggedIn'>Log Out</a>
Better yet, put the elements that are only visible to authenticated users in a div tag and hide/show them en-mass.

Meteor collection in ui-router resolve

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;
});
}
}

Angular service unable to execute function

function loadUserCredentials() {
var token = new Object();
token.token = window.localStorage.getItem(LOCAL_TOKEN_KEY);
$http.post("http://localhost:8080/checkTokenValid",token).then(function(result){
console.log(result.data.success);
if(result.data.success){
useCredentials();
}
});
};
This loadCredentials() is loaded everytime the page refresh, I'm checking if it is authenticated at my angular.run
controlApp.run(function ($location, $rootScope,controlProvider){
$rootScope.$on('$routeChangeStart',function(event,next,nextParems){
if (!controlProvider.isAuthenticated()){
$location.path('/login');
}
})
});
I think this is running before loadCredentials complete, so I always get login page. Is there any way I can delay the angular.run? So that I can have my loadCredentials to check is the user having the valid token.
Yes you can do that by $timeout :
controlApp.run(function ($location, $rootScope, controlProvider, $timeout) {
$rootScope.$on('$routeChangeStart', function (event, next, nextParems) {
$timeout(function () {
if (!controlProvider.isAuthenticated()) {
$location.path('/login');
}
}, 500);
})
});

Angular: Dealing with reload page and User Authentication

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();
}
})
}

Wait for response in AngularJS $http

Okay, so I'm pretty sure I've found the answer multiple times - I just don't understand it.
I'm building a single page application which on each $routeChangeStart checks if the user is logged in. If the user isn't log in, redirect to login page.
My issue is that configService.isLoggedIn() is using $http, meaning it's asynchronous and the rest of the code won't wait for it to be resolved.
So the question in short: I need the isLoggedIn() function to be resolved before continuing with any other code.
I've seen a lot on $q.defer() but I can't wrap my head around it.
Thanks in advance.
app.service('configService', ['$http', '$q', '$location', '$rootScope', function ($http, $q, $location, $rootScope) {
var self = this;
this.isLoggedIn = function () {
$http.get('/internalAPI.php?fn=login').then(function (result) {
if (result.data.isLoggedIn === true) {
$rootScope.isLoggedIn = true;
}
else {
$rootScope.isLoggedIn = false;
}
}, function() {
$rootScope.isLoggedIn = false;
});
return $rootScope.isLoggedIn;
}
}]);
app.service('navigationService', ['$rootScope', '$location', '$timeout', 'configService', function ($rootScope, $location, $timeout, configService) {
var self = this;
$rootScope.$on('$routeChangeStart', function (event, next, current) {
if (configService.isLoggedIn() !== true) {
// no logged in user, redirect to /login
if (next.templateUrl != "resources/views/login.php") {
$location.path("/login");
$rootScope.subTitle = 'Login';
}
//user is logged in but is trying to view the login page, redirect
} else if (next.templateUrl == 'resources/views/login.php') {
$location.path('/');
}
You can just return promise from your function and operate on this promise object. According to Angular $http documentation the $http object is based on promise mechanism
app.service('configService', ['$http', function ($http) {
var self = this;
this.isLoggedIn = function () {
return $http.get('/internalAPI.php?fn=login');
}
}]);
app.service('navigationService', ['$rootScope', '$location', '$timeout', 'configService', function ($rootScope, $location, $timeout, configService) {
var self = this;
$rootScope.$on('$routeChangeStart', function (event, next, current) {
configService.isLoggedIn().then(function(result) {
if (result !== true) {
// no logged in user, redirect to /login
if (next.templateUrl != "resources/views/login.php") {
$location.path("/login");
$rootScope.subTitle = 'Login';
}
//user is logged in but is trying to view the login page, redirect
} else if (next.templateUrl == 'resources/views/login.php') {
$location.path('/');
}
}
}
}
Many thanks for that. Went for the approach Rene M. mentioned regarding building an interceptor and let the server-side scripts handle the authentication.
Definitely does the trick, if the 403 status is returned then redirect the user to the login page and update the isLoggedIn variable. Refactoring the code to remove use of the rootScope, was a dirty workaround until I got a hang of the whole angular way of authentication.
Attached a simple example below in case anyone would stumble upon this in the future.
app.config(['$routeProvider', '$locationProvider', '$httpProvider', function ($routeProvider, $locationProvider, $httpProvider) {
$httpProvider.interceptors.push('responseObserver');
app.factory('responseObserver', ['$location', '$q', function ($location, $q) {
return {
'responseError': function (errorResponse) {
switch (errorResponse.status) {
case 403:
//Place logic here
break;
case 500:
//Place logic here
break;
}
return $q.reject(errorResponse);
}
};
}]);

Categories

Resources