Реализация контроля доступа в пользовательском интерфейсе Angular может оказаться сложной задачей. Это легко, если вам не нужно использовать обещания, но когда вам нужно, все становится намного сложнее.

TL; DR. Прокрутите статью до конца, чтобы увидеть код без объяснения причин.

Тем не менее, мы смелые души, и я предполагаю, что вы знаете и любите обещания, иначе зачем вам когда-либо начинать использовать JavaScript и, конечно же, немного поработать с Angular ui-router .;)

Итак, чего мы хотим достичь? Мы хотим иметь возможность описывать правила, которые должны быть выполнены, чтобы перейти к запрашиваемому нами состоянию. Это должно выглядеть примерно так:

.state('login', {
    url: '/login',
    templateUrl: 'views/login.html', 
    controller: 'IrrelevantLoginController',
    rules: [
        'notAuthenticate', 
        'notRegistrationProcess'
    ]
})

По сути, мы добавили параметр rules к нашему состоянию, который будет содержать массив правил, поэтому пользователю необходимо пройти через все правила, чтобы перейти в состояние.

Angular ui-router поддерживает набор событий для переходов между состояниями, таких как $ stateChangeStart, $ stateChangeError и другое. Мы будем использовать событие $ stateChangeStart, чтобы запустить нашу проверку правил. Мы перенесем правила и итератор правил в новую службу AclService, которая будет отвечать за определение того, может ли пользователь получить доступ к состоянию. Итак, в нашей функции .run мы пишем что-то вроде этого:

$rootScope.$on('$stateChangeStart', function(e, toState, toParams, fromState, fromParams) {
        if(!AclService.execute(toState, fromState)) {
            // a function that will decide, 
            // where should user go if not allowed
            getMeOutOfHere();
        }
    });
});

Теперь, когда наш код не работает, давайте создадим нашу AclService. Функция AclService.execute () будет перебирать предоставленные правила и возвращать true или false в зависимости от результата выполнения цепочки правил.

angular.module('awesomeApp')
    .service('AclService', function($q, CustomerService) {
        var rules = {
            notAuthenticate: function() {
                return !CustomerService.isAuthenticated();
            },
            notRegistrationProcess: function() { 
                return !CustomerService.isRegistrationProcess;
            }
        };

        this.execute = function(toState, fromState) {
            var result = true;
            if(toState.rules) {
                for(var index in toState.rules) {
                    if(result) {
                        rules[toState.rules[index]]()
                    }
                }
            }
                
            return result;
        };
    });

Мы создали кучу вещей, наша функция execute выполняет итерацию по правилам, правила возвращают свои логические значения, а окончательный результат возвращается обратно в функцию события $ stateChangeStart. Обратите внимание, насколько легко вы можете получить доступ к массиву правил.

Вы также могли заметить, что у нас есть служба CustomerService, которая содержит данные о клиентах для наших правил. Все работает, как только у нас есть объект Customer, заполненный данными. Но что, если CustomerService.isRegistrationProcess будет undefined, и нам нужно будет получить наш объект Customer?

Итак, мы немного меняем наше правило:

notRegistrationProcess: function () {
    var result = $q.defer();
    CustomerService.getCustomer().then(function(customer) {
        if(!customer.isRegistrationProcess) {
            result.resolve(true);
        } else {
            result.reject(false);
        }
    }, function(){
        result.reject();
    });

    return result.promise;
}

В приведенном выше фрагменте я использовал функцию getCustomer, которая пытается получить клиента с сервера, если клиент не определен, и возвращает нам обещание. Итак, теперь сама функция правила также возвращает обещание, а наша функция выполнения не обновлена ​​для работы с обещаниями, поэтому нам придется обновить ее соответствующим образом.

Но давай остановимся на этом и подумаем. Изменяя правило так, чтобы оно возвращало обещание вместо логического, мы меняем весь наш механизм правил на асинхронный. Теперь цикл for, который повторяет правила в нашей функции AclService.execute, будет получать обещание вместо логического значения для каждого выполняемого правила, и мы больше не можем полагаться на нашу переменную результата. Иногда он возвращает обещание, а иногда и логическое значение. Итак, нам нужно, чтобы наши правила были последовательными в типе данных, который они возвращают, давайте договоримся, что они всегда должны возвращать обещание. Теперь мы соответствующим образом изменим правило notAuthenticate:

notAuthenticate: function () {
    var result = $q.defer();
    if(!CustomerService.isAuthenticated()) {
        result.resolve(true);
    } else {
        result.reject(false);
    }
    return result.promise;
}

в этом руководстве CustomerService.isAuthenticated () использует локальное хранилище любого типа, поэтому мы превратим его в обещание.

Теперь функция execute будет получать обещания вместо логических.

Чтобы решить, каков наш результат, мы должны собрать все обещания правил, и в тот момент, когда все переменные будут разрешены, мы переберем их и выясним, готовы ли мы продолжить или нет.

this.execute = function(toState, fromState) {
    var defer = $q.defer();
    var result = true;
    if(toState.rules) {
        var promises = [];
        for(var index in toState.rules) {
            promises.push(rules[toState.rules[index]]().then(function(){},function() {result = false; }));
        }
        $q.all(promises).then( function() { defer.resolve(result);});
    } else {
        defer.resolve(result);
    }

    return defer.promise;
};

Давайте рассмотрим это подробнее. Сначала мы собираем все обещания правил в массив обещаний.

for(var index in toState.rules) {
    promises.push(rules[toState.rules[index]]().then(function(){},function() {result = false; }));
}

Обратите внимание на пустую функцию успеха, мы не хотим изменять значение результата на true, мы меняем его только в том случае, если в функции отказа мы получили false.

затем мы запускаем $ q.all для нашего массива обещаний, и когда все они разрешены, мы разрешаем нашу результирующую переменную функции execute:

$q.all(promises).then( function() { defer.resolve(result);});

и если у нас нет правил для повторения, мы разрешим наше предустановленное истинное значение.

Наконец-то мы закончили с нашим aclService, давайте вернемся к нашей функции .run. Теперь, когда наш aclService возвращает обещание, нам нужно изменить его на асинхронный способ, поэтому мы получим следующее:

$rootScope.$on('$stateChangeStart', function(e, toState, toParams, fromState, fromParams) {
    e.preventDefault();
    AclService.execute(toState, fromState).then(function(result){
        if(result) {
            $state.go(toState.name, toParams, {notify: false}).then(function() {
                $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
            });
        } else {
            getMeOutOfHere();
        }
    });
});

Из-за обещаний мы должны остановить переход состояния, вызвав метод preventDefault для переменной события. Больно, но другого выхода я не вижу. Итак, если мы получили положительное логическое значение от нашей aclService, мы снова запускаем переход, но на этот раз мы не уведомляем, чтобы этот процесс не зацикливался, а когда мы завершаем переход, мы транслируем $ stateChangeSuccess для тех, кто слушает.

$state.go(toState.name, toParams, {notify: false}).then(function() {
    $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
});

Итак, вот решение полностью

Пример routes.js:

.state('login', {
    url: '/login',
    templateUrl: 'views/login.html', 
    controller: 'IrrelevantLoginController',
    rules: [
        'notAuthenticate', 
        'notRegistrationProcess'
    ]
})

файл run.js:

angular.module('awesomeApp').run(function($rootScope, $location, $state, AclService) {
    function getMeOutOfHere() {
        // your fallback logic here
    }
    
    $rootScope.$on('$stateChangeError', function() {
        getMeOutOfHere();
    });
    $rootScope.$on('$stateChangeStart', function(e, toState, toParams, fromState, fromParams) {
        e.preventDefault();
        AclService.execute(toState, fromState).then(function(result){
            if(result) {
                $state.go(toState.name, toParams, {notify: false}).then(function() {
                    $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
                });
            } else {
                getMeOutOfHere();
            }
        });
    });
});

acl-service.js:

angular.module('awesomeApp')
    .service('AclService', function($q, CustomerService) {
        var rules = {
            notAuthenticate: function () {
                var result = $q.defer();
                if(!CustomerService.isAuthenticated()) {
                    result.resolve(true);
                } else {
                    result.reject(false);
                }
                return result.promise;
            },
            notRegistrationProcess: function () {
                var result = $q.defer();
                CustomerService.getCustomer().then(function(customer) {
                    if(!customer.isRegistrationProcess) {
                        result.resolve(true);
                    } else {
                        result.reject(false);
                    }
                }, function(){
                    result.reject();
                });

                return result.promise;
            }
        };

        this.execute = function(toState, fromState) {
            var defer = $q.defer();
            var result = true;
            if(toState.rules) {
                var promises = [];
                for(var index in toState.rules) {
                    promises.push(rules[toState.rules[index]]().then(function(){},function() {result = false; }));
                }
                $q.all(promises).then( function() { defer.resolve(result);});
            } else {
                defer.resolve(result);
            }

            return defer.promise;
        };
    });

Вот и все, прокомментируйте, если вы сочтете эту статью полезной. Замечания приветствуются.