En nuestro post anterior hicimos que nustra api pueda registrar usuarios e incriptar la contraseña, ahora vamos a iniciar sesión con los datos que dejo el usuario y obtener el token para posteriormente actualizar el perfil del usuario o realizar cualquier otra acción.
1.- Diagrama de flujo
Primero debemos entender los pasos que debe realizar nuestra api para que el login sea exitoso.
2.- Como configurar nuestro controller en SailsJs
Como vimos en el diagrama de flujo debemos crear en nuestro AccountController con la función login donde validaremos los campos, cuenta de emial y password.
3.- Validar campos
cuando un usuario hacer una petición a nuestra api debemos validar todos los campos que sean necesarios y evitar que nuestra aplicacion se reinice la validaciones para el login son las siguientes:
- Validar que el email sea valido es decir que contenga el nombre de usuario seguido por @ y el nombre del dominio.
- Validar que la contraseña sea mayor a 6 caracteres.
En nuestro controller creamos la función Login donde vamos a validar el email y password https://github.com/herel/api-login/blob/master/api/controllers/AccountController.js
module.exports = { login : function(req,res){ var params = req.allParams(); if(!params.email || !validEmail(params.email)) return res.send(401, { error : true, message : "El correo es obligatorio ó no es valido", status : 401 }); if(!params.password || params.password.length <= 5) return res.send(401, { error : true, message : "la contraseña es muy corta", status : 401 }); ... }, |
3.- Validar si el usuario existe en la base de datos.
Ahora debemos hacer la siguiente validación.
- Si el usuario no existe mandamos el error 404 indicando que el usuario no existe en la base de datos.
Por experiencia debemos crear un servicio que se encarge de pedir información a nuestra base de datos de forma nativa.
Para este paso usamos el servicio UserService donde buscamos el usuario por email (clave primaria) https://github.com/herel/api-login/blob/master/api/services/UserService.js
login : function(req,res){ var params = req.allParams(); if(!params.email || !validEmail(params.email)) return res.send(401, { error : true, message : "El correo es obligatorio ó no es valido", status : 401 }); if(!params.password || params.password.length <= 5) return res.send(401, { error : true, message : "la contraseña es muy corta", status : 401 }); async.waterfall([ function existUser(cb){ UserService.findByEmail(params.email) .then(function($user){ if(!$user) return cb(null,{ error : true, status : 404 , message : "El usuario no existe"}); return cb(null,$user); }).catch(cb); } ],function done(err,result){ if(err && err.status) return res.send(err.status,err); else if(err) return res.send(500, { error : true , message : "Internal server error", status : 500 }); return res.json(result); }); } |
Si analizamos la funcion findByEmail le pasamos como parametro el email del usuario, Si el usuario existe o no se ejecuta la función then que viene del promise del UserService.
En caso de que ocurra un error ya sea en el query o al conectarse a la base de datos se ejecuta la funcion catch que viene del promise del UserService, siempre que ocurra un error debemos mosrtar en la api el error que ocurrio y pudas notidicar en tu app web o app mobile lo que ha ocurrido.
Si la variable $user tiene el valor undefined o false significa que el usuario no existe y demos mostrar el error 404.
Si existe el usuario entonces pasamos a la siguiente función de waterfall para validar si la contraseña es correcta.
4.- Validamos la contraseña
¡Bien! el usuario si existe pero ahora falta la parte mas importante que el usuario ingrese la contraseña correcta para poder generar el token o si la contraseña no es correcta mostrar el error y evitar que los usuarios entren a nuestro sistema.
Previamente en el tutorial anterior instalamos bcrypt para encriptar la contraseña ahora debemos usar la funcion compare para validar si la contraseña que envia el usuario es la misma que esta en nuestra base de datos para esto en nuestro Servicio UserService creamos una funcion que validara la contraseña.
validPassword : function(hashdb,plainpassword){ return new Promise(function(resolve, reject) { bcrypt.compare(plainpassword, hashdb, function(err, res) { if (err) return reject({ error: true, message: 'Internal server error', status: 500 }) return resolve(res ? true : false) }) }) } |
Recuerda utilizar promise ya que si ocurre un error mientras se esta validando la contraseña ejecutamos la funcion reject con el error que asu vez ejecutara la funcion catch de donde llamamos a la funcion. si todo sale bien enviamos un true si la contraseña que esta en la base de datos es la misma que nos envia el usuario, en caso de que no sea correcta enviamos un false en la funcion resolve.
Ahora es turno de validar utilizar en nuestro controller el servicio que acabamos de crear https://github.com/herel/api-login/blob/master/api/controllers/AccountController.js
login : function(req,res){ var params = req.allParams(); if(!params.email || !validEmail(params.email)) return res.send(401, { error : true, message : "El correo es obligatorio ó no es valido", status : 401 }); if(!params.password || params.password.length <= 5) return res.send(401, { error : true, message : "la contraseña es muy corta", status : 401 }); async.waterfall([ function existUser(cb){ UserService.findByEmail(params.email) .then(function($user){ if(!$user) return cb(null,{ error : true, status : 404 , message : "El usuario no existe"}); return cb(null,$user); }).catch(cb); }, function validPass($user,cb){ UserService.validPassword($user.password,params.password) .then(function($valid){ if(!$valid) return cb({ error : true, status : 401 , message : "La contraseña no es correcta "}); return cb(null,$user); }) } ],function done(err,result){ if(err && err.status) return res.send(err.status,err); else if(err) return res.send(500, { error : true , message : "Internal server error", status : 500 }); return res.json(result); }); } |
¡Genial! la contraseña es correcta ahora pasamos a la siguiente funcion de nuestro waterfall
5.- Creamos el token y lo almacenamos en redis
Con mi experiencia y por rendimiento lo ideal es almacenar el token en redis ya sea en el mismo servidor donde tardara solo unos ms en validar si el token existe en la memoria ram, en caso de que usemos amazon y tengamos redis en otro servidor debemos configurarlo dentro de la misma zona asi podemos conectarnos a travez de la ip privada (intranet) y mejorar los tiempos de respuesta.
Tambien podemos almacenar los tokens en la base de datos pero por experienca si tenemos miles de usuarios haciendo peticiones la memoria ram de mongo se puede saturar y hacer que cada peticiones tarde mucho tiempo en contectar o incluso nunca conteste.
En nuestro tutorial anterior creamos un servicio llamado RedisService que se encarga de conectar y insertar u obtener algun registro de la memoria ram.
Vamos a crear un servicio llamado TokenService que se encargue de guardar el token.
var jwt = require("jsonwebtoken"); module.exports = { create : function(data,expire){ return new Promise(function(resolve, reject) { var create = moment().unix(); var expire = expire ? expire : moment() .add(6, "month").unix(); var dataToken = { userId: data._id.toString(), //id de mongo create: create, expire: expire }; var token = jwt.sign(dataToken, process.env.TOKEN_KEY); var expire = 3600 * 24 * 30; //30 dias dura el token RedisService.set("TOKEN::" + data._id, dataToken, expire) .then(function(data) { return resolve(token); }) .catch(function(e) { return reject(e); }); }) } } |
En redis almacenamos el key “SESSION::” seguido el userId de mongodb creamos las variables create y expiree que contiene cuando se creo y cuando expira en unix time esto nos puede servir mas adelante para validar que solo puedan iniciar en una sesion o multiples sesiones.
6.- Integramos nuestro servicio
¡Genial! ya tenemos nuestro servicio que se encarga de crear el token y guardarlo en redis ahora vamos a integrarlo en nuestro controller https://github.com/herel/api-login/blob/master/api/controllers/AccountController.js
login : function(req,res){ var params = req.allParams(); if(!params.email || !validEmail(params.email)) return res.send(401, { error : true, message : "El correo es obligatorio ó no es valido", status : 401 }); if(!params.password || params.password.length <= 5) return res.send(401, { error : true, message : "la contraseña es muy corta", status : 401 }); async.waterfall([ function existUser(cb){ UserService.findByEmail(params.email) .then(function($user){ if(!$user) return cb(null,{ error : true, status : 404 , message : "El usuario no existe"}); return cb(null,$user); }).catch(cb); }, function validPass($user,cb){ UserService.validPassword($user.password,params.password) .then(function($valid){ if(!$valid) return cb({ error : true, status : 401 , message : "La contraseña no es correcta "}); return cb(null,$user); }) }, function createToken($user,cb){ TokenService.create($user) .then(function($token){ delete $user.password; return cb(null, { user : $user, token : $token }) }).catch(cb); } ],function done(err,result){ if(err && err.status) return res.send(err.status,err); else if(err) return res.send(500, { error : true , message : "Internal server error", status : 500 }); return res.json(result); }); } |
Si el token se creo correctamente y se guardo en redis entonces borramos del usuario la contraseña ya que por seguridad nunca debemos mostrar la contraseña encriptada y respondemos el requests con el token que nos servira para validar las peticiones que haga el usuario posteriomente y mandamos el objeto del usuario con la informacion basica par que nuestra app pueda funcionar.
7.- configurar ruta
Ya casi, solo nos falta agregar la url donde el usuario debe hacer la petición para esto vamos a nuestro archivo route y agregamos la siguiente ruta. https://github.com/herel/api-login/blob/master/config/routes.js
'POST /v1.0/account/login' : 'AccountController.login' |
8.- haciendo pruebas con PostMan
Genial ahora solo nos toca hacer pruebas con postman y verificar que todo el proceso funciona correctamente.
si hacemos una peticion con la contraseña incorrecta.
o con un correo que no existe.
pero si todos los campos son validamos obtenemos el usuario y el token.
si hacemos nuevamente la peticion notaremos que nos regresa un token nuevo.
Gracias por leer y compartir este tutorial cuentame que te parecio y que podemos mejorar aqui algunos libros que me han ayudado a mejorar las buenas practicas a la hora de programar.