I'm using this tutorial to figure out my authentication system for a web app that I am working on. I'm using ui-router's StateProvider and resolve system to reroute the user to the home page if they attempt to access one of the pages that needs authentication. Everything seems to be working, except that the resolve part doesn't seem to be actually working - i.e. my authenticate returns a rejected promise, yet the page loads like normal, despite the fact that there should be some sort of error because of this. What am I doing wrong?
app.states.js
angular
.module('app')
.config(routeConfig);
/** #ngInject */
function routeConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
// checks if user is logged in or not
// passes back rejected promise if not, resolved promise if true
function authenticated(authFactory, $q) {
var deferred = $q.defer();
authFactory.authenticate()
.then(function(authenticate) {
if (authenticate.data === 'true') {
deferred.resolve();
} else {
deferred.reject();
}
});
return deferred.promise;
}
// every new state that should include a sidebar must have it as a view
$stateProvider
.state('dashboard', {
url: '/dashboard/',
views: {
'sidebar': {
templateUrl: 'app/components/navbar/sidebar.html',
controller: 'SidebarController as vm'
},
'content': {
templateUrl: 'app/components/authenticated/dashboard.html',
controller: 'DashboardController as vm'
}
},
resolve: {
authenticated: authenticated
}
})
app.run.js
function runBlock($rootScope, $log, $state) {
$rootScope.$on('$stateChangeError', function () {
// Redirect user to forbidden page
$state.go('forbidden');
});
}
auth.factory.js
'use strict';
angular
.module('app')
.factory('authFactory', authFactory);
authFactory.$inject = ['$http', '$cookies'];
function authFactory($http, $cookies) {
var _token;
var service = {
authenticate: authenticate
};
return service;
// used to prevent user from accessing pages that they shouldn't have access to
// this is used exclusively in app.routes.js/app.states.js
function authenticate() {
// gets user's token from cookies, if no cookie, _token will be blank and server will return 403
// this part might be redundant with other functions, but I left it in for now just to make sure
if ($cookies.getObject('user')) {
_token = $cookies.getObject('user').token;
} else {
_token = '';
}
var request = $http({
method: 'POST',
url: 'http://localhost:8080/checkToken',
headers: {'x-auth-token': _token},
transformResponse: function(data) {
return data;
}
});
return request;
}
}
You need to place return deferred.promise outside then function, so that promise will get returned properly.
Code
function authenticated(authFactory, $q, $log) {
var deferred = $q.defer();
authFactory.authenticate()
.then(function(authenticate) {
if (authenticate.data === 'true') {
deferred.resolve();
} else {
deferred.reject();
}
});
return deferred.promise; //placed outside function
}
Related
Right now i am trying to make an Angular JS install application, to install a CMS. So i am trying to block access to a state (ui router), i am doing it with a resolve function. But the problem is, that i make a get request to an API, which returns true or false, and the resolve function do not wait for the get request to complete, so it just loads the state.
Here is my code:
app.run(['$rootScope', '$http', function($rootScope, $http) {
$rootScope.$on('$stateChangeStart', function() {
$http.get('/api/v1/getSetupStatus').success(function(res) {
$rootScope.setupdb = res.db_setup;
$rootScope.setupuser = res.user_setup;
});
});
}]);
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/404");
$stateProvider.state('db-install', {
url: "/install/db",
templateUrl: 'admin/js/partials/db-install.html',
controller: 'DBController',
resolve: {
data: function($q, $state, $timeout, $rootScope) {
var setupStatus = $rootScope.setupdb;
var deferred = $q.defer();
$timeout(function() {
if (setupStatus === true) {
$state.go('setup-done');
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}
}
})
.state('user-registration', {
url: "/install/user-registration",
templateUrl: "admin/js/partials/user-registration.html",
controller: "RegisterController"
})
.state('setup-done', {
url: "/install/setup-done",
templateUrl: "admin/js/partials/setup-done.html"
})
.state('404', {
url: "/404",
templateUrl: "admin/js/partials/404.html"
});
}]);
Here you can see a timeline for the loading of the page:
Here you can see what the API returns:
Your db-install resolver function needs to chain from the $http.get for install status.
$stateProvider.state('db-install', {
url: "/install/db",
templateUrl: 'admin/js/partials/db-install.html',
controller: 'DBController',
resolve: {
data: function($state, $http) {
return $http.get('/api/v1/getSetupStatus'
).then (function(result) {
var setupdb = result.data.db_setup;
var user_setup = result.data.user_setup;
//return for chaining
return setupdb;
}).then (function (setupStatus) {
//use chained value
if (setupStatus === true {
//chain with $state.go promise
return $state.go('setup-done');
} else {
//resolve promise chain
return 'setup-not-done';
};
})
}
}
})
By returning and chaining from the status $http.get, the resolver function waits before executing (or not executing) the $state.go.
For more information on chaining promises, see the AngularJS $q Service API Reference -- chaining promises.
The call to getSetupStatus gets executed in the $stateChangeStart so resolve is not aware that it has to wait. You can put the $http call inside of the resolve function, like this:
function($q, $state, $timeout) {
return $http.get('/api/v1/getSetupStatus')
.then(function(res) {
if(res.db_setup) {
$state.go('setup-done');
}
else {
return true;
}
});
}
By making the resolve parameter return a callback the state will load after the promise is resolved.
I'm trying to build some sort of authentication in my angular app and would like to redirect to a external URL when a user is not logged in (based on a $http.get).
Somehow I end up in an infinite loop when the event.preventDefault() is the first line in the $stateChangeStart.
I've seen multiple issues with answers on stackoverflow, saying like "place the event.preventDefault() just before the state.go in the else". But then the controllers are fired and the page is already shown before the promise is returned.
Even when I put the event.preventDefault() in the else, something odd happens:
Going to the root URL, it automatically adds the /#/ after the URL and $stateChangeStart is fired multiple times.
app.js run part:
.run(['$rootScope', '$window', '$state', 'authentication', function ($rootScope, $window, $state, authentication) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
event.preventDefault();
authentication.identity()
.then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
return;
} else {
$state.go(toState, toParams);
}
});
});
}]);
authentication.factory.js identity() function:
function getIdentity() {
if (_identity) {
_authenticated = true;
deferred.resolve(_identity);
return deferred.promise;
}
return $http.get('URL')
.then(function (identity) {
_authenticated = true;
_identity = identity;
return _identity;
}, function () {
_authenticated = false;
});
}
EDIT: Added the states:
$stateProvider
.state('site', {
url: '',
abstract: true,
views: {
'feeds': {
templateUrl: 'partials/feeds.html',
controller: 'userFeedsController as userFeedsCtrl'
}
},
resolve: ['$window', 'authentication', function ($window, authentication) {
authentication.identity()
.then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
}
})
}]
})
.state('site.start', {
url: '/',
views: {
'container#': {
templateUrl: 'partials/start.html'
}
}
})
.state('site.itemList', {
url: '/feed/{feedId}',
views: {
'container#': {
templateUrl: 'partials/item-list.html',
controller: 'itemListController as itemListCtrl'
}
}
})
.state('site.itemDetails', {
url: '/items/{itemId}',
views: {
'container#': {
templateUrl: 'partials/item-details.html',
controller: 'itemsController as itemsCtrl'
}
}
})
}])
If you need more info, or more pieces of code from the app.js let me know !
$stateChangeStart will not wait for your promise to be resolved before exiting. The only way to make the state wait for a promise is to use resolve within the state's options.
.config(function($stateProvider) {
$stateProvider.state('home', {
url: '/',
resolve: {
auth: function($window, authentication) {
return authentication.identity().then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
}
});
}
}
});
});
By returning a promise from the function, ui-router won't initialize the state until that promise is resolved.
If you have other or children states that need to wait for this, you'll need to inject auth in.
From the wiki:
The resolve keys MUST be injected into the child states if you want to wait for the promises to be resolved before instantiating the children.
I have an angular project set up that when a user goes to /cloud and they aren't logged in (isLoggedIn() return fails), it redirect to /login. And then if they are logged in and go to /login it redirects them back to /cloud.
However, when I click the logout button to clear the local storage, I get the following error (everything still works):
Error: null is not an object (evaluating 'a.$current.locals[l]')
According to my logs, it appears to be happening on the onEnter for my logout controller.
I'm new to angular so any advice or guidance would be greatly appreciated.
var routerApp = angular.module('myapp', ['ui.router'])
//config for state changes
.factory('Auth', function($http, $state, $q) {
var factory = { isLoggedIn: isLoggedIn };
return factory;
function isLoggedIn(token) {
return $http.post('/auth/session', {token:token});
}
})
.config(function($stateProvider, $urlRouterProvider, $locationProvider) {
$locationProvider.html5Mode(true);
$urlRouterProvider.otherwise('/cloud');
var authenticated = ['$q', 'Auth', '$rootScope', function ($q, Auth, $rootScope) {
var deferred = $q.defer();
if (typeof window.localStorage['authtoken'] === 'undefined') {
var authtoken = undefined;
} else {
var authtoken = window.localStorage['authtoken'];
}
Auth.isLoggedIn(authtoken).then(function() {
deferred.resolve();
}, function() {
deferred.reject();
});
return deferred.promise;
}];
var authGuest = ['$q', 'Auth', '$rootScope', function ($q, Auth, $rootScope) {
var deferred = $q.defer();
if (typeof window.localStorage['authtoken'] === 'undefined') {
var authtoken = undefined;
} else {
var authtoken = window.localStorage['authtoken'];
}
Auth.isLoggedIn(authtoken).then(function() {
deferred.reject();
}, function() {
deferred.resolve();
});
return deferred.promise;
}];
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'pages/templates/login.html',
resolve: { authenticated: authGuest }
})
.state('logout', { url: '/logout', onEnter: function($state) { localStorage.clear(); $state.go('login'); } })
.state('cloud', {
url: '/cloud',
templateUrl: 'pages/templates/cloud.html',
resolve: { authenticated: authenticated }
})
})
.run(function ($rootScope, $state) {
$rootScope.$on('$stateChangeError', function (event, from, error) {
if(from.name == "login") {
$state.go('cloud');
} else {
$state.go('login');
}
});
});
The problem seems to be here:
var authenticated = ['$q', 'Auth', '$rootScope', function ($q, Auth, $rootScope) {
var deferred = $q.defer();
if (typeof window.localStorage['authtoken'] === 'undefined') {
var authtoken = undefined;
} else {
var authtoken = window.localStorage['authtoken'];
}
Auth.isLoggedIn(authtoken).then(function() {
deferred.resolve();
}, function() {
deferred.reject();
});
return deferred.promise;
}];
This function will only run once on Configuration. The promise, once resolved, will continue to resolve to same value regardless of LocalStorage. Every time you
resolve: { authenticated: authenticated }
You will get back the authenticated value at Configuration step.
instead, add a controller to define functions to request status for each type
.controller('AuthenticationController', function($scope, '$q', Auth, '$rootScope') {
$scope.authenticated = function () {
var deferred = $q.defer();
if (typeof window.localStorage['authtoken'] === 'undefined') {
var authtoken = undefined;
} else {
var authtoken = window.localStorage['authtoken'];
}
Auth.isLoggedIn(authtoken).then(function() {
deferred.resolve();
}, function() {
deferred.reject();
});
return deferred.promise;
};
$scope.authGuest = function ($scope, '$q', Auth, '$rootScope') {
....
return deferred.promise;
};
});
Then, in your routing configurations:
.state('login', {
url: '/login',
templateUrl: 'pages/templates/login.html',
controller: 'AuthenticationController'
resolve: { authenticated: $scope.authGuest() }
})
.state('cloud', {
url: '/cloud',
templateUrl: 'pages/templates/cloud.html',
resolve: { authenticated: $scope.authenticated()}
})
Now, each time you be resolving a fresh promise.
Disclaimer: Without a working version to use, I formed this code for demonstration purposes. It should be treated a pseudo-code to lead in right direction.
I'm using ui-router in my angular application. Currently I've two routes /signin & /user.
Initially it shows /signin when the user clicks on the login button, I'm sending a ajax request and getting the user id. I'm storing the user id in localstorage and changing the state to /user.
Now, what I want, if a user is not loggedin, and user changes the addressbar to /user, it'll not change the view, instead it'll change the addressbar url to /signin again.
I'm try to use resolve, but it's not working. My code is:-
module.exports = function($stateProvider, $injector) {
$stateProvider
.state('signin', {
url: '/signin',
template: require('../templates/signin.html'),
controller: 'LoginController'
})
.state('user', {
url: '/user/:id',
template: require('../templates/user.html'),
resolve:{
checkLogin: function(){
var $state = $injector.get('$state');
console.log("in resolve");
if (! window.localStorage.getItem('user-id')) {
console.log("in if")
$state.go('signin');
}
}
},
controller: 'UserController'
})
}
Please help me to solve this problem.
I don't think it's allowed to change states in the middle of a state transition.
So, the way to address it is to have the checkLogin resolve parameter (I changed it below to userId) to be a function that either returns a value or a promise (in this case, a rejected promise, if you can't get the user-id).
You'd then need to handle this in $rootScope.$on('$stateChangeError') and check the error code.
resolve: {
userId: function ($q, $window) {
var userId = $window.localStorage.getItem('user-id');
if (!userId) {
return $q.reject("signin")
}
return userId;
}
}
And redirect in the $stateChangeError handler:
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
if (error === "signin") {
$state.go("signin");
}
});
If someone has this problem, you can solve it, using timeout service. It will put state switching call at the end of queue.
Also, you should use promises. Rejecting it will prevent initialization of that state:
resolve:{
checkLogin: function(){
var deferred = $q.defer();
var $state = $injector.get('$state');
if (!window.localStorage.getItem('user-id')) {
$timeout(function(){$state.go('signin');});
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise;
}
},
When I change route, from say /set/1 to /set/2, then it still shows the information from /set/1 until I manually refresh the page, I've tried adding $route.refresh to the ng-click of the links to these pages, but that didn't work either. Any ideas?
Below is the routing code, this works fine, all routing is done via links, just <a> tags that href to the route.
angular.module('magicApp', ['ngRoute']).config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'pages/home.html'
}).when('/set', {
redirectTo: '/sets'
}).when('/set/:setID', {
controller: 'SetInformationController',
templateUrl: 'pages/set.html'
}).when('/card', {
redirectTo: '/cards'
}).when('/card/:cardID', {
controller: 'CardInformationController',
templateUrl: 'pages/card.html'
}).when('/sets', {
controller: 'SetListController',
templateUrl: 'pages/sets.html'
}).when('/cards', {
controller: 'CardListController',
templateUrl: 'pages/cards.html'
}).when('/search/:searchterm', {
controller: 'SearchController',
templateUrl: 'pages/search.html'
}).otherwise({
redirectTo: '/'
});
}]);
Below is the code for the SetListController, it uses the routeParams to grab the correct information from a service, which works, when I go to /set/1 then it returns the right information, if I then go back then go to /set/2 it still shows the information from set 1, until I refresh the page.
.controller('SetInformationController', function($scope, $routeParams, $route, SetInformationService, CardSetInformationService) {
$scope.set = [];
$scope.cards = [];
function init() {
SetInformationService.async($routeParams.setID).then(function(d) {
$scope.set = d;
});
CardSetInformationService.async($routeParams.setID).then(function(d) {
$scope.cards = d;
})
}
init();
})
The HTML itself has no reference to the controller, or anything like that, just the objects in the scope, namely set and cards.
I figured it out! The problem wasn't actually with the routing it was with my service, here was the service before:
.factory('SetInformationService', function($http) {
var promise;
var SetInformationService = {
async: function(id) {
if ( !promise ) {
// $http returns a promise, which has a then function, which also returns a promise
promise = $http.get('http://api.mtgdb.info/sets/' + id).then(function (response) {
// The then function here is an opportunity to modify the response
console.log("Set Information");
console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
}
// Return the promise to the controller
return promise;
}
};
return SetInformationService;
})
where it should have been:
.factory('SetInformationService', function($http) {
var promise;
var SetInformationService = {
async: function(id) {
// $http returns a promise, which has a then function, which also returns a promise
promise = $http.get('http://api.mtgdb.info/sets/' + id).then(function (response) {
// The then function here is an opportunity to modify the response
console.log("Set Information");
console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return SetInformationService;
})