Vue.js y Firebase – Autenticar usuarios

En una publicación pasado habíamos visto como conectarnos a Firebase y así trabajar con su base de datos:

https://fernando-gaitan.com.ar/vue-js-y-firebase/

Sin embargo en aquel ejemplo cualquier usuario podía ingresar a la aplicación, y así realizar cualquier acción sobre los registros. En esta ocasión vamos a aprender cómo hacer para que un usuario inicie sesión, y pueda acceder a estos datos sólo si está autenticado.

Creando usuarios desde Firebase

http://firebase.com

Vamos a nuestra consola de Firebase, elegimos nuestro proyecto creado (yo había creado uno llamado Miko-Quir) y nos dirigimos en la barra derecha a “Authentication”. Luego a “Método de acceso” y habilitamos “Correo electrónico / contraseña”.

(El de Google no es necesario en este ejemplo, aunque en la imagen así lo esté)

Luego vamos a “Usuarios” y creamos uno con su correo electrónico y contraseña:

Reglas

En la publicación anterior no habíamos visto las reglas, por tanto cualquier persona tenía acceso a las compras; podía agregar, ver, modificar o eliminar cualquier registro.

Primero vamos a ir a “Database” y luego a “Reglas”.

Vamos a cambiar:

{
  "rules": {
    "compras": {
      ".read": true,
      ".write": true
    }
  }
}

Por lo siguiente:

{
  "rules": {
    "compras": {
      ".read": "auth.uid != null",
      ".write": "auth.uid != null"
    }
  }
}

Lo que significa que de ahora en adelante sólo los usuarios autentificados podrán tanto leer, como escribir en esta colección.

Iniciar y finalizar sesión

Vamos a nuestro archivo App.vue, y modificamos el controlador por el siguiente código:

import Hello from './components/Hello'
import Firebase from 'firebase'
let config = {
  //TU CONFIGURACIÓN
}
let app = Firebase.initializeApp(config);
let db = app.database();
let compras = db.ref('compras');
export default {
  name: 'App',
  firebase: {
    compras: compras
  },
  data(){
    return {
      is_signed: false,
      usuario: {
        email: '',
        contrasena: ''
      },
      compra_nueva: {
        nombre: '',
        cantidad: ''
      }
    }
  },
  created: function(){
    var that = this;
    Firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        that.is_signed = true;        
      } else {
        that.is_signed = false;
      }
    });
  },
  methods:{
    iniciarSesion: function(){
      var that = this;
      Firebase.auth().signInWithEmailAndPassword(that.usuario.email, that.usuario.contrasena).then(function(){
	toastr.success('Iniciaste sesión correctamente.', 'Aviso');
      }, function(){
	toastr.error('El correo o la contrasena son incorrectos.', 'Aviso');
      }).catch(function(error) {
	toastr.error('Error al intentar iniciar sesión.', 'Aviso');
      });
    },
    cerrarSesion: function(){
      Firebase.auth().signOut().then(function() {
        // Sign-out successful.
      }).catch(function(error) {
        toastr.error('Error al intentar cerrar sesión.', 'Aviso');
      });
    },
    agregar: function() {
      compras.push(this.compra_nueva, function(error){
        if (error){
          toastr.error('Error al intentar agregar el registro.', 'Aviso');
        }else{          
          toastr.success('Registro agregado correctamente.', 'Aviso');
        }
      });
      this.compra_nueva.nombre = '';
      this.compra_nueva.cantidad = ''; 
    },
    modificar: function(p_compra){      
      compras.child(p_compra['.key']).set({
        nombre: p_compra.nombre,
        cantidad: p_compra.cantidad
      }, function(error){
        if (error){
          toastr.error('Error al intentar modificar el registro.', 'Aviso');
        }else{          
          toastr.success('Registro modificado correctamente.', 'Aviso');
        }
      });      
    },
    eliminar: function(p_compra){
      compras.child(p_compra['.key']).remove(function(error){
        if (error){
          toastr.error('Error al intentar eliminar el registro.', 'Aviso');
        }else{          
          toastr.success('Registro eliminado correctamente.', 'Aviso');
        }
      });      
    },
    validarCompra: function(p_compra){
      return (
        p_compra.nombre.split(' ').join('') != '' &&
        !isNaN(parseInt(p_compra.cantidad, 10))
      );
    }
  }
}

Y la vista por:

<template>
  <div id="app" class="container">
    <div class="page-header">
      <h1> Lista de compras </h1>
    </div>
    <form v-if="is_signed">
      <button type="button" class="btn btn-danger" v-on:click="cerrarSesion()"> Cerrar sesión </button>
      <hr />
      <table class="table table-responsive">
        <thead>
          <tr>
            <th> Nombre </th>
            <th> Cantidad </th>
            <td>  </td>
          </tr>
        </thead>
        <tbody>
          <tr v-for="compra in compras">
            <td>
              <input type="text" class="form-control" placeholder="Ingrese el nombre del producto" v-model="compra.nombre" /> 
            </td>
            <td>
              <input type="number" class="form-control" placeholder="Ingrese la cantidad" v-model="compra.cantidad" /> 
            </td>
            <td>
              <a href="javascript:void(0);" title="Modificar" v-if="validarCompra(compra)" v-on:click="modificar(compra)">
                <span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
              </a>
              |
              <a href="javascript:void(0);" title="Eliminar" v-on:click="eliminar(compra)">
                <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
              </a>
            </td>
          </tr>
          <tr>
            <td> 
              <input type="text" class="form-control" placeholder="Ingrese el nombre del producto" v-model="compra_nueva.nombre" />
            </td>
            <td>  
              <input type="number" class="form-control" placeholder="Ingrese la cantidad" v-model="compra_nueva.cantidad" />
            </td>
            <td>
              <a href="javascript:void(0);" title="Agregar" v-if="validarCompra(compra_nueva)" v-on:click="agregar()">
                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
              </a>
            </td>
          </tr>
        </tbody>
      </table>
    </form>
    <form v-else class="form-signin" action="javascript:void(0)">
      <h2 class="form-signin-heading"> Iniciá sesión </h2>
      <label for="inputEmail" class="sr-only"> Email </label>
      <input type="email" id="inputEmail" class="form-control" placeholder="Email" v-model="usuario.email" />
      <label for="inputPassword" class="sr-only"> Contraseña </label>
      <input type="password" id="inputPassword" class="form-control" placeholder="Contraseña" v-model="usuario.contrasena">
      <button class="btn btn-lg btn-primary btn-block" type="submit" v-on:click="iniciarSesion()"> Ingresar </button>
    </form>
  </div>
</template>

Ahora vamos a pasar a explicar un poco el código.

En primer lugar creamos una propiedad ‘is_signed’, la cual va a cambiar dependiendo de si el usuario está logueado o no:

is_signed: false,

A su vez la vista, dependiendo del valor de esta propiedad mostrará el formulario de inicio de sesión, si es false, y un botón de cerrar sesión y la lista de compras si es true.

Dentro del método created() usamos onAuthStateChanged(). Este método va a escuchar los cambios del usuario, devolviendo un objeto con el estado actual del mismo. Por ejemplo, si el usuario aún no inició sesión, este método va a ejecutar un callback devolviendo un parámetro con el valor null, si el usuario en cambio ingresa con sus datos, va a volver a ejecutarse la acción, esta vez con los datos del usuario, y finalmente cuando éste finalice la sesión, devolviendo nuevamente null.

Y dependiendo de lo que nos devuelva este objeto cambiamos el valor de la propiedad ‘is_signed’, para indicarle a la vista si el usuario está logueado o no.

Firebase.auth().onAuthStateChanged(function(user) {
   if (user) {
     that.is_signed = true;
   } else {
     that.is_signed = false;
   }
});

Luego utilizamos el método signInWithEmailAndPassword() con los datos ingresados por el usuario (email y contraseña):

Firebase.auth().signInWithEmailAndPassword(that.usuario.email, that.usuario.contrasena).then(function(){
 toastr.success('Iniciaste sesión correctamente.', 'Aviso');
}, function(){
 toastr.error('El correo o la contrasena son incorrectos.', 'Aviso');
}).catch(function(error) {
 toastr.error('Error al intentar iniciar sesión.', 'Aviso');
});

El método then() va a dispararse después de la respuesta del servidor por iniciar sesión, éste recibirá dos parámetros, el primero va a dispararse si el inicio de sesión fue exitoso, mientras que el segundo lo hará si los datos son incorrectos. Y el método catch() en el caso de que haya otro tipo de error.

Y finalmente para cerrar sesión:

Firebase.auth().signOut().then(function() {
 // Cerró sesión. 
})
.catch(function(error) {
  toastr.error('Error al intentar cerrar sesión.', 'Aviso'); 
});

Registros por usuario

Hasta todo funciona, sin embargo, el problema que nos surge es que una vez que el usuario tiene acceso a la aplicación puede gestionar los registros de otros usuarios. Osea, yo puedo crear una compra y luego otro usuario puede acceder a la misma, modificar o eliminar el registro. Para ello vamos a hacer una serie de cambios que harán que cada compra se guarde como una colección de cada usuario.

Lo primero que vamos a hacer es lo siguiente: el método onAuthStateChanged() al recuperar el usuario (si es que éste inició sesión), nos permitirá acceder a este tipo de objeto, el cual contará con una propiedad llamada uid. Este dato no es más que un hash propio del usuario que permite identificarlo en la base de datos.

Así que primero vamos a eliminar las siguientes líneas:

let compras = db.ref('compras');

También vamos a eliminar:

firebase: {
   compras: compras
},

Esto es porque la lista de compras ahora dependerá directamente con el usuario que haya iniciado sesión, por lo cual ya no necesitamos la variable de referencia compras, sino que vamos a crear una nueva referencia cuando el usuario se autentifique.

También agregamos a data:

compras: []

Y el método created() lo reemplazamos por lo siguiente:

created: function(){
    var that = this;
    Firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        that.is_signed = true;
        that.$bindAsArray('compras', db.ref('compras/' + user.uid));
      } else {
        that.is_signed = false;
        that.compras = [];
      }
    });
},

De este forma el usuario no va a trabar con todas las compras, sino con sus propio registros:

that.$bindAsArray('compras', db.ref('compras/' + user.uid));

Lo siguiente que vamos a hacer es cambiar las reglas de firebase yendo a Database y luego a Reglas, y vamos a cambiar éstas por lo siguiente:

{
  "rules": {    
    "compras": {
      "$uid": {
        ".read": "auth.uid != null && auth.uid == $uid",
        ".write": "auth.uid != null && auth.uid == $uid"
      }
    }
  }
}

Si nos fijamos bien, cada compra va a estar encapsulada por medio del identificador de usuario ($uid)

Pero además las reglas tanto de lectura como escritura van a cambiar, osea que el usuario no sólo va a tener que estar autenticado, sino que también deben ser los registros creados por el mismo, y no podrá ni ver, ni modificar, ni eliminar los registros que no le pertenezcan.

A continuación dejo el componente App.vue con el código que vimos en esta ocasión:

Descargar ejemplo