Dave Chenell
Dave Chenell

Reputation: 601

UI-router dynamic state access level

I have a state setup like this. This serves dynamic pages from the database based on the :pageSlug via pageObj in the resolve. I am wondering if its possible to make data.access on the state itself a dynamic variable as well, meaning the state would get its access property from the database.

    .state('site.home.page', {
        url: ':pageSlug',
        data: {
            access: AccessLevels.anon //make me dynamic! Can I access the resolve from here?
        },
        resolve: {
            pageObj : function($stateParams, REST, $state) {
                return REST.getOne("/page?slug="+$stateParams.pageSlug).then(function(rtnData) {
                    $state.current.data.access = rtnData.access; //this doesn't work, $state.current doesn't seem to be loaded?
                    return rtnData;
                });
            },
        },

For reference, I'm using a similar setup to this http://www.frederiknakstad.com/2013/01/21/authentication-in-single-page-applications-with-angular-js/

Any help would be much appreciated.

EDIT: Turns out, $state isn't available in resolve or onEnter. A injectable $transition$ state is in the process of being integrated. https://github.com/angular-ui/ui-router/issues/1257

Upvotes: 1

Views: 1803

Answers (2)

jvandemo
jvandemo

Reputation: 13296

First of all I think the server should just return a proper HTTP status code when access is denied so you can handle it safely (and preventing data to go over the wire that should not be seen).

But assuming that is not an option, if I were you, I would create a service to store the ACL's that you receive from the server while resolving the pages, so something like:

(I'm typing this out of my head, so consider this more like meta code)

// Service to keep track of ACL's
.factory('aclService', [function(){
    var service = {},
        acls = {};

    service.setAcl = function(slug, acl){
        acls[slug] = acl;
    };

    service.getAcl = function(slug){
        return acls[slug] || 'access denied'; // or whatever you want to return by default
    };

    return service;
}]);

so you can use it to store the ACL when you receive it:

.state('site.home.page', {
    url: ':pageSlug',
    data: {
        access: AccessLevels.anon //make me dynamic! Can I access the resolve from here?
    },
    resolve: {
        pageObj : function($stateParams, REST, $state, aclService) {
            return REST.getOne("/page?slug="+$stateParams.pageSlug).then(function(rtnData) {

                // Store the ACL in the service
                aclService.setAcl($stateParams.pageSlug, rtnData.access);
                });
            }
        }
    });

and then consult the ACL to decide if you want to load the state or not:

// Listen to the SUCCESS event
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){

    var acl = aclService.getAcl(toParams.pageSlug);

    // Do whatever check is needed to verify access
    if(toState.data.access !== acl){

        // Do whatever you like to do here when access is denied
        // The state is already loaded but you can redirect the user away
    }
});

Since you're using a service to store a cache of ACL, you could further optimize and also add an additional check before the state is transitioned to, like:

// Listen to the START event here
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){

    var acl = aclService.getAcl(toParams.pageSlug);

    // Return early if no cache is found
    if(acl === undefined){
        return;
    }

    // Do whatever check is needed to verify access
    if(toState.data.access !== acl){

        // Do whatever you like to do here when access is denied
        // The state will no longer be loaded now
    }
});

If you really want to take it to the extreme, you could preload the aclService with all ACL's when bootstrapping your AngularJS application so you have a hot cache to start with.

Hope that helps!

Upvotes: 1

TheSharpieOne
TheSharpieOne

Reputation: 25726

when $state.current.data.access = rtnData.access; runs, $state.current is the state that you are coming from, not going to. $state.current only gets changed after everything has resolved.

You may want to log $state.current there to see if it is what you think it should be and not from the ~previous state (that you are coming from). Same with $stateParams.

Depending on when you need to access the data, you can set it up in the $stateChangeSuccess event.

Upvotes: 1

Related Questions