2

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?

Tushar
  • 1,115
  • 1
  • 10
  • 26

4 Answers4

3

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.

Community
  • 1
  • 1
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • +1 for showing me the right way..I've modified my code taking some of your logic..and it's working.Have a look. – Tushar Nov 25 '15 at 05:07
0

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?
       }
    }
});
jsonmurphy
  • 1,600
  • 1
  • 11
  • 19
  • Yes, that's exactly why I've had to give up on preventDefault in route events some time ago. While it is possible in theory to get away with it, it usually adds more trouble than it can solve. – Estus Flask Nov 24 '15 at 13:35
  • @jsonmurphy, I think you misunderstood the question. I already had that logic, just for sake, I'm adding it to my question. Please have a look. Now, what I want is, I don't want to use resolve multiple times. – Tushar Nov 24 '15 at 13:35
  • OK so im a bit confused.. Is it that you want to pass the `userInfo` (or whatever `isAuthenticated` returns) to every route once they are authenticated? – jsonmurphy Nov 24 '15 at 13:47
  • @jsonmurphy - Yeah, Is there any better way to do it? – Tushar Nov 24 '15 at 14:13
  • You could add it to the `toParams` object as `toParams.userInfo = ...` and then access it via the `$stateParams` service in your controller with `$stateParams.userInfo`. I tested it [here](http://plnkr.co/edit/VjC1w9fHs4SWS8qJcOUN?p=preview) and it seemed to work (check line 20 and 47 in js file). Let me know if works and I'll update my answer. – jsonmurphy Nov 24 '15 at 17:31
0
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.

Tushar
  • 1,115
  • 1
  • 10
  • 26
  • Looks fine to me, I've accidentally decorated provider methods to take a short cut. Just remember that there can be race conditions, the method can be called before it was patched. A good way to avoid that is to load patched module instead of the original, like it was done here http://stackoverflow.com/a/33813535/3731501 – Estus Flask Nov 25 '15 at 07:54
0

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',
    ...
})
Community
  • 1
  • 1
jusopi
  • 6,791
  • 2
  • 33
  • 44