Backend NodeJS
Compétences visées
- compréhension des protocoles HTTP, Websocket, REST
- déploiement d’une application avec pm2 et Nginx
- usage intermédiaire d’Express
- usage d’un serveur d’applications (FeatherJS)
- échange d’information temps-réel avec les websockets
- compréhension du flot d’authentification
- [usage d’un moteur de template (pug)]
Structure générale des applications web et mobiles
Avec le protocole HTTP
- Le client (navigateur) fait une requête d’accès à une ressource auprès d’un serveur Web selon le protocole HTTP
- Le serveur vérifie la demande, les autorisations et effectue la tache demandée. Il envoie en retour une réponse au client.
- Le client interprète la réponse reçue (ex: modification de l’affichage)
Il faut noter que le serveur ne peut pas envoyer d’information à un client spontanément, c’est à dire sans que celui-ci ne lui ait fait une demande.
Avec le protocole Websocket
Le client fait des demandes au serveur, mais le protocole websocket permet également à un serveur d’envoyer des informations de son propre chef à un ou plusieurs de ses clients connectés.
Les connexions entre client et serveur par un websocket sont persistantes, par opposition aux connexions http ordinaires.
Sites web statiques
Les premiers sites web étaient entièrement statiques. Il en existe encore beaucoup, notamment pour des documentations. Ici les ressources accédées sont toutes statiques : pages html, images, vidéos, feuilles de style (css). Aucun script n’est exécuté, le navigateur affiche les éléments statiques html, css etc. et permet la navigation entre les pages en cliquant sur les liens hypertexte.
L’arborescence des fichiers html définit la structure des urls.
Par exemple, l’accès à http://monsite.org/collections/hiver/manteaux.html
provoquera un accès en lecture à un fichier situé à l’emplacement relatif collections/hiver/manteaux.html
par rapport à la racine de l’emplacement des pages statiques, typiquement /var/www/monsite/html/
Applications web dynamiques
Deux éléments peuvent rendre un site web dynamique :
- du code javascript peut être ajouté aux pages html, permettant l’exécution de fonctions par le moteur javascript du navigateur.
- les requêtes du client vers le serveur peuvent être des commandes vers un serveur d’application, qui peut faire des accès à une base de données et/ou calculer dynamiquement une réponse au navigateur
Les protocole HTTP et Websocket
Protocole HTTP : requêtes et réponses
Une requête HTTP est un flux d’octets (un texte) envoyé par le client vers le serveur au travers du réseau. Elle est structurée en lignes.
Exemple d’une requête GET à l’url https://yesno.wtf/api
:
GET /api HTTP/1.1
Host: yesno.wtf
User-Agent: curl/7.64.1
Accept: */*
En retour, le serveur renvoie une réponse
à cette requête, avec une structure analogue.
Exemple de réponse à la requête précédente :
Status: 200 OK
Transfer-Encoding: chunked
X-Powered-By: Phusion Passenger 5.0.1
access-control-allow-origin: *
access-control-request-method: *
cache-control: max-age=0, private, must-revalidate
etag: "c869ad1693dfe675a79a187fa6c1020e"
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-request-id: a6d669c1-401d-4355-aa9a-9dfcd97859a3
x-runtime: 0.002400
x-xss-protection: 1; mode=block
{
"answer": "no",
"forced": false,
"image": "https://yesno.wtf/assets/no/32-b62f1f8058c1d7f06c528319faccfb38.gif"
}
Les requêtes sont souvent envoyées par les navigateurs web :
- obtention du contenu d’une page
- obtention du contenu d’un fichier de script (js) ou de style (css)
- post des données d’un formulaire
- appel d’un webservice, sur le serveur d’origine ou sur un autre serveur (cross-origin)
Elles peuvent aussi être envoyées par tout type de programme ou d’application.
Principaux headers
Headers de la requête
Content-Type
, type du média du body de la requêteContent-Type: application/json Content-Type: application/json; charset=UTF-8 Content-Type: text/html
Accept
indique quels sont les types de contenu, exprimés sous la forme de types MIME, que le client sera capable d’interpréter
Headers de la réponse
Content-Type
, type du média du body de la réponseContent-Type: application/json Content-Type: text/html
‘ETag’
Etag: "chaine de hashage unique"
Si un ETag est présent dans la réponse, le navigateur ira toujours requêter le serveur pour vérifier qu’il a la dernière version. Si le navigateur possède une copie de la ressource en cache et si l’ETag sur le serveur est le même que celui du cache, le body de la ressource ne sera pas requêté et c’est celui du cache qui sera utilisé. S’il est différent, le navigateur chargera son cache avec le nouveau body et mettra à jour l’ETag. Cette stratégie est nécessaire pour les applications SPA dont le point de départ est le fichier
index.html
, afin que le déploiement d’une nouvelle version soit immédiatement disponible par les utilisateurs sans qu’ils aient besoin de faire un reload ou un hard-reload.
Le header ETag
est prioritaire sur le header Cache-Control
Cache-Control
Une directive Cache-Control
indique au navigateur de ne même pas chercher à charger une version plus récente d’une ressource avant une certaine date.
Cache-Control: "no-cache, no-store, max-age=0, must-revalidate"
Cookies
Un cookie est une paire (clé, valeur) qui est stockée dans un espace dédié du navigateur, associé au nom de domaine appelé.
Un serveur peut mettre dans une response
HTTP un ordre de stockage d’un nouveau cookie dans le navigateur client :
Set-Cookie: token=JjlmIyAZuV6u6HY4R8Vl8byOhvkkLzZDTcgTD7Paehe3xLy4VI; mode=dev;
Lors de chaque requête sur le même nom de domaine, le navigateur inclut systématiquement tous les cookies dans la requête HTTP, dans une ligne (attention à l’espace après ;
) :
Cookie: key1=value1; key2=value2;
Exemple:
GET /spec.html HTTP/1.1
Host: www.example.org
Cookie: token=JjlmIyAZuV6u6HY4R8Vl8byOhvkkLzZDTcgTD7Paehe3xLy4VI; mode=dev;
curl & cookie
--cookie "key=value"
Activité à réaliser
- Utiliser les devtools du navigateur pour examiner le contenu des requêtes et réponses HTTP, les cookies
- Utiliser l’option -v de la commande
curl
pour examiner le contenu des requêtes et réponses HTTP
Le protocole Websocket
C’est un protocole de communication bi-directionnel sur une connection TCP. Dans une communication par websocket il y a toujours un serveur et un client comme dans le protocole HTTP, mais le serveur peut à tout moment envoyer des données vers le client, sans qu’il s’agisse d’une réponse à une demande.
Les connexions entre un serveur et un client sont persistantes, contrairement aux connexions http. Un serveur peut être connecté à un grand nombre de clients
Un client connecté à un serveur peut lui envoyer des messages, et il est prévenu par des événements de l’arrivée de messages en provenance du serveur. Un serveur peut émettre des événements à un client particulier, ou globalement à tous les clients connectés (broadcast).
Activité à réaliser
Utiliser les devtools du navigateur pour examiner le contenu des messages websocket de https://web.whatsapp.com/
Spécification REST
Elle s’appuie sur la notion de ressource, une ressource étant associée à un endpoint.
Exemple : la ressource users
associée au end-point http://json-server.jcbuisson.dev/users
- chaque instance de ressource (par ex. chaque user) possède un identifiant, généralement autogénéré par la base de données
- les verbes HTTP GET, POST, PUT et DELETE correspondent à des opérations de type CRUD (CREATE, READ, UPDATE, and DELETE) sur les ressources
- cette spécification est applicable aussi bien au protocole HTTP qu’au protocole Websocket
GET
La méthode GET est utilisée pour obtenir du serveur la liste des toutes les ressources d’un certain type, ou une ressource particulière précisée par son identifiant
Exemples :
GET http://json-server.jcbuisson.dev/users
GET http://json-server.jcbuisson.dev/users/89
GET http://json-server.jcbuisson.dev/users?gender=male
Les deux requêtes font référence au end-point
http://json-server.jcbuisson.dev/users
; la première demande la liste de tous les utilisateurs, la seconde la liste de tous les utilisateurs hommes, la troisième l’utilisateur d’identifiant 89.
curl http://json-server.jcbuisson.dev/users
POST
La méthode POST est utilisée pour créer une nouvelle ressource sur le serveur
Exemple :
POST http://json-server.jcbuisson.dev/users
Cette requête créée une nouvelle resource pour le endpoint http://json-server.jcbuisson.dev/users
, c’est à dire un nouvel utilisateur. Les données qui concernent l’utilisateur doivent être placées dans la section DATA
de la requête.
curl -X POST -H 'Content-Type: application/json' -d '{"firstname": "John", "lastname": "Doe"}' http://json-server.jcbuisson.dev/users
PUT
La méthode PUT est utilisée pour mettre à jour une ressource spécifique sur le serveur
Exemple :
PUT http://json-server.jcbuisson.dev/users/89
L’ensemble des données de l’utilisateur 89 doit être placé dans la section DATA de la requête.
curl -X POST -H 'Content-Type: application/json' -d '{"firstname": "Jon", "lastname": "Doe"}' http://json-server.jcbuisson.dev/users/1
PATCH
La méthode PATCH est également utilisée pour mettre à jour une ressource spécifique sur le serveur
Exemple :
PATCH http://json-server.jcbuisson.dev/users/89
À la différence de PUT, seules les données à modifier doivent être placées dans la section DATA de la requête
curl -X PATCH -H 'Content-Type: application/json' -d '{"firstname": "Joe"}' http://json-server.jcbuisson.dev/users/1
DELETE
La méthode DELETE permet de supprimer une ressource spécifique Example:
DELETE http://json-server.jcbuisson.dev/users/1
curl -X DELETE http://json-server.jcbuisson.dev/users/1
OPTIONS
OPTIONS est utilisée pour obtenir les options de communication avec la cible : verbes HTTP possibles, méthodes d’authentification demandées, utilisation de la politique CORS (Cross Origin Resource Sharing)
Déploiement d’un site statique sur un serveur Nginx
Fichier de configuration
# /etc/nginx/sites-available/mathieu.dufullstack.fr
server {
listen 80;
server_name mathieu.dufullstack.fr www.mathieu.dufullstack.fr;
location / {
root /home/mathieu;
}
}
Activation
ln -s /etc/nginx/sites-available/mathieu.dufullstack.fr /etc/nginx/sites-enabled
service nginx reload
En cas d’erreur de syntaxe, taper nginx -t
Virtual Host par défaut
L’accès à dufullstack.fr
ou à xxx.dufullstack.fr
pour une valeur de xxx
non définie dans les configurations, conduit à servir un des virtual hosts par défaut.
Le virtual host par dėfaut est celui dont le nom est le premier dans l’ordre lexicographique dans /etc/nginx/sites-enabled
. Souvent, on créera un virtual host par dėfaut de nom 000_default
Activité à réaliser
- chaque étudiant déploie sa page personnelle statique sous forme d’un virtual host sur
dufullstack.fr
- créer un virtual host par défaut
Express
Express est le framework serveur web le plus utilisé pour NodeJS. Il facilite la construction d’applications structurées en microservices.
Installation
$ npm install express
Hello World
const express = require('express')
const app = express()
app.get('/foo', function (request, response) {
response.send('Hello Foo!')
})
app.get('/bar', function (request, response) {
response.send('Hello Bar!')
})
app.listen(3000, function () {
console.log("Server listening on port 3000")
})
Production de la réponse
res.send(val)
L’appel res.send(val)
envoie la réponse val
dans la réponse res
et ferme la connexion :
- si
val
eststring
, leContent-Type
de la réponse est mis àtext/html
- si
val
estobject
ouarray
, leContent-Type
de la réponse est mis àapplication/json
etval
est stringifié - le
Content-Length
de la réponse est affecté - le status de la réponse est mis à 200
response
est automatiquement fermée
res.status(code)
L’appel response.status(code)
affecte le status de response
à code
et renvoie l’objet response
modifié, pour chaînage. Exemple :
response.status(500).send('Erreur inconnue')
res.sendStatus(code)
res.sendStatus(200) // equivalent to res.status(200).send('OK')
res.sendStatus(403) // equivalent to res.status(403).send('Forbidden')
res.sendStatus(404) // equivalent to res.status(404).send('Not Found')
res.sendStatus(500) // equivalent to res.status(500).send('Internal Server Error')
res.sendFile(path)
Transfère le fichier path
en assignant au header Content-Type
de la requête, le mime-type associé à l’extension du fichier transféré
res.redirect(path)
Redirige vers la route path
Headers
Ajouter un header :
res.set('Content-Type', 'application/json')
res.set('Cache-Control', 'max-age=3600, must-revalidate')
Envoyer un cookie au navigateur
res.cookie(<name>, <value>[, <options>])
ex :
res.cookie('prefs', 'video-games,books')
Les cookies sont ensuite ajoutés à chaque requête ultérieure du navigateur.
Extraction des query params
Ils sont directement accessibles sous forme d’un dictionnaire dans request.query
const express = require('express')
const app = express()
app.get('/api/pages', function (request, response) {
let queryParams = request.query
console.log('queryParams', queryParams)
//...
response.send(...)
})
const port = process.env.PORT || 3000
app.listen(port, function () {
console.log(`app listening on port ${port}`)
})
Test :
$ curl http://localhost:3000/api/pages?a=1&b=azer
Déploiement d’une application express sur un serveur Nginx
Démonisation du programme serveur avec PM2
PM2 est un process manager pour applications nodejs
$ sudo npm install -g pm2
Pour lancer le processus serveur et l’ajouter à la liste des processus gérés par PM2 :
$ pm2 start app.js
...
[PM2] Spawning PM2 daemon with pm2_home=/Users/chris/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /Users/chris/workspaces/FULLSTACK/node-server/server.js in fork_mode (1 instance)
[PM2] Done.
┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬──────────┬───────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼──────────┼───────┼──────────┤
│ app │ 0 │ fork │ 21716 │ online │ 0 │ 0s │ 0% │ 9.2 MB │ chris │ disabled │
└──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴──────────┴───────┴──────────┘
Use `pm2 show <id|name>` to get more details about an app
Visionnage en continu des logs :
$ pm2 log <id>
Configuration d’un VirtualHost pour Nginx
# /etc/nginx/sites-available/myapp.dufullstack.fr
server {
listen 80;
server_name myapp.dufullstack.fr www.myapp.dufullstack.fr;
# static assets (images, videos, etc.)
location /static {
add_header Access-Control-Allow-Origin *;
root /var/www/static;
}
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Activité à réaliser
- créer une application qui affiche la charge cpu du serveur. Un bouton ‘update’ permet de rafraichir l’information. La déployer sur
http://cpu.dufullstack.fr
- créer une application qui reproduit le fonctionnement de
yesno.wtf
. La déployer surhttp://yesno.dufullstack.fr
Middlewares
Une application express est une succession ordonnée d’appels de fonctions middleware, qui transforment progressivement response
à partir de request
:
- middlewares d’application
- middlewares de routage
- middleware de traitement d’erreurs
- etc.

app.use(<middleware>)
Exemple :
let logger = (req, res, next) => {
console.log('request', req)
next()
}
app.use(logger)
app.use(<route>, <middleware>)
Exemple : app.use('/api/users', middlewareFunction)
- capture
/api/users
, mais aussi/api/users/<id>
, etc. - capture tous les verbes GET, POST, etc.
Exemple : app.use('/api/users/:id', middlewareFunction)
- l’argument
id
est accessible dansrequest.params.id
Middleware avec paramètres
let middleware = (param1, param2) => (req, res, next) => {
...
}
app.use(middleware(p1, p2))
Middlewares de parsing du body
express.json()
etc. ajoutent un attribut body
à request
contenant le contenu parsé du corps (body) de la requête, en fonction du Content-Type
spécifié dans les headers
// Content-Type: application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }))
// Content-Type: application/json
app.use(express.json())
// Content-Type: html/text
app.use(express.text())
Test :
$ curl -X POST -d "azer" -H "Content-Type: text/plain" http://localhost:3000/api/pages?path=/my/page
Middleware de gestion des fichiers statiques à partir du système de fichiers du serveur
const express = require('express')
const app = express()
app.use(express.static('/var/www/html/myapp'))
app.listen(3000, () => console.log('running'))
Une requête GET /aaa/bbb/ccc.x
conduit à servir le fichier statique /var/www/html/myapp/aaa/bbb/ccc.x
app.use('/public', express.static(path.join(__dirname, 'public')))
Une requête GET /public/index.html
conduit à servir le fichier statique <dirname>/public/index.html
Middleware d’ajout de headers CORS
Par défaut les API XMLHttpRequest et Fetch utilisées par les navigateurs interdisent les requêtes vers une destination différente de l’origine (nom de domaine ET numéro de port). Toutefois, ces requêtes sont possibles avec l’accord du serveur. Pour cela, avant d’interdire une requête non-CORS, le client envoie une requête OPTION vers le serveur afin de demander son accord.
Le middleware cors
permet d’autoriser de telles requêtes en ajoutant des headers spécifiques.
// allows CORS requests for all origins
app.use(cors())
Middleware de gestion des cookies
Le middleware cookie-parser
extrait les cookies présents dans la requête et les met dans l’attribut req.cookies
sous forme d’un dictionnaire (cookie_name, cookie_value)
app.use(cookie-parser())
Middleware de gestion des bearer tokens
const bearerToken = require('express-bearer-token')
...
app.use(bearerToken())
Après que la requête soit passée au travers du middleware bearerToken
, le token est attaché à l’attribut .token
de la requête.
Ce token peut être situé à différents endroits :
- dans le header
Authorization: Bearer <token>
(le plus fréquent) - clé
access_token
dansreq.body
- clé
access_token
dansreq.query
- (Optional) cookie avec la clé
access_token
Helmet
Ajoute des headers qui implémentent de bonnes pratiques de sécurité
app.use(helmet())
Activité à réaliser
- créer une application qui présente un formulaire nom/prénom/date de naissance, et qui affiche le json associé dans la console du backend à chaque validation
- compléter l’application pour que les utilisateurs soient enregistrés dans une base NeDB
Fonctions et librairies de requêtes HTTP
NodeJS (back-end)
Plusieurs librairies existent, basées sur le package http
.
Exemple avec la librairie axios
:
let response = await axios({
url: USERS_ENDPOINT,
method: 'post',
headers: {
"Authorization": `Bearer ${sessionToken}`,
"Content-Type": "application/json",
},
data: userData,
})
let createdUser = response.data
...
Javascript (front-end)
Les requêtes http effectuées depuis un script par le navigateur sont appelées requêtes XHR (XML HTTP Request). On les effectue avec la fonction built-in fetch
.
Exemple :
let response = await fetch(USERS_ENDPOINT, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: userData,
})
let createdUser = await response.json()
Activité à réaliser
- créer et déployer une application qui saisit le nom d’une ville et en affiche la température
Feathers
Feathers est une librairie légère, utilisable principalement côté serveur, mais également côté client.
Feathers opère sur les aspects suivants :
- les opérations back-end, qui sont abstraites sous forme de services
- des connecteurs vers toutes les bases de données, sous forme de services REST prédéfinis
- l’authentification
- le transport entre client et serveur peut être http ou websocket, par simple configuration et sans modification du reste du code
- le temps-réel, grace à un mécanisme de type publish/subscribe (transport par websocket)
- une librairie client simplifie l’accès aux services par le front-end
Exemple d’application temps-réel : liste partagée
Clonez le projet : https://gitlab.com/buisson31/fs-feathers-items, branche dom-api

backend/src/app.js
const feathers = require('@feathersjs/feathers')
const socketio = require('@feathersjs/socketio')
const memory = require('feathers-memory')
// Create a Feathers application
const app = feathers()
// Configure the Socket.io transport
app.configure(socketio())
// Create a channel that will handle the transportation of all realtime events
app.on('connection', connection => app.channel('everybody').join(connection))
// Publish all realtime events to the `everybody` channel
app.publish(() => app.channel('everybody'))
// Create an 'items' service which stores the items in memory (instead of a database)
app.use('items', memory())
// Start the server on port 3030
app.listen(3030)
console.log('listening on port 3030')
frontend/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Liste partagée</title>
</head>
<body>
<h2>Liste de courses partagée</h2>
<label>Item
<input type="text" />
</label>
<button type="submit">Ajouter</button>
<hr/>
</body>
</html>
<script type="module">
import io from 'socket.io-client'
import feathers from '@feathersjs/client'
// Create a websocket connecting to Feathers server
const socket = process.env.NODE_ENV === 'production' ? io() : io('localhost:3030')
const app = feathers()
app.configure(feathers.socketio(socket))
function addItemElement(item) {
const itemElement = document.createElement('li')
itemElement.innerText = item.text
document.body.appendChild(itemElement)
}
// get & display already existing items
app.service('items').find({}).then(itemsList => {
itemsList.forEach(item => addItemElement(item))
})
// Listen to new items being created
app.service('items').on('created', item => {
console.log('item created!', item)
addItemElement(item)
})
const input = document.querySelector('input')
const addButton = document.querySelector('button[type="submit"]')
addButton.addEventListener('click', ev => {
// Create a new item
app.service('items').create({ text: input.value })
input.value = ''
})
</script>
Activité à réaliser
Utiliser les devtools de Firefox pour visualiser le contenu des messages échangés au travers de la connexion websocket persistante
Déploiement d’une application FeathersJS avec transport websocket sur un serveur Nginx
server {
listen 80;
server_name myapp.fr www.myapp.fr;
location = /favicon.ico { access_log off; log_not_found off; }
# default location - for directives 'location /' and 'location /index.html'
root /home/chris/workspaces/myproject/dist;
# static assets (images, videos, etc.)
location /static {
add_header Access-Control-Allow-Origin *;
root /var/www/static;
}
location /index.html {
# because the path to /index.html never changes, ETag is used to leverage browser-side caching
etag on;
}
location / {
# js, css etc. static files other than /index.html contain fingerprints (hashes) in their filenames
# so ETag is not needed. Cache them for 5 years
etag off;
add_header Cache-Control "max-age=315360000, immutable";
# for history mode vue app, all non-static requests should be redirected to index.html
try_files $uri $uri/ /index.html;
}
# feathersjs websocket route
location /socket.io {
proxy_pass http://localhost:3030;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Services
Toutes les taches du back-end sont abstraites sous la forme de services.
Un service est un point d’accès (endpoint) REST, qu’on utilise un transport HTTP ou Websocket.
Implémentation
Un service est une simple classe possédant une ou plusieurs des méthodes suivantes :
class MyService {
async create(data, params)
async get(id, params)
async find(params)
async update(id, data, params)
async patch(id, data, params)
async remove(id, params)
setup(app, path)
}
Dans l’exemple, le service ‘items’ s’appuie sur l’adapter feathers-memory
qui permet un stockage de type REST en mémoire (non persistant).
Activité à réaliser
Ajouter (et gérer) un bouton ‘supprimer’ en face de chaque item dans l’interface web. On utilisera la méthode REST ‘remove’ du service ‘items’.

Services de bases de données
FeathersJS permet en quelques lignes de code de créer un service qui fournit un accès REST sur une table de base de données relationnelle, ou une base NoSQL. FeathersJS dispose d’adapters pour la plupart des SGBD (DBMS) existants.
Dans ce qui suit on va utiliser le query-builder Knex avec l’adapter feathers-knex
On utilisera aussi le système de configuration de feathers, qui prend les constantes dans un répertoire config
Installation de PostgreSQL sur Linux (debian)
apt install postgresql
sudo su postgres
psql # CLI de postgres, cheatsheet: https://gist.github.com/Kartones/dd3ff5ec5ea238d4c546
# \password postgres
-> "motdepasse" x 2
# createdb fs-feathers-items
<Ctrl-d>
backend/config/default.json
{
"dbConfig": {
"client": "pg",
"connection": "postgres://postgres:motdepasse@localhost:5432/fs-feathers-items"
}
}
backend/src/knex.js
const knex = require('knex')
module.exports = function (app) {
const { client, connection } = app.get('dbConfig')
const db = knex({ client, connection })
app.set('knexClient', db)
}
backend/src/services/database/items/items.model.js
module.exports = async function(app, tableName) {
try {
const db = app.get('knexClient')
await db.schema.createTable(tableName, table => {
table.increments('id')
table.timestamp('created_at').defaultTo(db.fn.now()) // ex: 2020-11-23 06:32:48.524937+01
table.text('text').notNull()
})
console.log(`Created ${tableName} table`)
} catch(err) {
console.log(`Error creating ${tableName} table: ${err.toString()}`)
}
}
backend/src/services/database/items/items.service.js
const service = require('feathers-knex');
module.exports = function(app) {
const db = app.get('knexClient')
let name = 'items'
app.use(name, service({
Model: db,
name,
paginate: false,
}))
}
backend/src/scripts/create-tables.js
const feathers = require('@feathersjs/feathers')
const configuration = require('@feathersjs/configuration')
const knex = require('../knex')
const createItemsTable = require('../services/database/items/items.model')
async function main() {
const app = feathers()
app.configure(configuration())
app.configure(knex)
// database
await createItemsTable(app, "items")
process.exit(0)
}
main()
backend/src/app.js
...
const configuration = require('@feathersjs/configuration')
const knex = require('./knex')
const itemsService = require('./database/items/items.service.js')
...
app.configure(configuration())
app.configure(knex)
app.configure(itemsService)
...
Services ‘custom’
On peut aussi créer des services totalement personalisés, mais leur utilisation doit nécessairement passer par l’utilisation d’une ou plusieurs des 7 méthodes ‘create’, ‘get’, ‘find’, etc.
Exemple : ajout d’un service ‘custom’ d’envoi de mail
app.js
...
const emailService = require("./services/custom/email/emailService")
app.configure(emailService)
...
src/services/email/emailService.js
const nodemailer = require('nodemailer')
module.exports = function(app) {
class MailService {
create(data) {
const mailConfig = {
"host": "smtp.online.net",
"port": "587",
"secure": false,
"auth": {
"user": "fullstack@shdl.fr",
"pass": process.env.SMTP_PASSWORD,
},
"name": "shdl.fr"
}
const transporter = nodemailer.createTransport(mailConfig)
try {
return transporter.sendMail({
from: data.from,
to: data.to,
subject: data.subject,
text: data.text,
html: data.html,
})
} catch(err) {
console.log(err)
}
}
}
app.use("email", new MailService())
}
Client node : src/scripts/send-mail.js
// Usage:
// cd backend
// node src/scripts/send-email.js buisson@n7.fr Hello "Hello JC how are you doing?"
const io = require('socket.io-client')
const feathers = require('@feathersjs/feathers')
const socketio = require('@feathersjs/socketio-client')
const socket = io('http://localhost:3030')
const app = feathers()
app.configure(socketio(socket))
async function main() {
await app.service('email').create({
from: 'buisson@enseeiht.fr',
to: process.argv[2],
subject: process.argv[3],
text: process.argv[4],
})
process.exit(0)
}
main()
Activité à réaliser
Créer un client web d’envoi de mail, avec l’interface suivante :
Hooks
Ce sont des méthodes qui peuvent être appelées avant ou après l’exécution d’une des méthodes d’un service :
- vérifier que l’utilisateur est authentifié
- vérifier que l’utilisateur a des privilèges suffisants
- limiter les requêtes d’un utilisateur à ce qu’il est autorisé à accéder
- valider le ‘payload’ d’une requête par rapport à un schema
- adapter certains attributs du payload des requêtes (ajouter une date de modification, normaliser un numéro de téléphone)
- retirer certains champs d’une réponse (ex: mot de passe)
- déclencher des actions (ex: envoyer un email, créer une entrée d’historisation)
Exemple :
app.service('users').hooks({
before: {
patch: [async context => {
context.data.updatedAt = new Date();
return context;
}],
...
Lorsqu’un hook produit une erreur, seuls les hooks d’erreur sont appelés. Les autres hooks et le service lui même (s’il n’a pas déjà été exécuté) ne sont pas appelés.
FeathersJS fournit de nombreux hooks prédéfinis qu’il suffit d’ajouter dans la bonne rubrique. Par exemple, des hooks d’authentification prédéfinis permettent de vérifier que l’utilisateur est authentifié (hook authenticate
), de hasher un mot de pass (hook hashPassword
), de supprimer un attribut/valeur d’un object résultat (hook protect
) :
app.js
...
app.configure('./services/users/users.hooks')
...
users.hooks.js
const { authenticate } = require('@feathersjs/authentication').hooks
const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks
module.exports = {
before: {
all: [],
find: [authenticate('jwt')],
get: [authenticate('jwt')],
create: [hashPassword('password')],
update: [hashPassword('password'), authenticate('jwt')],
patch: [hashPassword('password'), authenticate('jwt')],
remove: [authenticate('jwt')],
},
after: {
all: [
protect('password'),
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
}
Problématique de l’authentification
Gestion des mots de passe
- ils doivent être stockés de façon hashée dans la BD, jamais en clair
- la production du hash est non-inversible, mais permet de vérifier la concordance
- le hash doit être salé, sinon il peut être attaqué par des dictionnaires de valeurs hashées de mots de passe
Librairie bcrypt
$ npm install bcrypt
Production du hash
const bcrypt = require('bcrypt')
let saltRounds = 10
let hash = bcrypt.hashSync("eureka", saltRounds)
hash –> ‘$2bMc07H0wBqBL9a6g32ecig.LbUbk2uBeCYIlDrcZ0Goe9lSLXVeTj2’
Le hash d’un mot de passe est modifié à chaque appel (salage), mais cela n’affecte pas la vérification de la compatibilité d’un mot de passe candidat avec l’une quelconque de ses valeurs de hashage
Vérification d’un mot de passe candidat
bcrypt.compareSync("eureka", hash) // true
bcrypt.compareSync("eurehack", hash) // false
Les tokens JWT
Voir https://jwt.io
Un token JWT est composé de trois parties, séparées par un point :
- une en-tête qui indique notamment le type de signature
- un payload json, non crypté. Sa validité est garantie par la signature
- une signature
Après authentification d’un utilisateur, le serveur lui transmet un token (de session) que le client ajoutera à toutes ses requêtes. Typiquement, un client stocke ce token dans un cookie.
Le serveur n’accepte de traiter une requête que si elle contient un token dont la signature est valide.
Le payload du token contient typiquement l’id de l’utilisateur et une date d’expiration. Conrairement à d’autres types de tokens, le serveur n’a pas besoin de stocker les tokens JWT car ils contiennent dans leur payload les informations dont il a besoin.
Créer un token
let jwt = require('jsonwebtoken')
let token = jwt.sign({ foo: 'bar' }, "MYSECRET")
Lire le payload d’un token
let payload = jwt.decode(token)
Vérifier la signature d’un token
try {
let payload = jwt.verify(token, "MYSECRET")
} catch(err) {
console.log("signature invalide")
}
Authentification par cookies vs authentification par tokens
lors d’une authentification par cookies, un cookie d’authentification est envoyé par le backend vers le navigateur lors du login. Toute requête du frontend vers le backend est alors sécurisée, car elle contient automatiquement le cookie d’authentification.
si l’authentification est basée sur des tokens (ex: OAuth), un token d’accès est envoyé par le backend vers le frontend lors du login. Le frontend stocke ce token d’accès dans LocalStorage ou SessionStorage ou plus rarement dans un cookie, et toutes les requêtes vers le backend doivent comporter un header ‘Authorization: Bearer ’.
Authentification avec FeathersJS
FeathersJS contient des fonctions back et front d’authentification par token. Elles sont de deux sortes :
- l’authentification dite ‘locale’, avec identifiant/mot de passe
- l’authentification OAuth 2 avec tout type de serveur d’authentification (Google, Facebook, LinkedIn, etc.)
Authentification ‘locale’ avec FeathersJS
TODO
Authentification OAuth 2 avec FeathersJS
TODO
Activité à réaliser
- Créer une application web avec un écran de login via Google
Projet à rendre
Spécification
Réaliser une application de saisie de notes :
- une liste des notes existantes est toujours visible dans une colonne à gauche ; la sélection d’une note provoque son affichage dans la partie droite
- un bouton ‘+’ en haut de la colonne permet de créer une nouvelle note et de passer en mode édition ; un bouton ‘-’ en face de chaque nom de note permet de l’effacer
- les notes peuvent être éditées ; en mode édition une note est rédigée en markdown ; en mode lecture elle est affichée “décorée”
- le nom d’une note tel qu’il apparait dans la colonne de gauche correspond à la première ligne du texte de la note
- l’application sera déployée à l’url :
http://notes-myname.dufullstack.fr
Modalités
Le travail sera rendu sous forme d’un projet Gitlab nommé fs-notes
et par la résolution successive des issues suivantes :
- “[back-end] Mise en place” : créer un répertoire
backend/
qui contient une application Feathers minimale, et un répertoirefrontend/
qui contient un client web minimal - “[back-end] Modèle de données” : créer dans
backend/
les services de données nécessaires à l’application, et des tests Mocha de création / suppression / modification de notes - “[front-end] Editeur” : créer dans
frontend/
un éditeur markdown par utilisation de la librairiemarked
- “[front-end] Application” : finaliser la partie front-end de l’application
[OPTIONNEL] Production de pages HTML côté serveur
Express permet de plugger des moteurs de template de façon unifiée et de produire des pages avec la fonction render
.
app.set("views", path.join(__dirname, "views"))
app.set("view engine", "xxx")
...
app.get("/", (req, res) => {
res.render("index", { title: "Home" })
})
pug
est un des moteurs de template les plus populaire.
Exemple
server.js
const express = require('express')
const path = require('path')
const app = express()
app.set("views", path.join(__dirname, "views"))
app.set("view engine", "pug")
app.use(express.static(path.join(__dirname, "public")))
app.get("/", (req, res) => {
res.render("index", {
name: "Chris",
url: "https://wiki.formation-fullstack.fr",
})
})
app.listen(3000, function () {
console.log("Server listening on port 3000")
})
views/index.pug
doctype html
html
head
title Hello #{name}
link(rel="stylesheet" href="/style.css")
body
p.greetings#guy Hello #{name}!
p.plain
| Pour un text multi-ligne,
| il est préférable d'utiliser cette écriture à base de 'pipe'
a(href = url) Lien vers la formation Fullstack
public/style.css
.greetings {
color: green;
text-transform: uppercase;
font-size: 2em;
}
.plain {
color: blue;
}
Le html calculé est :
<!DOCTYPE html>
<html>
<head>
<title>Hello Chris</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<p class="greetings" id="guy">Hello Chris!</p>
<a href="https://wiki.formation-fullstack.fr">Lien vers la formation Fullstack</a>
</body>
</html>