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 :
S’il n’y a pas de conflits, vous pouvez propager la mise-à-jour dans votre fork. Et vous aurez alors le message suivant.
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.
- Si vous n’avez pas codé le serveur, utilisez https://api.lifweb.os.univ-lyon1.fr
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
etnpm 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 commandenpx cross-env PORT=3001 NODE_ENV=development npx nodemon main.js
.- Créer un lien sur la première instance http://localhost:3000/ et ensuite un autre sur la seconde instante http://localhost:3001/.
- Les liens de l’un doivent être visibles avec l’autre. Pourquoi ?
- 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 ?
- Voir pour cela https://zelark.github.io/nano-id-cc/. La longueur est configurable avec la variable d’environnement
LINK_LENGTH
.
- Voir pour cela https://zelark.github.io/nano-id-cc/. La longueur est configurable avec la variable d’environnement
- 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 deGET /api/{short}
pour l’utiliser.
- Pour cela, incrémenter l’attribut
- 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 commandessh-keygen -f gitlabci.pem -y
. Le résultat est une clef de la formessh-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 quegit
utilise la clef privéegitlabci.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 nommeraGITLABCI_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
(remplacerdot_
par un point.
). - Adapter le avec le nom DNS de votre VM.
- Renommer le fichier en
- 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 jobdeploy-job
- Si tout fonctionne, vous pouvez voir le contenu de
- Adapter la commande passée à SSH dans le script pour que
gitlabci
fassegit 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.
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.