web-dev-qa-db-fra.com

AngularJS, ui.router, modèle de charge et contrôleur basés sur le rôle de l'utilisateur

J'ai développé une application de page unique qui utilise une api REST. Les utilisateurs doivent se connecter pour accéder à l'application. Lorsqu'un utilisateur se connecte, il est redirigé vers/tableau de bord. Sur cette URL/route, Je voudrais charger un modèle et un contrôleur différents en fonction du rôle de l'utilisateur (par exemple, utilisateur normal ou admin utilisateur).

J'ai regardé https://github.com/angular-ui/ui-router/wiki sous la section des modèles mais aucune des options ne prend en charge ce que j'essaie de réaliser.

  • En utilisant templateUrl et la fonction (stateParams), je ne peux pas injecter le service qui m'aide à déterminer le rôle d'utilisateur afin que je puisse charger le modèle, par ex. views/ user /dashboard.html ou views/ admin /dashboard. html
  • En utilisant templateProvider, je dois injecter le service qui m'aide à déterminer le rôle d'utilisateur, mais comment puis-je charger le modèle?

Toute solution doit également charger différents contrôleurs en fonction du rôle d'utilisateur, par exemple UserDashboardController ou AdminDashboardController.

Donc, ce dont j'ai besoin, c'est d'une route unique qui charge un modèle ET un contrôleur différent en fonction d'une variable de rôle utilisateur définie dans un service lorsqu'un utilisateur se connecte.

Suis-je en train de penser dans la bonne direction, ou devrais-je mettre en œuvre une autre solution?

Toute aide à ce sujet serait grandement appréciée.

32
user3596298

Chargement du modèle et du contrôleur en fonction du rôle de l'utilisateur

Bien que techniquement ui-router La fonction templateUrl ne prend pas en charge les services d'injection, vous pouvez utiliser templateProvider pour injecter service qui contient la variable role ou la charge de manière asynchrone, puis utilisez $templateFactory pour renvoyer du contenu HTML. Prenons l'exemple suivant:

var app = angular.module('app', ['ui.router']);

app.service('session', function($timeout, $q){
    this.role = null;

    this.loadRole = function(){
        //load role using axax request and return promise
    };
});

app.config(function($stateProvider, $urlRouterProvider){
    $stateProvider.state('dashboard', {
        url: '/dashboard',
        templateProvider: function(session, $stateParams, $templateFactory){
          return session.loadRole().then(function(role){
              if(session.role == 'admin'){
                return $templateFactory.fromUrl('/admin/dashboard.html', $stateParams);
              } else {
                return $templateFactory.fromUrl('/user/dashboard.html', $stateParams);
              }
          });
        }
      });

    $urlRouterProvider.otherwise('/dashboard');
});

En ce qui concerne controller, vous pouvez indiquer que vous souhaitez utiliser un contrôleur spécifique à l'intérieur de l'élément racine de chaque modèle avec ng-controller. Ou de même, vous pouvez utiliser l'option controllerProvider pour injecter service qui aura déjà role résolu par templateProvider. Jetez un œil à l'exemple suivant de l'option controllerProvider dans ui-router définition de l'état:

controllerProvider: function(session){
  if(session.role == 'admin'){
    return 'AdminCtrl';
  } else {
    return 'UserCtrl';  
  }
}

Bien sûr, vous pouvez facilement supprimer les doublons de ce code et définir un micro DSL plus accessible pour faciliter la définition de différentes règles pour des rôles et des vues particuliers.

La démo suivante devrait vous aider à comprendre le code.

Est-ce une bonne approche?

Comme d'habitude, cela dépend grandement du contexte. Pour aider vous à trouver une réponse, permettez-moi de suggérer d'abord les questions suivantes:

  • Combien de vues présentées aux rôles diffèrent?

Allez-vous masquer seulement quelques button et autres éléments d'action, ce qui rend une page en lecture seule pour les utilisateurs réguliers et modifiable pour les superutilisateurs? Si les modifications sont minimes, j'irais probablement en utilisant les mêmes vues et en ne masquant que des éléments particuliers, forgeant probablement une directive similaire à ng-if qui permettrait d'activer/désactiver certaines fonctionnalités de façon déclarative only-role='operator, admin'. D'un autre côté, si les vues vont être très différentes, alors l'utilisation de modèles différents peut simplifier considérablement le balisage.

  • Combien d'actions disponibles sur page diffèrent selon le rôle?

Les actions qui semblent similaires en surface diffèrent-elles dans le fonctionnement interne pour différents rôles? Par exemple, si vous avez une action Modifier disponible à la fois pour le rôle user et admin mais dans un cas, elle démarre wizard comme L'interface utilisateur et sous une autre forme complexe pour les utilisateurs avancés ayant alors un controller séparé est plus logique. D'un autre côté, si les actions admin sont un surensemble des actions user, alors avoir un seul contrôleur semble plus facile à suivre. Notez que dans les deux cas, conserver controller est payant - ils ne doivent coller les vues qu'au comportement encapsulé dans les services/afficher les modèles/modèles/choisir un nom

  • Aurez-vous de nombreux liens contextuels - liens menant à page particuliers à différents endroits de l'application?

Par exemple, être en mesure de fournir une navigation vers une page en écrivant simplement ui-sref="dashboard" quel que soit l'utilisateur actuel role peut être bénéfique s'il existe dans divers contextes. Si tel est le cas, les avoir définis sous une route/un état unique semble plus facile à gérer qu'une logique conditionnelle utilisée pour construire différents ui-sref/ng-href en fonction du rôle. Cependant, vous pouvez également définir dynamiquement des routes/états en fonction du rôle de l'utilisateur - chargé dynamiquement ou non

  • Les vues et les actions disponibles pour différents rôles sur un page évolueront-elles séparément ou ensemble?

Parfois, nous créons d'abord des fonctionnalités pour les utilisateurs réguliers, puis pour premium et enfin pour ultime. Il n'est pas inhabituel de diviser le travail sur les pages pour user et admin entre les membres de l'équipe, surtout si des limites claires peuvent être tracées facilement. Dans ce cas, avoir des views et controllers séparés peut simplement permettre aux développeurs d'éviter les conflits. Bien sûr, ce ne sont pas tous les arcs-en-ciel et les licornes - l'équipe doit être très disciplinée pour éliminer la duplication qui se produira probablement.

J'espère que mes suggestions vous aideront à décider.

23
miensol

Suis-je en train de penser dans la bonne direction, ou devrais-je mettre en œuvre une autre solution?

OMI, vous ne devriez pas le faire de cette façon.

Ici, je propose 2 autres solutions selon la façon dont votre application est implémentée.

1) Si les droits de vos rôles peuvent être configurés (vous pouvez avoir une page séparée pour configurer vos rôles, attribuer des droits à vos rôles, ...). Ensuite, utilisez seulement 1 modèle et 1 contrôleur pour vos rôles (utilisateurs normaux, utilisateurs administratifs, etc.) et utilisez ng-show, ng-class, .. pour afficher votre code HTML en conséquence.

Dans ce cas, peu importe que l'utilisateur soit un utilisateur normal ou un administrateur, ce n'est que le nom de notre rôle. Ce qui nous importe, c'est le droits et c'est dynamique => Par conséquent, nous devons afficher le html dynamiquement en fonction des droits configurés (pour sûr, il y a aussi des vérifications côté serveur lorsque les utilisateurs effectuent une action pour empêcher l'utilisateur de créer une requête http malveillante et de la publier sur le serveur). Si nous devions utiliser des modèles séparés pour cela, il y a innombrables cas.

Le point de cette solution est que les fonctions de la page sont les mêmes à vos rôles, il vous suffit de afficher/masquer les fonctions de la page en fonction de l'utilisateur.

2) Si les droits des rôles sont fixes (ne peuvent pas être configurés) et fonctionnalité des vues pour les utilisateurs normaux et les administrateurs sont différents. Il est préférable d'utiliser les états séparés pour ces vues et d'autoriser l'accès à ces vues en fonction de l'utilisateur connecté (à coup sûr, il existe également une autorisation côté serveur lorsque les utilisateurs effectuent une action).

La raison en est: la vue utilisateur admin et la vue utilisateur normale ont fonctionnalités différentes (qui doivent être séparées l'une de l'autre)

15
Khanh TO

Si vous utilisez la version a de angular supérieur à 1,2, vous pouvez faire une directive avec un templateUrl comme fonction.

Donc, les idées de base sont que vous avez une vue de tableau de bord qui a une directive personnalisée qui déterminera le modèle en fonction du niveau utilisateur. Donc quelque chose comme ça:

(function () {
  'use strict';
  angular.module('App.Directives')
    .directive('appDashboard', ['UserManager', function (UserManager) {
      return {
        restrict: 'EA',
        templateUrl: function(ele, attr){
            if (UserManager.currentUser.isAdmin){
                return 'admin.html';
            }else{
                return 'user.html';
            }
        }
      };
    }]);
})(); 
7
Shawn C.

I. Ne pas utilisez "... single route qui charge un modèle différent ... ", serait ma suggestion, ma réponse.

Si possible:

Essayez de prendre du recul et de reconsidérer toute la conception et
Essayez de affaiblir le sentiment que les utilisateurs de notre application sont intéressés par url.

Ils ne sont pas. Et s'ils comprennent vraiment ce qu'est url, barre d'adresse ... ils l'utilisent pour copy, send et paste... ne pas enquêter sur ses parties ...

II. Suggestion: Imposer l'utilisation du routeur ui indique :

... UI-Router est organisé autour d'états , qui peuvent [~ # ~] éventuellement [~ # ~] ont des routes, ainsi que d'autres comportements, attachés ...

Cela signifie que nous devons reconsidérer notre application en tant que groupe/hiérarchie d'états bien définis. Ils peuvent avoir url défini, mais n'ont pas à (par exemple état d'erreur sans url)

III. Comment pouvons-nous profiter de la construction de notre application dans les États?

Séparation des préoccupations - devrait être notre objectif.

L'état est une unité qui rassemble certains voir/ contrôleurs, resolvers, données personnalisées ...

Cela signifie qu'il pourrait y avoir plus états réutilisant vues, contrôleurs, etc. De tels états peuvent vraiment différer (même vue, contrôleur différent). Mais ils sont à usage unique - ils sont là pour gérer certains scénarios:

  • administration du dossier Utilisateur/Employé
  • liste des utilisateurs/employés - informations ala PhoneList (juste email, téléphone ...)
  • Administration de la sécurité - Quels sont les droits d'un utilisateur ...

Et encore une fois, il pourrait y avoir de nombreux états . Avoir même des centaines d'États ne sera pas un problème de performance. Ce ne sont que des définitions, un ensemble de références à d'autres pièces, qui devraient être utilisées ... plus tard ... si vraiment nécessaire.

Une fois que nous avons défini cas d'utilisation, user stories au niveau de l'état , nous pouvons les regrouper en ensembles/héritières.
Ces groupes pourraient être présentés ultérieurement à différents rôles d'utilisateur dans un format différent (différents éléments de menu)

Mais à la fin, nous avons gagné beaucoup de liberté et simplifié la maintenabilité

IV. Gardez l'application en marche et en pleine croissance

S'il y a peu d'États, le maintien de la race ne semble pas être un problème. Mais il pourrait arriver, que l'applicaton réussisse. Réussir et grandir ... à l'intérieur de sa conception.

La division des définitions de l'état (en tant qu'unité de travail) et de leurs hiérarchies (à quel rôle utilisateur peut accéder quels états) simplifierait sa gestion.

Appling de la sécurité en dehors des états (Les écouteurs d'événements ala '$stateChangeStart' ) est beaucoup plus facile, puis la refactorisation sans fin des fournisseurs de modèles. De plus, la partie principale de la sécurité doit toujours être appliquée sur un serveur, quelle que soit l'interface utilisateur autorisée.

V. Résumé:

Bien qu'il existe une fonctionnalité aussi intéressante qu'un templateProvider, qui pourrait faire des choses intéressantes pour nous (par exemple ici: Changer le menu de navigation en utilisant UI-Router dans AngularJs ) ...

... nous ne devons pas l'utiliser pour des raisons de sécurité. Cela pourrait être implémenté en tant que menu/hiérarchie construit à partir d'états existants, en fonction du rôle actuel. Les écouteurs d'événements doivent vérifier si l'utilisateur arrive à l'état accordé, mais la vérification principale doit être appliquée sur un serveur ...

4
Radim Köhler

Vous n'avez pas vraiment besoin de le faire avec un routeur.

La chose la plus simple est d'utiliser un seul modèle pour tous les rôles et d'utiliser dynamique ng-include à l'intérieur. Supposons que vous ayez un injecteur dans $ scope:

<div ng-include="injector.get('session').role+'_dashboard.html'"></div>

Vous devriez donc avoir user_dashboard.html et admin_dashboard.html vues. À l'intérieur de chacun, vous pouvez appliquer un contrôleur séparé, par exemple user_dashboard.html:

<div id="user_dashboard" ng-controller="UserDashboardCtrl">
    User markup
</div>
3
gorpacrate

J'ai utilisé la solution suivante (qui n'est peut-être pas idéale, mais elle a fonctionné pour moi dans ce type de scénarios):

  1. Spécifiez le contrôleur dans le modèle lui-même, en utilisant ngController.

  2. Chargez le modèle en utilisant un nom de vue générique (par exemple views/dashboard.html).

  3. Modifiez ce à quoi views/dashboard.html Fait référence en utilisant $templateCache.put(...) chaque fois que le rôle d'utilisateur connecté change.


Voici un exemple dépouillé de l'approche:

app.controller('loginCtrl', function ($location, $scope, User) {
    ...
    $scope.loginAs = function (role) {
        // First set the user role
        User.setRole(role);

        // Then navigate to Dashboard
        $location.path('/dashboard');
    };
});

// A simplified `User` service that takes care of swapping templates,
// based on the role. ("User" is probably not the best name...)
app.service('User', function ($http, $templateCache) {
    var guestRole = 'guest';
    var facadeUrl = 'views/dashboard.html';
    var emptyTmpl = '';
    var errorTmpl = 'Failed to load template !';
    var tempTmpl  = 'Loading template...';

    ...

    // Upon logout, put an empty template into `$templateCache`
    this.logout = function () {
        this.role = guestRole;
        $templateCache.put(facadeUrl, emptyTmpl);
    };

    // When the role changes (e.g. upon login), set the role as well as the template
    // (remember that the template itself will specify the appropriate controller) 
    this.setRole = function (role) {
        this.role = role;

        // The actual template URL    
        var url = 'views/' + role + '/dashboard.html';

        // Put a temporary template into `$templateCache`
        $templateCache.put(facadeUrl, tempTmpl);

        // Fetch the actual template (from the `$templateCahce` if available)
        // and store it under the "generic" URL (`views/dashboard.html`)
        $http.get(url, {cache: $templateCache}).
              success(function (tmpl) {
                  $templateCache.put(facadeUrl, tmpl);
              }).
              error(function () {
                  // Handle errors...
                  $templateCache.put(facadeUrl, errorTmpl);
              });
    };

    // Initialize role and template        
    this.logout();
});

// When the user navigates to '/dashboard', load the `views/dashboard.html` template.
// In a real app, you should of course verify that the user is logged in etc...
// (Here I use `ngRoute` for simplicity, but you can use any routing module.)
app.config(function ($routeProvider) {
    $routeProvider.
        when('/dashboard', {
            templateUrl: 'views/dashboard.html'
        }).
        ...
});

Voir aussi ceci courte démo.
(J'utilise ngRoute pour plus de simplicité, mais cela ne fait aucune différence puisque tout le travail est effectué par le User un service.)

3
gkalpak

Je sais que cela fait un moment que cette question n'a pas été publiée, mais j'ajoute ma réponse car la méthode que j'utilise est différente des autres réponses ici.

Dans cette méthode, je sépare complètement les URL de route et de modèle en fonction du rôle de cet utilisateur et redirige l'utilisateur vers la page d'indexation s'il se trouve dans une route qu'il n'est pas autorisé à afficher.

Avec UI Router, j'ajoute essentiellement un attribut de données comme celui-ci à l'état:

.state('admin', {
            url: "/admin",
            templateUrl: "views/admin.html",
            data: {  requireRole: 'admin' }
        })

Lorsque l'utilisateur est authentifié, je stocke ses données de rôle dans les localstorage et $rootscope du contrôleur comme ceci:

var role = JSON.stringify(response.data); // response from api with role details

// Set the stringified user data into local storage
localStorage.setItem('role', role);

// Putting the user's role on $rootScope for access by other controllers
$rootScope.role = response.data;

Enfin, j'utilise le $stateChangeStart pour vérifier le rôle et rediriger l'utilisateur si l'utilisateur n'est pas censé afficher la page:

.run(['$rootScope', '$state', function($rootScope, $state) {

        // $stateChangeStart is fired whenever the state changes. We can use some parameters
        // such as toState to hook into details about the state as it is changing
        $rootScope.$on('$stateChangeStart', function(event, toState) {

                var role = JSON.parse(localStorage.getItem('role'));
                $rootScope.role = role;

                // Redirect user is NOT authenticated and accesing private pages
                var requireRole = toState.data !== undefined
                                  && toState.data.requireRole;

                 if( (requireRole == 'admin' && role != 'admin')) )
                 {
                   $state.go('index');
                   event.preventDefault();
                   return;
                 }
     }

});

En plus de ce qui précède, vous devrez toujours effectuer une vérification d'autorisation côté serveur avant d'afficher des données à l'utilisateur.

1
Neel

Pas besoin d'une longue explication ici.

Utilisez résoudre et modifier $ route. $$ route.templateUrl, ou utilisez routeChangeError en passant la nouvelle route ou le paramètre pertinent à la promesse.

var md = angular.module('mymodule', ['ngRoute']);
md.config(function($routeProvider, $locationProvider) {
    $routeProvider.when('/common_route/:someparam', {
        resolve: {
            nextRoute: function($q, $route, userService) {
                defer = $q.defer()
                userService.currentRole(function(data) { defer.reject({nextRoute: 'user_based_route/'+data) });
                return defer.promise;
            }
        }
    });
    $rootScope.$on("$routeChangeError", function(evt, current, previous, rejection) {
      if (rejection.route) {
        return $location.path(rejection.route).replace();
      }
    });
});
1
Gepsens

Il y a un excellent projet https://github.com/Narzerus/angular-permission il a besoin de ui-router. Le projet est nouveau fonctionne néanmoins bien.

0
TJ_