This resolve function works everywhere else, and I'm stumped as to why the profile page is still executing when the promise is rejected. We produced this bug by logging in, deleting the token from storage and then trying to navigate to the profile page. It goes through all the authentication steps, and logs out. It hits the redirection line, but then it still loads the profile page, throwing errors because the auth has been cleared.
Any help you can offer would be great. Let me know if you need more info.
app.config('$routeProvider')
.when('/profile', {
templateUrl: '/templates/profile.html',
controller: 'ProfileController',
resolve: {
auth: function (SessionService) {
SessionService.resolve()
}
}
}).
otherwise({
redirectTo: '/login'
})
function resolve () {
if (self.isAuthenticated()) {
return $q.when({auth: true})
} else {
$q.reject({auth: false})
self.logout() // first we go here
}
}
}
function logout () {
var auth = self.storage()
if (...) {
...
} else {
self.clearUserAuthentication() // then here
$location.path('/login') // it redirects here, but still initializes the profile controller
}
}
/profile route always resolves because auth always returns resolved promise (or better put it: it doesn't return rejected promise and doesn't throw exception). Correct code would be:
.when('/profile', {
templateUrl: '/templates/profile.html',
controller: 'ProfileController',
resolve: {
auth: function (SessionService) {
return SessionService.resolve()
}
}
}).
Note, that it's very important that auth handler returns promise. If you omit return keyword it results into implicit return undefined. This is meaningless but is still considered as resolved promise.
Related
In my scenario, when a visitor navigates to a page (or route) of the first time they should be anonymously authenticated (I'm using Firebase). For context; later on the visitor may migrate their anonymous session after they have logged in, with Facebook, for example.
If the anonymous authentication fails for some reason, they are redirected to an error page (route) -- one of few pages that do not require any authentication.
I am using promises to:
check if the visitor is already authenticated
if they are not, try and anonymously authenticate them
if they are authenticated successfully, resolve the promise (and route)
if the authentication fails for some reason, reject the promise
Question:
The promise always seems to be rejected the first time a visitor navigates to a page (route) that requires authentication, even when the visitor has been anonymously authenticated successfully (in step 2 above); why would this be happening?
Below I have included my code, and added a comment to highlight the section that seems to be causing the problem.
Thanks for your help with this, it is always appreciated!
var app = angular.module('vo2App', ['firebase', 'ngCookies', 'ngRoute']);
app.config(['$locationProvider', '$routeProvider', function ($locationProvider, $routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeCtrl',
templateUrl: '/views/home.html'
})
.when('/login', {
controller: 'LoginCtrl',
templateUrl: '/views/login.html'
})
.when('/oops', {
controller: 'OopsCtrl',
resolve: {
currentAuth: function (){
return null;
}
},
templateUrl: '/views/oops.html'
});
}]);
app.run(['$location', '$rootScope', 'Auth', 'ErrorMsg', function ($location, $rootScope, Auth, ErrorMsg) {
$rootScope.$on('$routeChangeError', function (event, next, prev, error) {
if (error === 'AUTH_REQUIRED') {
ErrorMsg.add('Unauthorized');
// TODO: Make all error messages constants
$location.url('/oops');
}
});
$rootScope.$on('$routeChangeStart', function (event, next, current) {
if (! ('resolve' in next)) {
next.resolve = {};
}
if (! ('currentAuth' in next.resolve)) {
next.resolve.currentAuth = function ($q, Auth) {
var deferred = $q.defer();
Auth.$requireAuth().then(deferred.resolve, function () {
/* ** The following line seems to be causing the problem ** */
Auth.$authAnonymously().then(deferred.resolve, deferred.reject('AUTH_REQUIRED'));
});
return deferred.promise;
};
}
});
}]);
deferred.reject is not passed to then as error function, it is called every time.
Auth.$authAnonymously().then(deferred.resolve, deferred.reject('AUTH_REQUIRED'));
That's why deferred.promise is always rejected.
And you should possibly know that the code above is usually referred to as deferred antipattern. It is preferable to use existing promises instead of creating a new one with defer().
I'm struggling to get Angular route resolve working. I find the documentation less than useless for more complex parts of the javascript framework like this.
I have the following service:
app.service("AuthService", ["$http", "$q", function($http, $q){
this.test = function(){
return $q(function(resolve, reject){
var auth = false;
resolve(auth);
});
}
}]);
Now my routes looks like this:
app.config(["$routeProvider", "$locationProvider", function($routeProvider, $locationProvider){
$locationProvider.html5Mode(true).hashPrefix("!");
$routeProvider
.when("/account", {
templateUrl: "/views/auth/account.html",
controller: "AccountController",
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (auth) return true;
else return false;
});
}]
}
});
}]);
What I want to happen here is the following:
User goes to /account
AuthService is fired and returns a variable (true or false)
In the resolve, if the returned value is false, the route cannot be loaded
If the returned value is true, the user is authenticated and can view the route
I don't think I've fully understood how to use resolve and my method so far does not work. Could someone please explain the correct way to do this?
The resolve block when configuring routes is designed to make navigation conditional based upon the resolution or rejection of a promise.
You are attempting to handle this by resolving with a resolution value of true or false
Please try the following:
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (!auth){
throw 'not authorized';
}
});
}]
}
This will cause the promise to be rejected and therefore not allow the routing to continue/complete.
Also of note is that the value coming out of the promise resolution will be injected into the handling controller
This solution works for me, I also tried with .then function but it's incorrect because resolve performs it.
resolve: {
auth:
["AuthService", "AnotherService", "$rootScope", function(AuthService, AnotherService, $rootScope) {
if ($rootScope.yourCondition){
return AuthService.getFunction();
}
else{
return AnotherService.getAnotherFunction();
}
}]
},
views: {
'partialView#': {
/* == Component version == */
component: "yourComponent",
bindings: {
auth: 'auth', // Inject auth loaded by resolve into component
params: '$stateParams'
}
}
}
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;
}
},
So let's say I have the following user property and I want to restrict access to a page. This is in my Firebase noSQL database but I think this could pertain to obtaining data from anywhere.
{
"users": {
"simplelogin:1": {
"properties": { "admin_user": true }
}
}
}
So in my javascript I have the following:
var user_properties = new Firebase("https://<MY-URL>.com/users/"+auth.uid+"/properties");
user_properties.once("value", function(properties) {
if(properties.val().admin_user == false)
window.location.replace("/");
});
So, on the page load of the "admin page", I load this javascript. And if they aren't an admin, the page is supposed to redirect.
However, I'm having the problem where the admin page will load for a second while it gathers the data and then redirect.
Does anyone have any suggestions on how I can make the page redirect before the page even loads?
Security rules in Firebase can prevent the page from data from being viewed without permission. Then you simply need a client-side solution to redirect the page. The simple answer here is to use resolve in your routes.
You can find a complete implementation of this approach in the angularFire-seed project. Here's the relevant code:
"use strict";
angular.module('myApp.routes', ['ngRoute', 'simpleLogin'])
.constant('ROUTES', {
'/home': {
templateUrl: 'partials/home.html',
controller: 'HomeCtrl',
resolve: {
// forces the page to wait for this promise to resolve before controller is loaded
// the controller can then inject `user` as a dependency. This could also be done
// in the controller, but this makes things cleaner (controller doesn't need to worry
// about auth status or timing of displaying its UI components)
user: ['simpleLogin', function(simpleLogin) {
return simpleLogin.getUser();
}]
}
},
'/chat': {
templateUrl: 'partials/chat.html',
controller: 'ChatCtrl'
},
'/login': {
templateUrl: 'partials/login.html',
controller: 'LoginCtrl'
},
'/account': {
templateUrl: 'partials/account.html',
controller: 'AccountCtrl',
// require user to be logged in to view this route
// the whenAuthenticated method below will resolve the current user
// before this controller loads and redirect if necessary
authRequired: true
}
})
/**
* Adds a special `whenAuthenticated` method onto $routeProvider. This special method,
* when called, invokes the requireUser() service (see simpleLogin.js).
*
* The promise either resolves to the authenticated user object and makes it available to
* dependency injection (see AuthCtrl), or rejects the promise if user is not logged in,
* forcing a redirect to the /login page
*/
.config(['$routeProvider', function($routeProvider) {
// credits for this idea: https://groups.google.com/forum/#!msg/angular/dPr9BpIZID0/MgWVluo_Tg8J
// unfortunately, a decorator cannot be use here because they are not applied until after
// the .config calls resolve, so they can't be used during route configuration, so we have
// to hack it directly onto the $routeProvider object
$routeProvider.whenAuthenticated = function(path, route) {
route.resolve = route.resolve || {};
route.resolve.user = ['requireUser', function(requireUser) {
return requireUser();
}];
$routeProvider.when(path, route);
}
}])
// configure views; the authRequired parameter is used for specifying pages
// which should only be available while logged in
.config(['$routeProvider', 'ROUTES', function($routeProvider, ROUTES) {
angular.forEach(ROUTES, function(route, path) {
if( route.authRequired ) {
// adds a {resolve: user: {...}} promise which is rejected if
// the user is not authenticated or fulfills with the user object
// on success (the user object is then available to dependency injection)
$routeProvider.whenAuthenticated(path, route);
}
else {
// all other routes are added normally
$routeProvider.when(path, route);
}
});
// routes which are not in our map are redirected to /home
$routeProvider.otherwise({redirectTo: '/home'});
}])
/**
* Apply some route security. Any route's resolve method can reject the promise with
* { authRequired: true } to force a redirect. This method enforces that and also watches
* for changes in auth status which might require us to navigate away from a path
* that we can no longer view.
*/
.run(['$rootScope', '$location', 'simpleLogin', 'ROUTES', 'loginRedirectPath',
function($rootScope, $location, simpleLogin, ROUTES, loginRedirectPath) {
// watch for login status changes and redirect if appropriate
simpleLogin.watch(check, $rootScope);
// some of our routes may reject resolve promises with the special {authRequired: true} error
// this redirects to the login page whenever that is encountered
$rootScope.$on("$routeChangeError", function(e, next, prev, err) {
if( angular.isObject(err) && err.authRequired ) {
$location.path(loginRedirectPath);
}
});
function check(user) {
if( !user && authRequired($location.path()) ) {
$location.path(loginRedirectPath);
}
}
function authRequired(path) {
return ROUTES.hasOwnProperty(path) && ROUTES[path].authRequired;
}
}
]);
When using the resolves in ui-router, is it possible to have something resolve first (an authentication check for example) before the other resolves on a state are even started?
This is an example of how I'm doing my authentication check right now:
angular.module('Example', [
'ui.router',
'services.auth',
'services.api'
])
.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('order', {
url: '/order',
views: {
'main': {
templateUrl: 'order/order.tpl.html',
controller: 'OrderCtrl'
}
},
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', function (api) {
return api.getCards();
}],
shippingAddresses: ['api', function (api) {
return api.getShippingAddresses();
}]
}
});
}]);
I'd like loggedIn to resolve first before cards and shippingAddresses start.
I could just put the other API calls in the controller (that's where they were before), but I'd like the data retrieved from them to be available before the templates start rendering.
This might work but then I couldn't inject the resolved values of each individual API call.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', 'api', function (auth, api) {
return auth.loggedIn()
.then(api.getCards())
.then(api.getShippingAddresses())
.then(function () {
// whatever....
});
}]
}
...
Edit:
Looks like the answer here solves my issue, maybe it's not the best way to solve this problem but it works.
Just by injecting the resolved value of the authentication check it will wait until it is resolved before continuing on with the other resolves.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', 'loggedIn', function (api, loggedIn) {
return api.getCards();
}],
shippingAddresses: ['api', 'loggedIn', function (api, loggedIn) {
return api.getShippingAddresses();
}]
}
...
I would answer this myself but I'm going to leave it open for other people to provide their solutions.
I've provided some proof of concept how promises can be combined together: http://jsfiddle.net/pP7Uh/1/
Any rejects will prevents from calling next then method
resolve: {
data: function (api) {
var state;
return api.loggedIn().then(function(loggedIn) {
state = loggedIn
return api.getCards();
}).then(function(getCards) {
return {
loggedIn: state,
getCards: getCards
}
}).catch(function(error) {
console.error('error', error)
});
}
}
After this in data resolved object you will have:
{
loggedIn:"calling loggedIn",
getCards:"calling getCards"
}