Paso 5: Autenticación y roles.

URL: https://github.com/Uniandes-isis2603/frontstepbystep.git
Release: git checkout -f paso-5

Aplicación

En este paso nos enfocamos en simular una autenticación en la aplicación para permitir hacer determinadas acciones dentro de ella. Es de aclarar que la simulación se hace únicamente en el FrontEnd de la aplicación. Una autenticación real conlleva muchos más pasos y detalles que incluyen al BackEnd.

Cuando vamos a dar clic en la opción de login nos aparecerá un estado asociado al login como el siguiente:

Iniciamos sesión con un usuario de prueba que es administrador y podrá hacer todas las acciones posibles.

En este caso, una vez ha iniciado sesión se puede observar en la parte superior derecha de la aplicación que aparece el nombre del usuario que inició sesión y la opción de logout.

En la siguiente imagen podemos ver a otro usuario que no es administrador tratando de crear un autor, pero, la aplicación le indica que no tiene permisos.

Antes de iniciar es importante hacer un cambio en la versión de nuestro angular-ui-router

Vaya al archivo bower.json y reemplace la dependencia de angular-ui-router por la siguiente "angular-ui-router": "~1.0.11". Este cambio se hace por compatibilidad de unos métodos que vamos a usar más adelante.

Requerimientos Nuevos

  1. Iniciar sesión y manejo de roles

Iniciar sesión

  1. Crear el template para login y logout
  2. Crear en el módulo loginModule el estado asociado a login y logout.
  3. Crear los controladores asociado a los estados.
  4. Actualizar el index.html para incluir los nuevos controladores y css.
  5. Actualizar app.js
  6. Actualizar template author.delete.html, authors.new.html
  7. Actualizar authors.mod.js

1. Crear el template para login y logout

El siguiente fragmento de código representa el template para el login que llamaremos como login.html en nuestra aplicación.

<div ng-if="!isAuthenticated()" class="container"> <form class="form-signin"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert()">{{alert.msg}} </alert> <h1 class="form-signin-heading text-muted">Sign In</h1> <input type="text" class="form-control" placeholder="Username" required="" autofocus="" ng-model="data.username"> <input type="password" class="form-control" placeholder="Password" required="" ng-model="data.password"> <select class="form-control" name="singleSelect" id="singleSelect" ng-model="data.rol" required="true"> <option value="">--- Please select rol ---</option> <!-- not selected / blank option --> <option value="admin">Administrator</option> <option value="assistant">Assistant</option> </select> <br> <button type="button" class="btn btn-lg btn-primary btn-block" ng-click="autenticar()"> Sign In </button> <a class="btn btn-lg btn-primary btn-block" ui-sref="booksList">Cancel</a> </form> </div>

Es importante notar que allí se hace uso también del ng-if que usamos en el paso anterior. También vemos que el formulario en el button de Sign In tenemos ng-click una directiva de angular para indicarle que vaya a un método en especifico en el controlador.

También otro cambio que notamos en la directiva ng-model es que tiene asociado el nombre de una variable en el scope llamada data que esta definida en el controlador.

El siguiente fragmento de código representa el template para el logout en nuestra aplicación.

<div class="center-block well text-center"> <span>You have been logged out</span> </div>

En este template vamos a indicarle al usuario que ha cerrado su sesión en nuestra aplicación.

2. Crear en el módulo loginModule el estado asociado a login y logout.

El módulo para el login y el logout sigue la misma convención de todos los módulos declarados hasta ahora en los anteriores pasos.

$stateProvider.state('login', { url: '/login', data: { requireLogin: false }, views: { 'mainView': { templateUrl: basePath + 'login.html', controller: 'loginCtrl' } } }).state('logout', { url: '/logout', data: { requireLogin: false, roles: [] } , views: { 'mainView': { templateUrl: basePath + 'logout.html', controller: 'logoutCtrl' } } });

Ahora, si ha detallado el código del módulo notamos que aparece algo nuevo para nosotros y es la variable data dentro de la definición del estado.

La variable data tiene dos propiedades que usaremos mas adelante. Por consiguiente, tengamos en cuenta que dentro de data están requiereLogin y roles.

3. Crear los controladores asociado a los estados.

Vamos a crear el controlador llamado login.ctrl que tendrá la siguiente lógica con un método llamado $scope.autenticar:

Lo primero que encontramos en el controlador es la definición de dos variables en el $scope

$scope.user = {}; $scope.data = {};

La variable $scope.user la vamos a usar para asignar los datos del usuario y la variable $scope.data es nuestra variable que vamos a usar para recoger cada uno de los valores ingresados en el formulario de login.

Luego, encontramos un $http con el método GET que nos lee una colección de usuarios que tenemos definidos en un JSON en la ruta data/users.json. Esto sigue la misma lógica que usamos en los primeros pasos cuando leíamos las colecciones de datos para mostrar la información de books, authors y editorials.

$http.get('data/users.json').then(function (response) { $scope.users = response.data; });

El método $scope.autenticar nos ayudará a validar la información que tenemos desde el formulario contra nuestra colección de usuarios.

$scope.autenticar = function () { var flag = false; for (var item in $scope.users) { if ($scope.users[item].user == $scope.data.username && $scope.users[item].password == $scope.data.password && $scope.users[item].rol == $scope.data.rol) { flag = true; $scope.user = $scope.users[item]; $state.go('booksList', {}, {reload: true}); break; } } if (!flag) { $rootScope.alerts.push({type: "danger", msg: "Incorrect username or password."}); } else { sessionStorage.token = $scope.user.token; sessionStorage.setItem("username", $scope.user.user); sessionStorage.setItem("name", $scope.user.name); sessionStorage.setItem("rol", $scope.user.rol); $rootScope.currentUser = $scope.user.name; } };

Lo lógica que sigue el método es:

  1. Validar que lo ingresado en el formulario coincida en usuario ($scope.data.username), contraseña ($scope.data.password) y rol ($scope.data.rol) con los usuarios que tenemos en el arreglo llamado $scope.users

  2. Cuando se ha cumplido la condición asignamos a $scope.user los valores asociados a ese usuario, y pedimos enviamos al usuario a un estado ya existente.

  3. Hacemos uso del sessionStorage para almacenar allí los valores del usuario.

Vamos a crear el controlador llamado logout.ctrl que tendrá la siguiente lógica:

if (sessionStorage.getItem("username")) { sessionStorage.clear(); } else { $state.go('booksList', {}, {reload: true}); }

Cuando analizamos el código notamos que validamos si en sessionStorage existe la propiedad username, es decir, nos indica si el usuario esta o no autenticado en nuestra aplicación. Si la condición se cumple hacemos sessionStorage.clear(); para cerrar sesión.

4. Actualizar el index.html para incluir los nuevos controladores y css.

<!-- Login--> <script src="src/modules/login/login.mod.js" type="text/javascript"></script> <script src="src/modules/login/login.ctrl.js" type="text/javascript"></script> <script src="src/modules/login/logout.ctrl.js" type="text/javascript"></script> <link rel="stylesheet" href="resources/css/login.css" />

5. Actualizar app.js

En el app.js tenemos los siguientes cambios que nos ayudarán a manejar la simulación de nuestro inicio de sesión en la aplicación.

  • Hacer uso del bloque de run que nos permite ejecutar fragmentos de código al iniciar la aplicación. Para mas detalles de run blocks puede ampliar la información en la documentación de AngularJS.

En este fragmento de código inyectamos $rootScope y $transitions.

app.run(['$rootScope', '$transitions', function ($rootScope, $transitions) {...
  • Hacemos uso del método onSuccess de $transtions que nos permite verificar que una vez se haya completado un estado satisfactoriamente podamos conocer todos los detalles del estado.
$transitions.onSuccess({to: '*'}, function (trans) { var $state = trans.router.stateService; var requireLogin = $state.current.data.requireLogin var roles = $state.current.data.roles

Ahora, si usted detalla el anterior fragmento de código tenemos una variable llamada $state que nos almacena todo lo que tiene determinado estado. En ese orden de ideas, tenemos declaradas las variables requiereLogin y roles que van a almacenar lo que contiene el estado en la variable data que habíamos definido arriba en el modulo de login

  • Definir dos métodos en el $rootScope que nos ayudarán a la simulación de la autenticación.
$rootScope.isAuthenticated = function () { if (sessionStorage.getItem("username") != null) { $rootScope.currentUser = sessionStorage.getItem("name"); return true; } else { return false; } }; $rootScope.hasPermissions = function () { if (($rootScope.isAuthenticated) && (roles.indexOf(sessionStorage.getItem("rol")) > -1)) { return true; } else { return false; } };

Explicación de los anteriores métodos, $rootScope.isAuthenticated permite verificar si en sessionStorage hay datos del usuario como el username devolviendo un true o false dependiendo del caso. $rootScope.hasPermissions permite verificar si el rol que tiene el usuario esta asociado a los roles que permite el estado.

Es decir que,

roles.indexOf(sessionStorage.getItem("rol")) > -1)

La variable roles es un arreglo con los roles permitidos para ese estado, y al obtener el rol del usuario verificamos que exista en ese arreglo.

  • El último fragmento de código que encontramos es el siguiente:
if (requireLogin && (sessionStorage.getItem("username") === null)) { event.preventDefault(); $state.go('login', $state.params); }

Allí validamos si el estado necesita o no de autenticación por medio de la variable requiereLogin y usamos el event.preventDefault(); detiene la acción predeterminada de un elemento, es decir, el método preventDefault() cancela el evento si es cancelable, lo que significa que la acción predeterminada que pertenece al evento no ocurrirá. Para más detalles puede consultar aquí. Dicho en otras palabras estamos impidiendo que se vaya a un estado que requiere login sin la autenticación realizada por el usuario.

Todo el código completo es como el siguiente:

app.run(['$rootScope', '$transitions', function ($rootScope, $transitions) { $transitions.onSuccess({to: '*'}, function (trans) { var $state = trans.router.stateService; var requireLogin = $state.current.data.requireLogin var roles = $state.current.data.roles $rootScope.isAuthenticated = function () { if (sessionStorage.getItem("username") != null) { $rootScope.currentUser = sessionStorage.getItem("name"); return true; } else { return false; } }; $rootScope.hasPermissions = function () { if (($rootScope.isAuthenticated) && (roles.indexOf(sessionStorage.getItem("rol")) > -1)) { return true; } else { return false; } }; if (requireLogin && (sessionStorage.getItem("username") === null)) { event.preventDefault(); $state.go('login', $state.params); } }); }]);

6. Actualizar template author.delete.html, authors.new.html

En los templates debemos hacer uso de los métodos que acabamos de crear para permitir o no determinadas acciones.

El siguiente fragmento de código muestra el mensaje de You do not have permission for this action. Si el usuario esta autenticado pero no tiene permisos. Este mismo fragmento de código se usa en ambos templates.

<div class="container" ng-if="isAuthenticated() && !hasPermissions()"> <div class="row justify-content-center"> <div class="col-md-12"> <h1 class="float-left display-3 mr-4">403 Forbidden</h1> <h4 >You do not have permission for this action.</h4> <p >Contact the administrator to obtain permission.</p> </div> </div> </div>

Dentro de nuestra etiqueta div vamos a usar nuevamente la directiva de ng-if. Para validar que esta autenticado y tiene los permisos para la acción.

<div ng-if="isAuthenticated() && hasPermissions()">

7. Actualizar authors.mod.js

La actualización en el módulo de authors se ve reflejada añadiendo al código la variable de data.

Por ejemplo:

.state('authorsCreate', { url: '/create', parent: 'authors', views: { 'detailView': { templateUrl: basePath + '/new/authors.new.html', controller: 'authorNewCtrl' } }, data: { requireLogin: true, roles: ['admin'] } }).state('authorUpdate', { url: '/update/{authorId:int}', parent: 'authors', param: { authorId: null }, views: { 'detailView': { templateUrl: basePath + '/new/authors.new.html', controller: 'authorUpdateCtrl' } }, data: { requireLogin: true, roles: ['admin', 'assistant'] } }).state('authorDelete', { url: '/delete/{authorId:int}', parent: 'authors', param: { authorId: null }, views: { 'detailView': { templateUrl: basePath + '/delete/author.delete.html', controller: 'authorDeleteCtrl' } }, data: { requireLogin: true, roles: ['admin'] } });

Si usted detalla el código vemos que para los estado create y delete esta el rol admin mientras que para el update incluimos el rol de assistant esto quiere decir que, el rol de assistant solo podrá editar en nuestra aplicación.

results matching ""

    No results matching ""