Go to Hackademy website

Une bibliothèque pour gérer l'authentification avec Vue.js, partie 2 — en route vers HTTP

Tom Panier

Posté par Tom Panier dans les catégories front

À l’heure où j’écris ces lignes, il m’est difficile de prévoir le temps qu’il fera quand vous les lirez. Laissons donc cette fois-ci les considérations météorologiques de côté et replongeons-nous directement dans ce qui nous intéresse : la suite et fin de la mise en place de notre bibliothèque d’authentification pour nos applications Vue.js !

La dernière fois, nous en étions restés à une implémentation propre et fonctionnelle, quoi qu’encore assez théorique : dans un cas du vrai monde, on a souvent besoin d’interagir avec un serveur HTTP, que ce soit pour se connecter ou simplement pour effectuer des requêtes authentifiées, forts du fait d’avoir un utilisateur connecté. C’est donc ce que nous allons voir… maintenant !

Récapitulatif stratégique

Le besoin de supporter un « nouveau » protocole d’authentification ne nous oblige en rien à jeter aux orties notre implémentation actuelle, qui reste fort pratique en développement : afin de pouvoir passer facilement de l’une à l’autre, nous allons employer un autre pattern bien connu, à savoir strategy. Derrière ce terme aux relents guerriers se cache simplement le fait d’abstraire l’implémentation concrète de l’orchestration, afin de pouvoir faire varier la première indépendamment de la seconde. Ça tombe bien, nous avons déjà réalisé cette abstraction dans la première partie : getAuthentication est un paramètre passé à l’initialisation de notre plugin (qui joue donc, ici, le rôle d’orchestrateur), que nous allons pouvoir très facilement faire varier — à la condition que son remplaçant respecte scrupuleusement la même API, cela va sans dire.

Avant toute chose, préparons le terrain avec un peu de renommage :

  • Le paramètre auth du plugin devient strategy (dans son propre fichier, ainsi que dans src/main.js) ;
  • src/getAuthentication.js devient src/strategies/getMockStrategy.js, le test éponyme devant être renommé et modifié en conséquence.

Nous allons également remanier légèrement le plugin afin que ce soit lui qui fasse appel à formatUser en lieu et place de getMockStrategy, pour que cette harmonisation des objets utilisateurs soit elle aussi agnostique de leur provenance. Ce faisant, nous allons centraliser ce comportement dans une nouvelle méthode nommée setUser, histoire de se mâcher le travail si ledit comportement vient à évoluer à l’avenir :

import formatUser from "./formatUser";

export default {
  install: (Vue, { strategy }) => {
    Vue.prototype.$auth = new Vue({
      data() {
        return {
          user: null
        };
      },

      methods: {
        setUser(user) {
          if (user) {
            this.user = formatUser(user);
          }
        },

        init() {
          return strategy.getConnectedUser()
            .then(user => {
              this.setUser(user);
            })
            .catch(console.error);
        },

        login(email, password) {
          return strategy.login(email, password)
            .then(user => {
              this.setUser(user);
            })
            .catch(console.error);
        },

        logout() {
          return strategy.logout()
            .then(() => {
              this.user = null;
            })
            .catch(console.error);
        }
      }
    });
  }
};

Si vous vous souvenez bien de la première partie de cet article, vous devriez aussi avoir constaté une différence non négligeable : les méthodes encapsulant l’authentification (via le paramètre strategy) font désormais usage de Promises.

Ah, mais oui ! Pourquoi donc ?

La raison est simple : lors de la mise en place de notre nouvelle stratégie, basée sur HTTP, nous allons devoir gérer une notion d’asynchronicité, les appels réseau étant ainsi gérés en JavaScript. De fait, nous devons faire en sorte d’être compatibles, donc autant le faire dès maintenant !

Plusieurs choses sont à noter concernant getMockStrategy :

  • Elle doit elle aussi utiliser des Promises pour être compatible avec cette nouvelle implémentation ;
  • Suite au retrait de l’utilisation de formatUser, la déclaration de la variable user peut passer de let à const (et le mock peut être ôté du test correspondant) ;
  • Le local storage contiendra désormais l’objet utilisateur non formatté — en cas de pépin, un petit localStorage.clear() devrait remettre les choses d’équerre.

Voyons à quoi ressemble le fichier modifié :

/**
 * @param {Object[]} users
 * @param {String}   [lsKey]
 *
 * @return {Object}
 */
export default function getMockStrategy(users, lsKey = "user") {
  return {
    /**
     * @return {Promise}
     */
    getConnectedUser() {
      const user = localStorage.getItem(lsKey);

      return Promise.resolve(user ? JSON.parse(user) : null);
    },

    /**
     * @param {String} email
     * @param {String} password
     *
     * @return {Promise}
     */
    login(email, password) {
      const user = users.find(user => user.email === email && user.password === password);

      if (user) {
        localStorage.setItem(lsKey, JSON.stringify(user));
      }

      return Promise.resolve(user || null);
    },

    /**
     * @return {Promise}
     */
    logout() {
      localStorage.removeItem(lsKey);

      return Promise.resolve();
    }
  };
}

Ainsi que ses tests, qui doivent désormais faire usage de l’instruction expect.assertions, les assertions étant résolues de manière asynchrone :

import getMockStrategy from "../../../src/strategies/getMockStrategy";

const strategy = getMockStrategy([{
  email: "martin.catty@delegateip.com",
  password: "Valipat01"
}]);

describe("getMockStrategy", () => {
  describe("login", () => {
    it("returns the logged-in user object upon success", () => {
      expect.assertions(1);

      strategy.login("martin.catty@delegateip.com", "Valipat01").then(user => {
        expect(user).toEqual({
          email: "martin.catty@delegateip.com",
          password: "Valipat01"
        });
      });
    });

    it("returns null upon failure", () => {
      expect.assertions(1);

      strategy.login("nobody@example.com", "nobody").then(user => {
        expect(user).toBeNull();
      });
    });
  });
});

N’oublions pas de modifier également AuthWrapper afin de tenir compte là aussi du caractère nouvellement asynchrone du processus de connexion :

<template>
  <div>
    <div v-if="$auth.user">
      <button type="button" @click="$auth.logout">Log out</button>
      <App />
    </div>
    <LoginForm v-else :errorMessage="errorMessage" @submit="login" />
  </div>
</template>

<script>
import App from "./App";
import LoginForm from "./LoginForm";

export default {
  data() {
    return {
      errorMessage: ""
    };
  },

  methods: {
    login({ email, password }) {
      this.$auth.login(email, password).then(() => {
        if (!this.$auth.user) {
          this.errorMessage = "Authentication failed, please try again";
        }
      });
    }
  },

  created() {
    this.$auth.init();
  },

  components: { App, LoginForm }
};
</script>

Les tests passent toujours ? Fort bien ! Nous pouvons enfin entrer dans le vif du sujet.

Vers HTTP et au-delà

Occupons-nous donc de mettre en place une API HTTP minimaliste pour développer la stratégie idoine en local. Nous allons utiliser Express :

$ npm i express --save-dev

Créons ensuite un fichier server.js à la racine de notre projet :

const express = require("express");

const app = express();
const port = 3000;

const users = [
  {
    email: "jim@example.com",
    password: "j1m"
  },
  {
    email: "bob@example.com",
    password: "b0b"
  }
];

let connectedUser = null;

function sendConnectedUser(res) {
  if (!connectedUser) {
    res.sendStatus(401);
    return;
  }

  res.send({ email: connectedUser.email });
}

app.use(express.json());

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");

  next();
});

app.get("/connected", (req, res) => {
  sendConnectedUser(res);
});

app.post("/login", (req, res) => {
  connectedUser = users.find(user => user.email === req.body.email && user.password === req.body.password) || null;

  sendConnectedUser(res);
});

app.get("/logout", (req, res) => {
  connectedUser = null;

  res.end();
});

app.listen(port, () => console.log("API started at http://localhost:" + port));

Cette implémentation sommaire mais pragmatique de nos trois endpoints devrait faire l’affaire ! Testons-la en console comme des barbus :

$ curl http://localhost:3000/connected
Unauthorized
$ curl http://localhost:3000/login -H "Content-Type: application/json" -d '{"email":"jim@example.com","password":"j1m"}'
{"email":"jim@example.com"}
$ curl http://localhost:3000/connected
{"email":"jim@example.com"}
$ curl http://localhost:3000/logout
$ curl http://localhost:3000/connected
Unauthorized

Parfait ! Passons ensuite à ce qui nous intéresse vraiment ici : la rédaction de la stratégie HTTP pour notre bibliothèque. Cela se passera dans le fichier src/strategies/getHTTPStrategy.js :

/**
 * @param {Object} config
 * @param {String} config.connectedURL
 * @param {String} config.loginURL
 * @param {String} config.logoutURL
 *
 * @return {Object}
 */
export default function getHTTPStrategy({ connectedURL, loginURL, logoutURL }) {
  return {
    /**
     * @return {Promise}
     */
    getConnectedUser() {
      return fetch(connectedURL)
        .then(response => {
          if (response.ok) {
            return response.json();
          }

          if (response.status !== 401) {
            throw new Error(response.status);
          }

          return null;
        });
    },

    /**
     * @param {String} email
     * @param {String} password
     *
     * @return {Promise}
     */
    login(email, password) {
      return fetch(loginURL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password })
      })
        .then(response => {
          if (!response.ok) {
            throw new Error(response.status);
          }

          return response.json();
        });
    },

    /**
     * @return {Promise}
     */
    logout() {
      return fetch(logoutURL);
    }
  };
}

Quelles observations pouvons-nous faire, à chaud, sur cet ajout à notre bibliothèque ?

  • Contrairement à getMockStrategy, cette factory prend en paramètre les URLs des différents endpoints avec lesquels elle devra interagir ;
  • On ne fait rien en cas de réponse de type 401 Unauthorized lors de l’appel à /connected, car c’est une réponse « normale » : elle signifie simplement que notre utilisateur n’est pas (encore) connecté.

En mockant fetch, on peut assez simplement écrire des tests pour cette stratégie, en créant test/specs/getHTTPStrategy.spec.js :

import getHTTPStrategy from "../../../src/strategies/getHTTPStrategy";

function mockFetch(data, params) {
  return jest.fn().mockImplementation(() => Promise.resolve({ json: () => data, ...params }));
}

const strategy = getHTTPStrategy({ connectedURL: "", loginURL: "", logoutURL: "" });

describe("getMockStrategy", () => {
  describe("getConnectedUser", () => {
    it("returns the logged-in user object upon success", () => {
      global.fetch = mockFetch({ email: "jim@example.com" }, { ok: true });

      expect.assertions(1);

      strategy.getConnectedUser().then(user => {
        expect(user).toEqual({ email: "jim@example.com" });
      });
    });

    it("does nothing upon 401", () => {
      global.fetch = mockFetch(null, { ok: false, status: 401 });

      expect.assertions(1);

      strategy.getConnectedUser().then(user => {
        expect(user).toBeNull();
      });
    });

    it("throws upon other error", () => {
      global.fetch = mockFetch(null, { ok: false, status: 418 });

      expect.assertions(1);

      strategy.getConnectedUser().catch(error => {
        expect(error.message).toEqual("418");
      });
    });
  });

  describe("login", () => {
    it("returns the logged-in user object upon success", () => {
      global.fetch = mockFetch({ email: "jim@example.com" }, { ok: true });

      expect.assertions(1);

      strategy.login("jim@example.com", "j1m").then(user => {
        expect(user).toEqual({ email: "jim@example.com" });
      });
    });

    it("throws upon 401", () => {
      global.fetch = mockFetch(null, { ok: false, status: 401 });

      expect.assertions(1);

      strategy.login("nobody@example.com", "nobody").catch(error => {
        expect(error.message).toEqual("401");
      });
    });

    it("throws upon other error", () => {
      global.fetch = mockFetch(null, { ok: false, status: 418 });

      expect.assertions(1);

      strategy.login("nobody@example.com", "nobody").catch(error => {
        expect(error.message).toEqual("418");
      });
    });
  });
});

Le seul intérêt réel de ce mock est de gérer l’appel à la méthode json du « vrai » fetch, à laquelle on fait ici retourner le contenu désiré (en l’occurrence le premier argument de notre fonction mockFetch).

Maintenant que notre bibliothèque supporte une utilisation « réelle » en HTTP, nous allons pouvoir mettre à profit les fruits de notre travail jusqu’ici afin de lui permettre d’effectuer des requêtes authentifiées.

Appelez couverts !

Faciliter les requêtes HTTP authentifiées est l’intérêt principal de tout ce que nous avons fait pour le moment, en tirant parti du fait que nous avons des informations utilisateur fiables côté front. Pour ce faire, nous allons « enrober » fetch afin d’en obtenir une version qui gérera cet aspect de manière transparente.

L’intérêt de cet « enrobage » est multiple, puisqu’il va également nous permettre de simplifier l’API originelle de fetch, laquelle est très complète mais assez bas niveau et donc assez peu pratique dans le cadre d’un usage répété. Nous allons notamment partir du principe que toutes nos interactions avec le serveur seront basées sur du JSON, et faire en sorte que les cas d’erreurs HTTP (codes 4xx et 5xx) soient traités comme tels, afin de rendre leur traitement homogène avec celui de toutes les autres erreurs pouvant survenir.

Commençons donc par-là ! Nous allons en premier lieu créer un nouveau service nommé buildFetchParams, afin d’abstraire le formatage du second argument de fetch, à savoir son objet de paramétrage :

/**
 * @param {Object} [params]
 *
 * @return {Object}
 */
export default function buildFetchParams(params = {}) {
  return {
    ...params,
    ..."body" in params && { body: JSON.stringify(params.body) },
    headers: {
      ..."headers" in params && params.headers,
      "Content-Type": "headers" in params && "Content-Type" in params.headers ? params.headers["Content-Type"] : "application/json"
    }
  };
}

Nous faisons bien en sorte, comme évoqué plus haut, d’ajouter un header Content-Type désignant un contenu JSON si celui-ci n’a pas été préalablement spécifié. Nous prenons également en charge la transformation du corps de la requête (un objet JS) en chaîne de caractères (le JSON en question), afin de ne plus requérir le fait de l’écrire à chaque fois.

Nous créons ce service à part pour deux raisons :

  • Nous souhaitons pouvoir éventuellement l’utiliser dans d’autres contextes si c’est pertinent (avec une bibliothèque tierce qui s’appuie sur fetch en interne, par exemple) ;
  • Nous voulons également pouvoir tester et valider le formatage des paramètres à part : si nous faisions ce traitement directement dans notre fetch « enrobé », nous n’aurions pas de moyen simple de vérifier cette partie en particulier puisque le résultat ne serait pas disponible à l’extérieur de la fonction. L’écriture des tests ne doit évidemment pas influer sur l’implémentation, mais puisque la première raison existe, on ne va pas se priver de les rendre plus fiables ;)

Puisqu’on en parle, testons-le !

import buildFetchParams from "../../src/buildFetchParams";

describe("buildFetchParams", () => {
  it("stringifies the request body, if any", () => {
    expect(buildFetchParams({
      body: { foo: "bar" }
    }).body).toEqual("{\"foo\":\"bar\"}");
  });

  it("declares a JSON content type header", () => {
    expect(buildFetchParams().headers).toEqual({
      "Content-Type": "application/json"
    });
  });

  it("preserves existing content type, if any", () => {
    expect(buildFetchParams({
      headers: { "Content-Type": "text/html" }
    }).headers).toEqual({
      "Content-Type": "text/html"
    });
  });

  it("preserves other headers, if any", () => {
    expect(buildFetchParams({
      headers: { "Accept": "text/html" }
    }).headers).toEqual({
      "Accept": "text/html",
      "Content-Type": "application/json"
    });
  });

  it("preserves other params, if any", () => {
    expect(buildFetchParams({
      method: "POST"
    })).toEqual({
      headers: { "Content-Type": "application/json" },
      method: "POST"
    });
  });
});

Cet aspect étant géré, écrivons maintenant notre service fetchJSON :

import buildFetchParams from "./buildFetchParams";

/**
 * @param {String} url
 * @param {Object} [params]
 *
 * @return {Promise}
 */
export default function fetchJSON(url, params) {
  return fetch(url, buildFetchParams(params))
    .then(response => {
      if (!response.ok) {
        throw new Error(response.status);
      }

      return response.json();
    });
}

Au-delà d’appeler buildFetchParams, celui-ci répond bien à notre second objectif : faire en sorte de traiter les cas d’erreurs HTTP comme les autres erreurs, afin de pouvoir les catch en fin de chaîne.

Là encore, écrivons les tests qui vont bien :

import fetchJSON from "../../src/fetchJSON";

function mockFetch(data, params) {
  return jest.fn().mockImplementation(() => Promise.resolve({ json: () => data, ...params }));
}

describe("fetchJSON", () => {
  it("performs an HTTP request and parses the JSON response payload upon success", () => {
    global.fetch = mockFetch({ foo: "bar" }, { ok: true });

    expect.assertions(1);

    fetchJSON("").then(data => {
      expect(data).toEqual({ foo: "bar" });
    });
  });

  it("throws upon failure", () => {
    global.fetch = mockFetch({ foo: "bar" }, { ok: false, status: 500 });

    expect.assertions(1);

    fetchJSON("").catch(error => {
      expect(error.message).toEqual("500");
    });
  });
});

Notez la réutilisation de mockFetch, afin là aussi de décorréler nos tests de toute interaction avec le réseau. Nous pourrions éventuellement envisager d’en faire un module à part, mais nous allons nous en abstenir ici pour ne pas complexifier cet exemple outre mesure. En revanche, nous ne mockons pas buildFetchParams : ce serait peu pertinent à écrire, nous faisons donc le choix de considérer que son utilisation est un détail d’implémentation de ce service.

Bien que ce ne soit pas notre but premier, nous pouvons utiliser fetchJSON dans getHTTPStrategy afin de simplifier le code de ce dernier :

import fetchJSON from "../fetchJSON";

/**
 * @param {Object} config
 * @param {String} config.connectedURL
 * @param {String} config.loginURL
 * @param {String} config.logoutURL
 *
 * @return {Object}
 */
export default function getHTTPStrategy({ connectedURL, loginURL, logoutURL }) {
  return {
    /**
     * @return {Promise}
     */
    getConnectedUser() {
      return fetchJSON(connectedURL)
        .catch(error => {
          if (error.message !== "401") {
            throw error;
          }

          return null;
        });
    },

    /**
     * @param {String} email
     * @param {String} password
     *
     * @return {Promise}
     */
    login(email, password) {
      return fetchJSON(loginURL, {
        method: "POST",
        body: { email, password }
      });
    },

    // ...
  };
}

Passons maintenant aux choses sérieuses ! Dans le contexte de notre API d’exemple, nous allons partir du principe que nos objets utilisateurs contiennent une clé token, dont la valeur correspondante contient, comme de raison, un jeton qu’il nous faut envoyer avec les requêtes. Modifions donc server.js, en faisant au passage en sorte d’accepter le header Authorization, dont nous allons avoir besoin :

// ...

const users = [
  {
    email: "jim@example.com",
    password: "j1m",
    token: "hellodarknessmyoldfriend"
  },
  {
    email: "bob@example.com",
    password: "b0b",
    token: "ivecometotalktoyouagain"
  }
];

let connectedUser = null;

function sendConnectedUser(res) {
  if (!connectedUser) {
    res.sendStatus(401);
    return;
  }

  res.send({
    email: connectedUser.email,
    token: connectedUser.token
  });
}

app.use(express.json());

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");

  next();
});

// ...

Et prenons ce nouveau champ token en compte dans src/formatUser.js :

// ...

/**
 * @param {Object} user
 *
 * @return {Object}
 */
export default function formatUser(user) {
  return {
    email: user.email,
    displayName: getDisplayName(user),
    token: user.token
  };
}

Sans oublier d’impacter les tests de ce dernier :

import formatUser from "../../src/formatUser";

describe("formatUser", () => {
  // ...

  it("takes token into account", () => {
    expect(formatUser({
      email: "jim@example.com",
      token: "itscomingrightforus!"
    })).toEqual({
      email: "jim@example.com",
      displayName: "jim@example.com",
      token: "itscomingrightforus!"
    });
  });
});

Nous allons ensuite faire appel à fetchJSON dans notre plugin :

import formatUser from "./formatUser";
import fetchJSON from "./fetchJSON";

export default {
  install: (Vue, { strategy }) => {
    Vue.prototype.$auth = new Vue({
      data() {
        return {
          user: null
        };
      },

      methods: {
        // ...

        fetch(url, params) {
          return fetchJSON(url, {
            ...params || {},
            headers: {
              ...params && params.headers ? params.headers : {},
              "Authorization": `Bearer ${this.user.token}`
            }
          });
        }
      }
    });
  }
};

J’attire votre attention sur quelques points quant à cet ajout :

  • On part du principe que this.user est toujours disponible, puisqu’on n’effectuera aucun appel avant que l’utilisateur soit authentifié ;
  • On ajoute le header Authorization tel que prévu ; là aussi, on aurait pu isoler cette logique ailleurs afin de pouvoir l’utiliser dans un autre contexte.

Dès lors, nous pouvons appeler this.$auth.fetch depuis n’importe lequel de nos composants pour effectuer simplement une requête HTTP authentifiée ! Pour nous en assurer, testons ça directement en ajoutant un appel d’exemple dans le hook created du composant HelloWorld :

export default {
  // ...

  created() {
    this.$auth.fetch("/");
  }
};

Si nous inspectons la requête via les outils de développement du navigateur, nous constatons bien la présence de notre token !

Le token est présent dans la requête HTTP

« No » means « no »!

Je vous propose de finir en beauté en faisant en sorte de déconnecter automatiquement notre utilisateur si une requête HTTP nous revient avec un code 401 Unauthorized : en pratique, nous réinitialiserons l’objet idoine, ce qui aura pour effet d’afficher de nouveau le formulaire de connexion.

Afin de tester facilement, ajoutons un endpoint dédié à server.js :

// ...

app.get("/unauthorized", (req, res) => {
  res.sendStatus(401);
});

// ...

Ainsi qu’un bouton permettant de l’appeler directement dans le composant HelloWorld :

<template>
  <div class="hello">
    <h1></h1>
    <button type="button" @click="unauthorized">Unauthorized</button>
    <!-- ... -->
  </div>
</template>

<script>
export default {
  // ...

  methods: {
    unauthorized() {
      this.$auth.fetch("http://localhost:3000/unauthorized")
        .catch(error => {
          if (error.message !== "401") {
            console.error(error);
            throw error;
          }

          this.$auth.logout();
        });
    }
  }
};
</script>

Ce premier exemple nous permet de constater la facilité à obtenir le comportement voulu localement ; pour preuve, dupliquons tout ça pour, cette fois-ci, confronter notre application à une réponse 404 Not Found :

// ...

app.get("/not-found", (req, res) => {
  res.sendStatus(404);
});

// ...

C’est un peu bête d’écrire un endpoint exprès pour une 404, non ?

Certes ! Nous pourrions en effet obtenir une telle réponse en appelant une URL inexistante sur notre serveur, mais la réponse ainsi obtenue ne serait pas configurée de manière homogène concernant les CORS ; nous faisons donc au plus simple, ne s’agissant que d’utilitaires de développement.

<template>
  <div class="hello">
    <h1></h1>
    <button type="button" @click="unauthorized">Unauthorized</button>
    <button type="button" @click="notFound">Not found</button>
    <!-- ... -->
  </div>
</template>

<script>
export default {
  // ...

  methods: {
    // ...

    notFound() {
      this.$auth.fetch("http://localhost:3000/not-found")
        .catch(error => {
          if (error.message !== "401") {
            console.error(error);
            throw error;
          }

          this.$auth.logout();
        });
    }
  }
};
</script>

En testant nos deux boutons, on constate bien que l’erreur 401 déconnecte l’utilisateur, alors que la 404 est simplement affichée en console.

Généralisons maintenant simplement ce comportement au niveau du plugin :

// ...

export default {
  install: (Vue, { strategy }) => {
    Vue.prototype.$auth = new Vue({
      // ...

      methods: {
        // ...

        fetch(url, params) {
          return fetchJSON(url, {
            // ...
          })
            .catch(error => {
              if (error.message !== "401") {
                throw error;
              }

              this.$auth.logout();
            });
        }
      }
    });
  }
};

Notre gestion d’erreur spécifique devient dès lors beaucoup plus simple !

export default {
  // ...

  methods: {
    unauthorized() {
      this.$auth.fetch("http://localhost:3000/unauthorized")
        .catch(console.error);
    },

    notFound() {
      this.$auth.fetch("http://localhost:3000/not-found")
        .catch(console.error);
    }
  }
};

Ce fonctionnement harmonieux a toutefois un bémol : le composant racine étant « démonté » en cas de déconnexion, l’état applicatif courant est perdu du même coup ; décorréler la gestion de cet état de la couche présentation via une solution telle que Vuex par exemple, ou encore l’utilisation du local storage, peuvent permettre de pallier ce défaut en offrant une expérience réellement transparente à l’utilisateur, qui n’aura dans notre exemple qu’à cliquer de nouveau sur le bouton !

Emballage

Le développement en lui-même est terminé ! Il ne nous reste plus qu’à transformer notre application d’exemple en package NPM digne de ce nom.

Commençons par exposer ses différentes composantes (notamment les modules référencés dans src/main.js) dans un point d’entrée central, à savoir index.js (placé directement à la racine du projet) :

import AuthPlugin from "./src/plugin";
import AuthWrapper from "./src/components/AuthWrapper";
import getHTTPStrategy from "./src/strategies/getHTTPStrategy";
import getMockStrategy from "./src/strategies/getMockStrategy";
import buildFetchParams from "./src/buildFetchParams";
import fetchJSON from "./src/fetchJSON";

export {
  AuthPlugin,
  AuthWrapper,
  getHTTPStrategy,
  getMockStrategy,
  buildFetchParams,
  fetchJSON
};

Il nous faut bien évidemment exposer les deux stratégies d’authentification, afin de permettre au consommateur du paquet d’utiliser l’une ou l’autre ; nous ajoutons également buildFetchParams et fetchJSON pour les rendre exploitables dans des contextes variés ainsi que nous l’avions envisagé.

Nous pouvons également indiquer le chemin vers ce point d’entrée dans le champ main de package.json (index.js, tout simplement, ce qui est en réalité sa valeur par défaut) ; nous devons aussi ôter le champ private de ce même fichier, afin d’autoriser la publication sur NPM !

Enfin, afin de faire en sorte que le paquet n’embarque que le code nécessaire mais que l’application d’exemple reste versionnée (afin de faciliter les développements ultérieurs), nous allons ajouter un fichier .npmignore, lui aussi à la racine du projet :

!index.js
!src/components/AuthWrapper.vue
!src/components/LoginForm.vue
!src/strategies/getHTTPStrategy.js
!src/strategies/getMockStrategy.js
!src/buildFetchParams.js
!src/fetchJSON.js
!src/formatUser.js
!src/plugin.js

Et voilà ! Vous pouvez désormais publier votre belle bibliothèque toute neuve sur votre registry NPM privé, ou encore l’installer directement depuis un repository Git, et l’utiliser dans vos applications !

End of the road

J’espère de tout cœur que ce double article vous aura plu et que vous y aurez appris quelque chose ! Je vous donne rendez-vous très prochainement pour de nouvelles pérégrinations dans le monde merveilleux — ou pas — de JavaScript — ou pas. D’ici là, restez au chaud, et bonne année !


L’équipe Synbioz.
Libres d’être ensemble.

Articles connexes

Jeu de la vie et rendus de la mort

09/09/2019

Bonjour à tous, aujourd’hui je vous propose de revoir un classique du monde du développement, le jeu de la vie. Automate cellulaire plus qu’un vrai jeu, c’est avant tout un algorithme qui va nous...

Houdini, CSS by JS

21/03/2019

Bonjour à tous, bienvenue dans le monde magique de l’illusion et des faux-semblants, où un background peut souvent en cacher un autre. Que le rideau se lève le temps d’apercevoir ce qui se cache...

Architecture en trois tiers d'une application Vue.js

21/02/2019

Ça commence à faire un petit moment que je vous bassine avec Vue.js, et que je vous fais construire des single-page applications en s’appuyant dessus. Néanmoins, nous n’avons jamais réellement parlé...

Dark mode et CSS

24/01/2019

Bonjour à tous, aujourd’hui un sujet (presque) d’actualité puisque nous allons parler du mode sombre de MacOS, mais surtout d’une nouvelle manière — assez radicale — de penser nos interfaces. Le...