Реализация контроля доступа в пользовательском интерфейсе 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; }; });
Вот и все, прокомментируйте, если вы сочтете эту статью полезной. Замечания приветствуются.