Requisitos del post

  1. Conocer Node.js.
  2. Conocer lo mas básico de enrutamiento en Express.
  3. Conocer ES6 (en concreto, arrow functions y funciones de orden superior de arrays .every e .includes).
  4. Conocer que son las funciones de orden superior.

Al lio

Una de las ventajas en cuanto a mantenibilidad que nos ofrece el framework para Node.js Express es el uso de middlewares, que es un código que se ejecuta entre la petición y nuestro código final que vinculamos a nuestra ruta (lo llamaremos controlador).

Sirven para modificar el objeto req, para realizar comprobaciones antes de permitir el acceso con next o denegarlo con res, o usarlo simplemente como sistema de logeo (para ir guardando las visitas que se hacen en una determinada ruta, etc).

Normalmente su uso mas extendido es para generar sistemas de autorización, que generalmente suelen ser comprobaciones si el usuario esta logeado o no. Pero en cuanto nuestra aplicacion crece o necesita de un sistema de permisos un poco mas elaborado, esos middlewares que habíamos creado, o necesitan mutar en monstruos gigantes para contemplar todos los posibles casos de uso que pasen por ellos, o necesitamos crear un montón mas para la misma solución, y luego ir encadenandolos luego.

Vamos a poner un caso de uso:

  1. Tenemos una aplicación, con la capacidad de tener usuarios que inician sesión.
  2. En la misma aplicación los usuarios tienen jerarquías (administradores, usuarios comunes, invitados, etc).
  3. Hemos creado un sistema de permisos para controlar lo que pueden hacer esas jerarquías, que se asigna a cada usuario, para por ejemplo:

    • Ver su perfil.
    • Editar perfiles.
    • Ver el perfil de otros.
    • Acceder al panel de administración.
  4. Esa lista de permisos no son mas que un array de strings, que almacenaremos en la variable de sesión req.session.permissions.

El código

¿Como controlamos estos permisos con middlewares? Fácil, generamos tantos como permisos necesitemos:

/**
 * Aquí están nuestros middlewares, uno por cada caso de uso.
 * Su lógica es simple, comprueban si req.session.permissions incluye el permiso solicitado.
 * Si no existe, simplemente se devuelve un 403.
 **/

// Para ver usuarios
const canSeeProfile = (req, res, next) =>
  req.session.permissions.includes("see_profile")
    ? next()
    : res.send("Acceso denegado");

// Para editar usuarios
const canUpdateProfile = (req, res, next) =>
  req.session.permissions.includes("update_profile")
    ? next()
    : res.send("Acceso denegado");

// Para ver otros perfiles de usuario
const canSeeOtherUsersProfiles = (req, res, next) =>
  req.session.permissions.includes("see_other_users_profile")
    ? next()
    : res.send("Acceso denegado");

// Acceder al panel de adminsitrador
const canManage = (req, res, next) =>
  req.session.permissions.includes("can_manage")
    ? next()
    : res.send("Acceso denegado");

// Las rutas de nuestra aplicación
app.get("/perfil", canSeeProfile, seeProfile);
app.get("/editar-perfil", canUpdateProfile, seeProfile);
app.get("/usuario", canSeeOtherUsersProfiles, seeProfile);
app.get("/admin", canManage, seeProfile);
app.get("/comprobacion-multiple", canManage, canSeeProfile, seeProfile);

Los problemas

Obviamente contaremos que cada middleware estará separado en su archivo para dejar el archivo de rutas mas limpio, pero aun asi, tenemos una serie de problemas:

  1. La semana que viene se propondrán una lista de cambio que elevan el numero de permisos, a unos 50.
  2. Habrá que generar 46 middlewares mas.
  3. Habrá que encadenar en algunos casos de uso, esos middlewares para comprobar que tenga un grupo de permisos.
  4. Y habrá que mantener eso luego.

Como nos acabamos de dar cuenta, mantener un sistema asi es inviable, porque por mucho que tengamos bien definida al estructura de archivos y la forma de hacerlo.

La solución

Para reducir este problema, podemos hacer middlewares configurables. De hecho usando esta aproximación solo tendríamos que mantener un solo middleware. ¿Como se consigue eso? Fácil: Métodos que devuelven métodos.

De hecho, el nombre correcto seria middlewares de orden superior, ya que una función de orden superior es aquella que puede devolver otra función.

La idea es tener ciertos métodos, a los que pasarles argumentos (en este caso serian permisos), y que estos métodos devuelvan una función anónima, que acepta los parámetros req, res y next, ya que esta función anónima es el middleware que al final, se ejecutara, pero con esos "datos extra" que pasamos como argumentos.

Como creo que esto se explicara mejor, vamos a refactorizar el código superior:

/**
 * Aquí están nuestro único middlewares.
 * Misma lógica que los anteriores, comprueba si req.session.permissions incluye los permiso solicitados.
 * Si no existe, simplemente se devuelve un 403.
 **/
const checkPermissions = permissions => (req, res, next) =>
  permissions.every(permission => req.session.permissions.includes(permission))
    ? next()
    : res.send("Acceso denegado");

// Las rutas de nuestra aplicación
app.get("/perfil", checkPermissions(["see_profile"]), seeProfile);
app.get("/editar-perfil", checkPermissions(["update_profile"]), updateProfile);
app.get("/usuario", checkPermissions(["see_other_users_profile"]), usersList);
app.get("/admin", checkPermissions(["can_manage"]), adminPanel);
app.get("/comprobacion-multiple", checkPermissions(["can_manages", "see_profile"]), seeProfile);

Y ya esta. Como podemos ver, acabamos de reducir drásticamente la cantidad de código que necesitamos. De hecho, acabamos de ahorrarnos esos futuros 46 middlewares. Pero vamos a explicarlo un poco:

Comento checkPermissions para leerlo mejor:

// checkPermissions es una arrow function,
// que admite un parámetro que nosotros hemos elegido: "permissions"
const checkPermissions = permissions =>
  // Esta arrow function devuelve otra arrow function, que es el código del middleware.
  (req, res, next) =>
    // Y dentro del middleware, nuestro código, que usara el parámetro "permissions".
    // Aquí simplemente comprobamos que todos los permisos que hemos pasado por el parámetro,
    // tengan presencia en "req.session.permissions"
    permissions.every(permission => req.session.permissions.includes(permission))
      ? next()
      : res.send("Acceso denegado");
}

Obviamente podemos usar este formato para generar middlewares de otro tipo, pero la idea, creo que esta clara.

Aqui dejo un repositorio con una pequeña demo funcional para probar: demo-middleware-configurable