简体   繁体   中英

Angular Authentication : Avoid multiple resolve for different routes

I'm developing an Angular application. In this, I'm authenticating the user before going to dashboard. To achieve this I have wrote the signIn function as

Sign-In Function

this.signIn = function(credentials) {
        console.info('AccountController[signIn] Called');

        AuthService
            .login(credentials)
            .then(function(authenticatedUser) {
                $scope.globals['currentUser'] = authenticatedUser;

                AuthService.setCurrentUser(authenticatedUser);

                $scope.globals['isAuthenticated'] = true;
                $location.path('/dashboard');                    

            }).catch(function(error) {
                console.warn('AccountController[signIn] :: ', error);
                Flash.Error(error);
                $scope.credentials.password = '';
            });
    };

I also want to restrict the user from accessing the routes, if they are not logged in. To achieve that I came up with this dirty code .

Routes

$stateProvider
        .state('signIn', {
            url: '/signIn',
            templateUrl: 'partials/signIn/signIn.html',
            data: {
                pageTitle: 'SignIn'
            },
            controller: 'AccountController',
            controllerAs: 'ac',
            resolve: {
                auth: ['$q', 'AuthService', function($q, AuthService) {
                    var userInfo = AuthService.isAuthenticated();
                    console.info('SignIn Route[isAuthenticated] :: ', userInfo);
                    if (!userInfo) {
                        return $q.when(userInfo);
                    } else {
                        return $q.reject({
                            isAuthenticated: true
                        });
                    }
                }]
            }
        })
        .state('dashboard', {
            url: '/dashboard',
            templateUrl: 'partials/dashboard.html',
            controller: 'DashboardController',
            access: {
                requiredLogin: true
            },
            resolve: {
                auth: ['$q', 'AuthService', function($q, AuthService) {
                    var authenticated = AuthService.isAuthenticated();
                    console.info('dashboard Route[isAuthenticated] :: ', authenticated);
                    if (authenticated) {
                        return $q.when(authenticated);
                    } else {
                        return $q.reject({
                            isAuthenticated: false
                        });
                    }
                }]
            }
        })
        .state('manageStudent', {
            url: '/manageStudent',
            templateUrl: 'partials/manageStudent.html',
            access: {
                requiredLogin: true
            },
            resolve: {
                auth: ['$q', 'AuthService', function($q, AuthService) {
                    var authenticated = AuthService.isAuthenticated();
                    if (authenticated) {
                        return $q.when(authenticated);
                    } else {
                        return $q.reject({
                            isAuthenticated: false
                        });
                    }
                }]
            }
        });


App.run(['$rootScope', 'settings', '$state', 'AuthService', '$location', function($rootScope, settings, $state, AuthService, $location) {
    $rootScope.$state = $state; // state to be accessed from view
    $rootScope.$settings = settings; // state to be accessed from view

    $rootScope.$on('$stateChangeStart', function(event, next,nextParams,prev,prevParams) {

        // If the user is logged in don't allow him to land on the Login Page


        if (next.access !== undefined) {
            if (next.access.requiredLogin && !AuthService.isAuthenticated()) {

                $location.path('/signIn');
            }
        }


    });


    $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {

        event.preventDefault();
        if (!error.isAuthenticated) {
            console.warn("I'm not Authenticated.Going to Sign-in");

            return $location.path('/signIn');
        } else {
            console.info("I'm Authenticated");
            $location.path('/dashboard');

        }
    });
}]);

Reason I said the above code DIRTY is because, If I have 10 routes which I want to protect from Unauthenticated user, I have to copy the same resolve function in all the routes.

So my question is , what should I do to get rid of multiple resolve function and being able to write DRY code?

Since auth should be resolved on each route change, it is insufficient to just wrap it into separate factory (which is a singleton and will run only once). To get round this limitation it should be a function

app.factory('authResolver', function ($q, AuthService) {
  return function () {
    // ...
  };
});

which runs on every route resolve

...
resolve: {
  auth: function (authResolver) {
    return authResolver();
  }
}

Still not that DRY, but that's the recommended humidity level.

More radical approach that may save the one from boilerplate resolve and save a few lines of code will be similar to that :

app.run(function ($rootScope, authResolver) {
  $rootScope.$on('$stateChangeStart', function (e, to) {
    if (to.doAuthPlease)
      to.resolve.auth = authResolver();
  });
});

and

...
doAuthPlease: true,
resolve: {}

The obvious difference with ngRoute in the mentioned answer is that in UI Router you need to have resolve object defined to be able to add new resolvers to the state dynamically. It can be treated like that or leaved as is.

You're on the right track so far. You have what looks like a custom data member access: { requiredLogin: true} on your state objects.

The next step is to use this with the State Change Events that ui-router provides:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
    if (toState.access.requiredLogin) {
       if(!AuthService.isAuthenticated()) {
           event.preventDefault();
           // redirect to signIn?
       }
    }
});

This would be placed in your .run block somewhere which means AuthService needs to be injected there as well. This should remove the need for the resolve block on every route.

Hope that helps.

Update:

if your AuthService.isAuthenticated() function returns a promise, it could be potentially dangerous to rely on the promise to resolve within the event handler (it may move on before the promise resolves). Its probably better that you run the AuthService function before the block (as the application starts) and then store it in a variable:

var isAuth;
AuthService.isAuthenticated().then(function (result) { isAuth = result });

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
    if (toState.access.requiredLogin) {
       if(!isAuth) {
           event.preventDefault();
           // redirect to signIn?
       }
    }
});
var $delegate = $stateProvider.state;
    $stateProvider.state = function(name, definition) {
        var unrestricted = ['signIn'];

        if (unrestricted.indexOf(name) === -1) {
            definition.resolve = angular.extend({}, definition.resolve, {
                auth: ['$q', 'AuthService', function($q, AuthService) {
                    var authenticated = AuthService.isAuthenticated();
                    if (authenticated) {
                        return $q.when(authenticated);
                    } else {
                        return $q.reject({
                            isAuthenticated: false
                        });
                    }
                }]
            });
        }


        return $delegate.apply(this, arguments);
    };

Here I'm dynamically adding the resolve to the routes which I want to restrict.

Because you're using ui.router states (and assuming you're using v0.2.0 or greater), you can use state inheritance to solve this with the resolve and not have to duplicate it all over your various states.

What Do Child States Inherit From Parent States?

Child states DO inherit the following from parent states:

  • Resolved dependencies via resolve
  • Custom data properties

Nothing else is inherited (no controllers, templates, url, etc).

Inherited Resolved Dependencies

New in version 0.2.0

Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.

src - https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states

I accomplish this by using an abstract base state that will defined essentially the same thing you're doing, checking to see if the user is allowed to proceed. Since all of my UI states inherit from the abstract parent state, the authentication dependency is resolved for each of them.

abstract base state

.state('baseState', {
    url: '',
    abstract: true,
    template: '<ui-view></ui-view>'
    resolve: {
        auth: ['$q', 'AuthService', function($q, AuthService) {
            var authenticated = AuthService.isAuthenticated();
            console.info('dashboard Route[isAuthenticated] :: ', authenticated);
            if (authenticated) {
                return $q.when(authenticated);
            } else {
                return $q.reject({
                    isAuthenticated: false
                });
            }
        }]
    }
})

other states

.state('dashboard', {
    parent: 'baseState'
    url: '/dashboard',
    templateUrl: 'partials/dashboard.html',
    controller: 'DashboardController',
    ...
})

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM