LIFWEB TP6 - Programmation Node.js avec hapi

Ce projet sur trois séances consiste à réaliser un service de réduction d’URL comme https://bit.ly/ ou https://tinyurl.com/. Il s’agit de réaliser :

  • D’une part un serveur d’API en Node.js,
  • D’autre part, un client AJAX qui propose une interface graphique sur cette API.

Introduction

🔖 Un projet de départ d’application est disponible sur le GitLab https://forge.univ-lyon1.fr/lifweb/lifweb-url-shortener. 🔖

👀 Un serveur de démonstration est disponible https://api.lifweb.os.univ-lyon1.fr. 👀

⚠️ Le serveur de démonstration est accessible uniquement depuis le campus (ou via VPN/relais) et utilise un certificat auto-signé. C’est une VM comme les autres de la forme lifweb-pxxxxxxx.lifweb.os.univ-lyon1.fr. ⚠️

Fonctionnalités

Le backend du service transforme une URL d’origine dite URL longue, comme https://perdu.com, en une URL réduite, par exemple http://localhost/WmFJQp qui redirigera vers l’URL d’origine lors d’une visite. Le service propose les routes suivantes, qui répondent des documents JSON (en-tête Content-Type: application/json) :

  • GET / accueil ;
  • GET /health informations générales sur le service ;
  • GET /api informations sur les liens, dont le nombre total ;
  • POST /api création d’un lien réduit à partir d’une URL longue ;
  • GET /api/{short}/status état d’un lien, dont le nombre de visites ;
  • DELETE /api/{short} suppression d’un lien.

La route suivante en revanche, ne répond pas de JSON, mais redirige l’utilisateur, on appelle cette action la visite du lien (court) :

  • GET /api/{short} redirige le lien raccourci vers l’URL longue d’origine.

Le frontend du service est un client AJAX dans le navigateur qui permet essentiellement de créer de nouveaux liens réduits. Il s’agit d’une stratégie client-side rendering où toute la construction du HTML et du DOM est faite du côté du navigateur, le serveur ne fournissant que des données JSON.

💡 Les deux parties backend / serveur et frontend / client sont indépendantes ne bloquez pas sur la partie serveur. 💡

Installation

⚠️ Faire un fork du projet de départ (bouton « fork » depuis l’interface web GitLab) https://forge.univ-lyon1.fr/lifweb/lifweb-url-shortener dans votre compte en gardant le même nom lifweb-url-shortener. ⚠️

⚠️ Votre fork devra être privé. Donnez des droits d’accès reporter à romuald.thion et à votre chargé de TP. ⚠️

Suivre le README.md du projet de départ pour l’installation et le premier lancement, en particulier la configuration de la base de données.

Mise-à-jour du projet

💡 Des modifications peuvent être apportées au projet d’origine (appelé dépôt amont 🇫🇷 / upstream 🇬🇧 ) que vous avez fork. 💡

Vous pouvez appliquer ces mises à jour sur votre propre dépôt via l’interface de GitLab. Si une mise à jour est disponible sur l’amont, vous aurez un message comme suit :

Mise-à-jour _upstream_ disponible

S’il n’y a pas de conflits, vous pouvez propager la mise-à-jour dans votre fork. Et vous aurez alors le message suivant.

pas de mise-à-jour _upstream_

Modalités de rendu

  • Le serveur complété est à déployer sur la VM fournie dans Tomuss pour le TP5.
    • Le serveur et le projet forge seront testés automatiquement le mardi 16 avril 2024, à 23:59.
    • Ne pas respecter les spécifications ou les consignes conduira à un échec.
  • Le client devra être montré au chargé de TP lors de la dernière séance de TP mardi 16 avril 2024.

Partie 1 : serveur

Dans cette partie, il n’y a pas encore de frontend, il s’agit uniquement du backend qui va recevoir des requêtes HTTP et répondre des contenus JSON. Il n’y a encore ni CSS, ni HTML, ni JS côté client.

À la fin de cette partie, on doit disposer d’un serveur qui a sensiblement les mêmes fonctionnalités que le serveur de démonstration https://api.lifweb.os.univ-lyon1.fr.

Exercice 1 : prise en main

Répondre aux questions de compréhension suivantes :

  • Donner la commande httpie pour créer un nouveau raccourci.
  • Donner les différences entre npm start et npm run prod.
  • Indiquer à quels moments la connexion à la base de données est ouverte est quand elle est fermée.
  • Ouvrir deux instances de l’application, une sur le port 3000 avec npm start et une autre sur le port 3001 avec la commande npx cross-env PORT=3001 NODE_ENV=development npx nodemon main.js.
  • Si on enregistre 2024 liens par heure, au bout de combien de temps aura-t-on 1% de chance d’avoir une collision, c’est-à-dire deux liens différents avec le même raccourci ?
  • Quels sont les différents niveaux supportés par server.log() ?
  • Comment exécuter les tests de tests/server.test.js ?

Exercice 2 : compléter les routes existantes

Compléter les routes existantes de l’API.

  • Ajouter la fonctionnalité qui permet de compter le nombre de fois où chaque lien est visité.
    • Pour cela, incrémenter l’attribut visit de la base de données à chaque visite.
    • Pour réaliser la fonctionnalité, ajouter une fonction dans db/index.js et modifier le contrôleur de GET /api/{short} pour l’utiliser.
  • Similairement, ajouter un attribut last_visited_at pour garder la date de dernière visite.
    • Ici, il faudra modifier en plus le schéma de la base de données pour ajouter l’attribut.
  • Implémenter la route /api/{short}/status
    • Retourner simplement tous les champs de la relation.
    • Si le lien demandé n’existe pas, retourner un code 404.

⚠️ Compléter tests/server.test.js et tester la mise-à-jour. ⚠️

Exercice 3 : fonctionnalités additionnelles

🚀 Vous pouvez sauter cet exercice en première intention et y revenir après. Il est assez indépendant. 🚀

Suppression de liens

Ajouter une fonctionnalité de suppression de lien raccourci sur la route DELETE /api/{short} On souhaite contrôler que seul le créateur d’un lien puisse le supprimer. Pour cela l’API fournit un code secret généré lors de la création du lien. Ce secret doit être communiqué lors de la requête de suppression pour que celle-ci réussisse.

  • Étendre la base de données avec un nouveau champ secret_key ;
  • Tirer un secret au hasard lors de la création d’un nouveau lien raccourci et l’enregistrer en base ;
  • Sur la route DELETE /api/{short} :
    • Si le lien demandé n’existe pas, retourner un code 404 ;
    • S’il n’y a pas d’attribut secret_key, retourner un code 400 ;
    • Si le secret ne correspond pas au lien demandé, retourner un code 401 ;
    • Sinon, supprimer de la base et renvoyer un code 200 avec les détails du lien supprimé.
  • Pour le traitement des cas d’erreurs précédents, adopter la stratégie fail fast, voir SO.

⚠️ Compléter tests/server.test.js et tester la nouvelle fonctionnalité. ⚠️

☣️ Faire attention à ne pas révéler le secret sur la route GET /api/{short}/status ! ☣️

Expiration de liens

Ajouter une date d’expiration optionnelle aux liens. Modifier la base de données et les routes pour que :

  • À la création du lien, on peut préciser un attribut expires_at de type date. Modifier le validateur de la route pour ajouter cet attribut optionnel.
  • Lors de la visite, si le lien est expiré, on renvoie un code 404, sinon on poursuit avec la redirection.
  • Ajouter un ramasse-miette qui périodiquement (e.g., toutes les 60 secondes) va supprimer tous les liens expirés de la base.

⚠️ Compléter tests/server.test.js et tester la nouvelle fonctionnalité. ⚠️

Exercice 4 (BONUS) : factorisation de code

À chaque fois qu’on visite une route /api/{short}/*, il faut vérifier que le lien existe. Utiliser l’événement onPreHandler du cycle de vie de la requête pour faire cette vérification et factoriser le code commun entre les routes qui utilisent request.params.short. On pourra déclarer le handler avec route.options.pre pour décorer la requête, voir la documentation.

⚠️ Vérifier l’absence de régressions avec les tests. ⚠️

Exercice 5 : déploiement de l’application

Reprendre l’exercice 2 et l’exercice 3 du TP5 pour déployer le serveur d’API sur votre VM depuis le compte non-privilégié gitlabci avec un service systemd. Il s’agit essentiellement de remplacer l’application du TP5 par la nouvelle.

⚠️ Configurer correctement le .env pour utiliser le serveur PostgreSQL local (variables PGHOST, PGPORT, PGDATABASE, PGUSER et PGPASSWORD). Penser à NODE_ENV=production. ⚠️

🚨 Vous aurez toutefois un problème : les liens générés par votre application vont être de la forme http://localhost:3000/{chaine_random} qui est l’adresse interne à la VM. On voudrait plutôt avoir l’URL publique https://lifweb-1xxxxxxx.lifweb.os.univ-lyon1.fr/{chaine_random} avec le nom de votre VM (noter le port par défaut et le https au lieu de http).🚨

Le serveur Nginx en reverse-proxy est la partie visible de l’extérieur qui redirige, en interne, vers l’application Node.js. Il faut donc transmettre l’adresse publique à votre application. Pour cela, utiliser la variable d’environnement FRONTEND et utiliser l’option du serveur hapi là où vous avez besoin de connaître l’adresse publique.

Exercice 6 (BONUS) : déploiement automatique via CI/CD GitLab

😎 Cet exercice vous permettra de gagner en efficacité en simplifiant considérablement le déploiement sur la VM. 😎

Accès de la VM à GitLab

Pour facilement mettre-à-jour votre application avec une nouvelle version depuis l’utilisateur gitlabci, on peut donner la clef publique au format SSH à GitLab pour permettre à gitlabci d’accéder en lecture au dépôt (en lecture seule). Pour cela :

  • Générer une clef publique à partir de la privée de votre utilisateur gitlabci avec la commande ssh-keygen -f gitlabci.pem -y. Le résultat est une clef de la forme ssh-rsa AAAAB3NzaC1yc2E....
  • Dans Deploy Key de votre projet, créer une nouvelle clef avec la clef publique précédente.
  • Dans le fichier /home/gitlabci/.ssh/config de la VM, configurer que git utilise la clef privée gitlabci.pem pour s’authentifier sur https://forge.univ-lyon1.fr/. Un exemple est donné sur la documentation de GitLab.

Maintenant, vous pouvez git clone et git pull directement depuis gitlabci et relancer l’application avec systemctl restart node.service depuis un compte privilégié. Pour aller plus loin, vous pouvez donner le droit d’exécuter cette commande à gitlabci en créant un fichier /etc/sudoers.d/99-node-service comme suit.

gitlabci ALL=NOPASSWD: /usr/bin/systemctl restart node.service

Accès de GitLab à la VM

Le GitLab https://forge.univ-lyon1.fr/ peut exécuter des scripts via sa chaîne d’intégration et de déploiement GitLab CI/CD. Ces scripts sont exécutés par un conteneur léger appelé runner lors des commits sur le dépôt.

  • Dans la phase d’intégration (Continuous Integration - CI), on exécutera typiquement des programmes comme ESlint ou Prettier via npm.
  • Pour la phase de déploiement (Continuous Delivery/Deployment - CD), il s’agit de pousser/tirer la dernière version du logiciel sur le serveur et de l’exécuter.

Ici, on utilisera un runner qui va se connecter à la VM, mettre à jour sa copie du dépôt et relancer le service. Pour cela, il faut que GitLab puisse se connecter à la VM. Pour cela :

  • Donner la clef privée de gitlabci à GitLab en la stockant dans une variable de type fichier qu’on nommera GITLABCI_SSH_PRIV_KEY. Le fichier nommé $GITLABCI_SSH_PRIV_KEY sera alors accessible depuis les scripts.
  • Ajouter le fichier .gitlab-ci.yml à la racine de votre projet GitLab.
    • Renommer le fichier en .gitlab-ci.yml (remplacer dot_ par un point .).
    • Adapter le avec le nom DNS de votre VM.
  • Dans l’onglet Build/Pipelines de votre projet, vous pourrez suivre l’exécution du script.
    • Si tout fonctionne, vous pouvez voir le contenu de /home/gitlabci/.ssh/authorized_keys dans le journal du job deploy-job
  • Adapter la commande passée à SSH dans le script pour que gitlabci fasse git pull au bon endroit et relancer le service.

Partie 2 : client

Il s’agit dans cette partie de créer un client de type Single Page Application (MDN) qui va utiliser l’API de la Partie 1 et faire le rendu complètement côté client. Vous pouvez utiliser votre propre serveur d’API ou à défaut celui fourni. Un exemple d’interface de client mis-en-forme avec https://bulma.io/ est proposé ci-dessous.

Capture d'écran du client JS

Exercice 7 : client AJAX de l’API

Le fichier HTML public/client.html et le fichier public/javascripts/app.js sont servis statiquement par hapi via le module inert. Les échanges entre le navigateur et le serveur se font en JSON sur l’API HTTP avec des fetch dans l’unique page client.html.

Chargez la page https://lifweb-12345678.lifweb.os.univ-lyon1.fr/client.html (en remplaçant 12345678 par votre numéro d’étudiant bien sûr) dans votre navigateur. Vous devriez voir apparaître la page HTML, mais le code JavaScript ne fonctionne pas encore.

Modifier le client de départ pour que la soumission du formulaire #create-link soit remplacée par une requête sur la route POST /api. Le code fourni utilise la variable apiServer qui doit être positionné (en haut du fichier app.js) sur votre serveur (ou éventuellement sur https://api.lifweb.os.univ-lyon1.fr/ si vous voulez tester votre client en passant par le corrigé enseignant du serveur). La page sera mise à jour selon le résultat du fetch :

  • En cas d’erreur (e.g., 400 si uri n’est pas une URL valide).
  • En cas de succès, afficher le lien raccourci.
  • Pour faciliter l’utilisation, quand un lien est généré avec succès, on proposera un bouton Copier l’URL qui copie le lien dans le presse-papier grâce à la Clipboard.writeText() de la Clipboard API.

Pour éviter une manipulation fastidieuse du DOM, la librairie lit-html standalone est installée et un exemple est donné. Cette bibliothèque propose un moteur de template qui permet de créer des éléments dont on utilisera surtout les expressions. Cette bibliothèque dispose d’une extension VSCode. L’alternative est d’utiliser des éléments HTML <templates> et de les cloner en JavaScript, comme dans l’exercice 3 du TP2-b.

⚠️ La page index.html pourrait être servie par un autre serveur que celui de l’API, par exemple avec Live Server. ⚠️

Exercice 8 : rendu CSS

Utiliser un framework CSS comme https://bulma.io/ ou https://purecss.io/ pour avoir un rendu graphique correct. A minima, utiliser https://simplecss.org/ ou un autre framework léger qui ne demande pas d’adaptation. Ces bibliothèques CSS peuvent être chargées dans le fichier HTML comme ci-après.

<head>
  <!-- Pour https://bulma.io/ -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css" />

  <!-- Pour purecss -->
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
    integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls"
    crossorigin="anonymous"
  />

  <!-- pour https://simplecss.org/ -->
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />
</head>

Exercice 8 (BONUS) : support des extensions par le client

Étendre le client pour gérer la fonctionnalité de suppression de lien et celle d’expiration lors de la saisie.