Node.js
Node.js est un environnement JavaScript côté serveur, Express est un framework pour créer des applications web, et Sequelize est un ORM pour SQL.
Informations
Date de publication :
Date de modification :
Catégories : javascript, nodejs, sequelize, api, express
Auteur :
meezyr

Node.js est un environnement qui exécute JavaScript hors navigateur, principalement sur des serveurs. Alors que dans un navigateur, JavaScript est interprété par son propre moteur JavaScript spécifique, sous Node.js, c’est le moteur V8 de Chrome qui est utilisé. Node.js intègre ce moteur V8 ainsi que plusieurs modules internes. Ses principaux atouts incluent l'usage de JavaScript, éliminant ainsi le besoin d'apprendre un nouveau langage, en plus de sa flexibilité, sa popularité et sa rapidité d’exécution. Cette dernière bénéficie de la performance du moteur V8 et d'une architecture non-bloquante. Node.js est versatile, supportant le développement de diverses applications regroupées en quatre grandes catégories : sites web, API REST, applications temps réel et scripts. À partir de sa version 10, Node.js supporte également l'utilisation directe d'ECMAScript 6.
1. Initialiser de NodeJs
1.1 Initialisation d'Express
Installation de Express :
npm init
npm install express --save
Pour en savoir plus sur l'installation d'Express, consultez la documentation officielle.
Configuration du app.js :
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req,res) => res.send("Hello, Express !"))
app.listen(port, () => console.log(`Application Node démarrée sur : http://localhost:${port}`))
1.2 Nodemon
Installation de Nodemon :
npm install --save-dev nodemon
Puis modifier le fichier package.json :
"scripts": {
"start": "nodemon app.js"
}
2. Express
2.1 Routes
Pour en savoir plus sur les routes Express, consultez la documentation officielle.
2.2 Réponses JSON
Une réponse HTTP est constituée de 3 éléments distincts : les données au format JSON, le type MIME application/json, et enfin le code de statut HTTP.
Pour retourner une réponse HTTP au format JSON, on utilise la méthode json() fournie par Express. Cette méthode s’occupe pour nous de structurer nos données au format JSON, et d’attribuer le type MIME application/json dans la requête de retour. Pour en savoir plus sur les réponses JSON, consultez la documentation officielle.
Pour envoyer un message de succès ou d'échec lors de l'envoi de la requête, on utilise le code suivant dans le fichier helper.js :
exports.success = (message, data) => {
return { message, data }
}
On utilise le code suivant dans le fichier app.js :
const express = require('express')
const { success } = require('./helper.js')
let pokemons = require('./mock-pokemon')
const app = express()
const port = 3000
app.get('/', (req,res) => res.send('Hello again, Express !'))
app.get('/api/pokemons', (req, res) => {
const message = 'La liste des pokémons a bien été récupérée.'
res.json(success(message, pokemons))
})
app.get('/api/pokemons/:id', (req, res) => {
const id = parseInt(req.params.id)
const pokemon = pokemons.find(pokemon => pokemon.id === id)
const message = 'Un pokémon a bien été trouvé.'
res.json(success(message, pokemon))
})
app.listen(port, () => console.log(`Notre application Node est démarrée sur : http://localhost:${port}`))
2.3 Middlewares
Les middlewares sont des fonctions JavaScript qui interviennent dans le traitement des requêtes entrantes et sortantes d'une API Rest. Ils se divisent en cinq catégories : d'application, de router, de gestion d'erreurs, intégrés, et tiers. Pour intégrer un middleware à une API Rest, on utilise la méthode use() d'Express. La fonction next() est essentielle pour signaler à Express que le middleware actuel a terminé et passer au suivant. L'utilisation d'un middleware tiers nécessite son installation et importation avant utilisation. Les middlewares peuvent être chaînés et communiquer via des paramètres partagés. L'ordre dans lequel les middlewares sont déclarés est crucial et doit être ajusté selon les besoins de traitement préalable.
Pour en savoir plus sur la création de middlewares, consultez la documentation officielle.
Pour en savoir plus sur l'utilisation de middlewares, consultez la documentation officielle.
Voici le code pour créer manuellement un middlewares dans le fichier app.js :
const logger = (req, res, next) => {
console.log('LOGGED')
next()
}
app.use(logger)
Installation d'un Middlewares déjà existant grâce à Morgan, utilisez la commande suivante pour l'installer :
npm install morgan --save-dev
Pour utiliser Morgan, il faut utiliser le code suivant dans le fichier app.js :
const morgan = require('morgan')
app.use(morgan('dev')) // Affiche par exemple "GET /api/pokemons 200 2.654 ms - 2205"
Installation d'un favicon via Middlewares, utilisez la commande suivante pour l'installer :
npm install serve-favicon --save
Pour utiliser Morgan, il faut utiliser le code suivant dans le fichier app.js :
const favicon = require('serve-favicon')
app.use(favicon(__dirname + '/favicon.ico')).use(morgan('dev'))
2.4 Opération CRUD
Les opérations dites "CRUD" correspondent à la création d’une ressource, sa récupération, sa modification et enfin sa suppression.
Pour ajouter un nouvel élément, on utilise le code suivant dans le fichier app.js :
app.post('/api/pokemons', (req, res) => {
const id = 123
const pokemonCreated = { ...req.body, ...{id: id, created: new Date()}}
pokemons.push(pokemonCreated)
const message = `Le pokémon ${pokemonCreated.name} a bien été crée.`
res.json(success(message, pokemonCreated))
})
Pour générer un identifant unique, on utilise la méthode suivante dans le fichier helper.js :
exports.getUniqueId = (pokemons) => {
const pokemonsIds = pokemons.map(pokemon => pokemon.id)
const maxId = pokemonsIds.reduce((a,b) => Math.max(a, b))
const uniqueId = maxId + 1
return uniqueId
}
Puis on utilise const id = getUniqueId(pokemons) pour implémenter la génération d'identifiants.
Pour parser les données grâce à un Middleware on utilise la commande suivante :
npm install body-parser --save
Pour parser les éléments, on utilise le code suivant dans le fichier app.js :
const bodyParser = require('body-parser')
app.use(favicon(__dirname + '/favicon.ico')).use(morgan('dev')).use(bodyParser.json())
Pour modifier un élément, on utilise le code suivant dans le fichier app.js :
app.put('/api/pokemons/:id', (req, res) => {
const id = parseInt(req.params.id);
const pokemonUpdated = { ...req.body, id: id }
pokemons = pokemons.map(pokemon => {
return pokemon.id === id ? pokemonUpdated : pokemon
})
const message = `Le pokémon ${pokemonUpdated.name} a bien été modifié.`
res.json(success(message, pokemonUpdated))
});
Pour supprimer un élément, on utilise le code suivant dans le fichier app.js :
app.delete('/api/pokemons/:id', (req, res) => {
const id = parseInt(req.params.id)
const pokemonDeleted = pokemons.find(pokemon => pokemon.id === id)
pokemons = pokemons.filter(pokemon => pokemon.id !== id)
const message = `Le pokémon ${pokemonDeleted.name} a bien été supprimé.`
res.json(success(message, pokemonDeleted))
});
3. Base de donnée
3.1 ORM
Un ORM (Object-Relational Mapping) est une méthode de programmation efficace qui facilite l'interaction avec une base de données en utilisant des langages simples tels que JavaScript. Les avantages de l'utilisation d'un ORM incluent l'inutile maîtrise du SQL, une couche d'abstraction qui masque les détails de la base de données en arrière-plan, et la disponibilité de requêtes, tant basiques qu'avancées, prêtes à l'emploi pour accéder aux données.
3.2 Installation de Sequelize
Sequelize est un ORM conçu pour les développeurs Node.js utilisant des bases de données SQL. Il repose entièrement sur les Promises pour gérer les opérations asynchrones, évitant ainsi le recours à des callbacks verbeux. Sequelize nécessite l'utilisation de drivers spécifiques pour se connecter à chaque type de base de données, rendant essentielle l'installation du driver approprié en plus de l'ORM. Pour en savoir plus sur Sequelize, consultez la documentation officielle.
Pour utiliser l'ORM Sequelize, on utilise les commandes suivantes :
npm install sequelize --save
npm install mariadb --save
Pour configurer l'ORM Sequelize, on utilise le code suivant dans le fichier app.js :
const { Sequelize } = require('sequelize')
const sequelize = new Sequelize(
'pokedex',
'root',
'',
{
host: 'localhost',
dialect: 'mariadb',
dialectOptions: {
timezone: 'Etc/GMT-2'
},
logging: false
}
)
sequelize.authenticate()
.then(_ => console.log('La connexion à la base de données a bien été établie.'))
.catch(error => console.error(`Impossible de se connecter à la base de données ${error}`))
3.3 Modèle Sequelize
Les modèles sont présents dans le dossier src/models de l'API. Voici un exemple de fichier modèle via le chemin /src/models/pokemon.js :
module.exports = (sequelize, DataTypes) => {
return sequelize.define('Pokemon', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
hp: {
type: DataTypes.INTEGER,
allowNull: false
},
cp: {
type: DataTypes.INTEGER,
allowNull: false
},
picture: {
type: DataTypes.STRING,
allowNull: false
},
types: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true,
createdAt: 'created',
updatedAt: false
})
}
Pour en savoir plus sur les datatypes, consultez la documentation officielle.
Pour utiliser le modèle dans Express, il faut utiliser le code suivant dans le fichier app.js :
const PokemonModel = require('./src/models/pokemon')
const Pokemon = PokemonModel(sequelize, DataTypes)
sequelize.sync({force: true})
.then(_ => console.log('La base de donnée a bien été initialisée !'))
3.4 Queries Sequelize
Pour instancier un modèle, on utilise le code suivant dans app.js :
sequelize.sync({force: true}).then(_ => {
pokemons.map(pokemon => {
Pokemon.create({
name: pokemon.name,
hp: pokemon.hp,
cp: pokemon.cp,
picture: pokemon.picture,
types: pokemon.types.join()
}).then(pokemon => console.log(pokemon.toJSON()))
})
console.log('La base de donnée a bien été initialisée !')
})
Pour en savoir plus sur les queries basics, consultez la documentation officielle.
Pour en savoir plus sur les queries finds, consultez la documentation officielle.
La méthode findAllAndCount de Sequelize pemet de connaître le nombre total de résultats, même si on retourne une nombre de résultats plus limité.
Nous pouvons mettre en place des getters et setters dans le fichier pokemon.js :
//...
types: {
type: DataTypes.STRING,
allowNull: false
get() {
return this.getDataValue('types').split(',')
},
set(types) {
this.setDataValue('types', types.join())
}
}
Pour en savoir plus sur le getter et setter, consultez la documentation officielle.
3.5 Architecture Express et Sequelize
Pour améliorer l'architecture de NodeJS, on peut créer un dossier /src/db, où nous allons mettre les fichiers mock-pokemon.js et sequelize.js. Le fichier sequelize.js contient le code suivant :
const { Sequelize, DataTypes } = require('sequelize')
const PokemonModel = require('../models/pokemon')
const pokemons = require('./mock-pokemon')
const sequelize = new Sequelize('pokedex', 'root', '', {
host: 'localhost',
dialect: 'mariadb',
dialectOptions: {
timezone: 'Etc/GMT-2',
},
logging: false
})
const Pokemon = PokemonModel(sequelize, DataTypes)
const initDb = () => {
return sequelize.sync({force: true}).then(_ => {
pokemons.map(pokemon => {
Pokemon.create({
name: pokemon.name,
hp: pokemon.hp,
cp: pokemon.cp,
picture: pokemon.picture,
types: pokemon.types.join() // Ou types: pokemon.types
}).then(pokemon => console.log(pokemon.toJSON()))
})
console.log('La base de donnée a bien été initialisée !')
})
}
module.exports = {
initDb, Pokemon
}
Le fichier app.js ne doit contenir que le code suivant :
const express = require('express')
const morgan = require ('morgan')
const favicon = require('serve-favicon')
const bodyParser = require('body-parser')
const sequelize = require('./src/db/sequelize')
const app = express()
const port = 3000
app.use(favicon(__dirname + '/favicon.ico')).use(morgan('dev')).use(bodyParser.json())
sequelize.initDb()
// Placer ici les points de terminaisons
require('./src/routes/findAllPokemons')(app)
require('./src/routes/findPokemonByPk')(app)
require('./src/routes/createPokemon')(app)
require('./src/routes/updatePokemon')(app)
require('./src/routes/deletePokemon')(app)
app.listen(port, () => console.log(`Notre application Node est démarrée sur : http://localhost:${port}`))
Pour améliorer l'architecture de NodeJS, on peut créer un dossier /src/routes, où nous allons mettre les fichiers permettant de récupérer les informations des pokemons. Le fichier findAllPokemons.js contient le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.get('/api/pokemons', (req, res) => {
Pokemon.findAll().then(pokemons => {
const message = 'La liste des pokémons a bien été récupérée.'
res.json({ message, data: pokemons })
})
})
}
Le fichier findPokemonByPk.js contient le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.get('/api/pokemons/:id', (req, res) => {
Pokemon.findByPk(req.params.id).then(pokemon => {
const message = 'Un pokémon a bien été trouvé.'
res.json({ message, data: pokemon })
})
})
}
Le fichier createPokemon.js contient le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.post('/api/pokemons', (req, res) => {
Pokemon.create(req.body).then(pokemon => {
const message = `Le pokémon ${req.body.name} a bien été crée.`
res.json({ message, data: pokemon })
})
})
}
Le fichier updatePokemon.js contient le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.put('/api/pokemons/:id', (req, res) => {
const id = req.params.id
Pokemon.update(req.body, {
where: { id: id }
})
.then(_ => {
Pokemon.findByPk(id).then(pokemon => {
const message = `Le pokémon ${pokemon.name} a bien été modifié.`
res.json({message, data: pokemon })
})
})
})
}
Le fichier deletePokemon.js contient le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.delete('/api/pokemons/:id', (req, res) => {
Pokemon.findByPk(req.params.id).then(pokemon => {
const pokemonDeleted = pokemon;
Pokemon.destroy({
where: { id: pokemon.id }
})
.then(_ => {
const message = `Le pokémon avec l'identifiant n°${pokemonDeleted.id} a bien été supprimé.`
res.json({message, data: pokemonDeleted })
})
})
})
}
4. Erreurs Express
Pour créer une API Rest solide, il est crucial de prévoir et gérer toutes les erreurs potentielles. On distingue deux catégories d'erreurs : les erreurs de programmation causées par le développeur et les erreurs opérationnelles, qui échappent à notre contrôle. Les codes de statut HTTP sont utiles pour communiquer rapidement et avec précision avec les utilisateurs de votre API Rest. En général, il y a trois issues possibles lors des interactions avec votre API Rest : un succès (200), une erreur client (400) et une erreur serveur (500). Pour les erreurs de type 400, il est particulièrement important de fournir des messages détaillés, car elles concernent des erreurs du côté client.
On peut gérer les erreurs avec Express, voici un exemple de code à mettre dans le ficher app.js :
app.use({res}) => {
const message = 'Erreur, ressouce introuvable !'
res.status(404).jso({message})
}
Pour en savoir plus sur la gestion des erreurs avec Express, consultez la documentation officielle.
Pour gérer les erreurs sur une route, on utilise le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.get('/api/pokemons', (req, res) => {
Pokemon.findAll()
.then(pokemons => {
const message = 'La liste des pokémons a bien été récupérée.'
res.json({ message, data: pokemons })
})
.catch(error => {
const message = 'Erreur, la liste n\'a pas été récupéré.'
res.status(500).json({ message, data: error })
})
})
}
Pour gérer les erreur sur une route delete, on peut utiliser le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.delete('/api/pokemons/:id', (req, res) => {
Pokemon.findByPk(req.params.id).then(pokemon => {
if (pokemon === null) {
const message = 'Erreur, le pokemon n\'existe pas !';
return res.status(404).json({message})
}
const pokemonDeleted = pokemon;
return Pokemon.destroy({
where: { id: pokemon.id }
})
.then(_ => {
const message = `Le pokemon avec l'identifiant n°${pokemonDeleted.id} a bien été supprimé.`
res.json({message, data: pokemonDeleted })
})
})
.catch(error => {
const message = 'Erreur, le pokemon n\'a pas été récupéré.'
res.status(500).json({ message, data: error })
})
})
}
5. Validations
Sequelize offre deux approches pour intégrer des règles de validation dans vos modèles : à travers des validateurs et des contraintes. En utilisant une combinaison de divers niveaux de gestion des erreurs, tels que les blocs catch, les codes HTTP, les validateurs et les contraintes, vous pouvez prévenir la plupart des erreurs susceptibles de survenir dans une API Rest. Une gestion efficace des erreurs est essentielle pour développer une API Rest solide et conviviale pour vos utilisateurs futurs.
5.1 Validation Sequelize
Les validateurs sont responsables de la validation des modèles directement dans le code JavaScript. En cas d'échec de la validation, Sequelize n'effectuera pas de requête SQL vers la base de données. Sequelize offre une large gamme de validateurs intégrés, adaptés à la plupart des scénarios de validation courants.
Pour implémenter des validations dans un modèle, par exemple utiliser le code suivant dans le fichier du modèle :
hp: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
isInt: {msg: 'Nombres entiers requis !'},
notNull: {msg: 'Cette proprété est requise !'}
}
},
Pour en savoir plus sur les validateurs, consultez la documentation officielle.
Pour gérer la validation des modèles dans les routes, on utilise le code suivant :
const { Pokemon } = require('../db/sequelize')
const { ValidationError } = require('sequelize')
module.exports = (app) => {
app.post('/api/pokemons', (req, res) => {
Pokemon.create(req.body)
.then(pokemon => {
const message = `Le pokémon ${req.body.name} a bien été crée.`
res.json({ message, data: pokemon })
})
.catch(error => {
if (error instanceof ValidationError) {
return res.status(400).json({ message: error.message, data: error })
}
const message = 'Erreur, le pokemon n\'a pas été créé.'
res.status(500).json({ message, data: error })
})
})
}
5.2 Validation personnalisé
Il est possible de créer ses propres validateurs personnalisés, qui permettent de mettre en place des scénarios de validation plus complexes, non couvert par les validateurs intégrés.
Pour implémenter des validations personnalisé dans un modèle, par exemple utiliser le code suivant dans le fichier du modèle :
const validTypes = ['type1', 'type2', 'type3']
types: {
type: DataTypes.STRING,
allowNull: false,
get() {
return this.getDataValue('types').split(',')
},
set(types) {
this.setDataValue('types', types.join())
},
validate: {
isTypesValid(value) {
if (!value) {
throw new Error('Erreur, 1 type minimum requis')
}
if (value.split(',').length > 3) {
throw new Error('Erreur, moins de 3 types requis')
}
value.split(',').forEach(type => {
if(!validTypes.includes(type)) {
throw new Error(`Erreur type n'est pas valide, voici la liste des types valide : ${validTypes}`)
}
});
}
}
},
Pour en savoir plus sur les validations personnalisé, consultez la documentation officielle.
5.3 Contraintes
Les contraintes sont des règles définies directement au niveau de la base de données par Sequelize. Par exemple pour vérifier l’unicité d’une valeur en base, une contrainte sera capable d’effectuer ce contrôle, contrairement à un validateur classique.
Pour mettre en place une contrainte, voici le code à implenter dans le modèle :
name: {
type: DataTypes.STRING,
allowNull: false,
unique: {
msg: 'Le nom est déjà pris'
},
//...
}
Pour en savoir plus sur les contraintes, utilisez la documentation officielle.
Pour gérer le message d'erreur des contrainte, il faut utiliser le code suivant dans le fichier createPokemon.js :
const { Pokemon } = require('../db/sequelize')
const { ValidationError, UniqueConstraintError } = require('sequelize')
module.exports = (app) => {
app.post('/api/pokemons', (req, res) => {
Pokemon.create(req.body)
.then(pokemon => {
const message = `Le pokémon ${req.body.name} a bien été crée.`
res.json({ message, data: pokemon })
})
.catch(error => {
if (error instanceof ValidationError) {
return res.status(400).json({ message: error.message, data: error })
}
if (error instanceof UniqueConstraintError) {
return res.status(400).json({ message: error.message, data: error })
}
const message = 'Erreur, le pokemon n\'a pas été créé.'
res.status(500).json({ message, data: error })
})
})
}
6. Requêtes avancées
Les paramètres de requête sont utilisés pour fournir des informations supplémentaires à un endpoint, permettant ainsi de personnaliser les réponses obtenues. Ils servent principalement à trier ou filtrer des ressources, alors que les paramètres d'URL sont destinés à identifier une ressource spécifique.
6.1 Recherche
Pour mettre en place et récupérer les paramètres passé dans l'URL, il nous faut utiliser le code suivant :
const { Pokemon } = require('../db/sequelize')
module.exports = (app) => {
app.get('/api/pokemons', (req, res) => {
if (req.query.name) {
const name = req.query.name
return Pokemon.findAll({ where: { name: name } })
.then(pokemons => {
const message = `Il y a ${pokemons.length} pokémons qui correspondent à votre recherche ${name}.`
res.json({ message, data: pokemons })
})
} else {
Pokemon.findAll()
.then(pokemons => {
const message = 'La liste des pokémons a bien été récupérée.'
res.json({ message, data: pokemons })
})
.catch(error => {
const message = 'Erreur, la lise n\'a pas été récupéré !'
res.status(500).json({message, data: error})
})
}
})
}
Pour en savoir plus sur les paramètres de recherche d'Express, consultez la documentation officielle.
Pour en savoir plus sur l'utilisation de Where dans Sequelize, consultez la documentation officielle.
6.2 Opérateurs
Les opérateurs de Sequelize permettent de créer des requêtes plus complètes. À la place de l'utilisation du Where comme ci-dessus, nous pouvons utiliser les opérateurs Sequelize, comme dans l'exemple suivant :
const { Op } = require("sequelize");
Foo.findAll({
where: {
rank: { // rank < 1000 OR rank IS NULL
[Op.or]: {
[Op.lt]: 1000,
[Op.eq]: null
}
}
}
})
Pour en savoir plus sur les opérateurs Sequelize, consultez la documentation officielle. Pour obtenir l'ensemble des opérateurs Op possible, consultez la documentation officielle.
6.3 Limite et Pagination
Pour mettre en place une limite et une pagination avec Sequelize, consultez la documentation officielle.
6.4 Ordonner et grouper
Pour mettre en place un ordre et des groupes avec Sequelize, consultez la documentation officielle.
6.5 Economie de ressouces
Afin d’économiser les ressources, il est tout à fait possible, et même encouragé, de limiter les appels inutiles à la base de données. On peut court-circuiter ces requêtes superflues au niveau de l’API Rest, en retournant directement un message d’erreur explicatif.
7. Authentification
L’authentification est un processus qui permet de restreindre l’accès aux points de terminaisons de notre API Rest. La mise en place d’une authentification côté API Rest nécessite de respecter deux exigences principales : encrypter le mot de passe et sécuriser l’échange des données.
7.1 Modèle Utilisateur
La création d'un modèle Sequelize pour les utilisateurs est posssible grâce au code suivant dans le fichier /src/models/user.js :
module.exports = (sequelize, DataTypes) => {
return sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: DataTypes.STRING,
unique: {
msp: 'Erreur, le nom est déjà pris !'
}
},
password: {
type: DataTypes.STRING
}
})
}
L’identifiant que les clients utilisent lors de l’authentification doit être unique.
7.2 Encrypter
Pour encrypter les mot de passe, nous utilisons Bcrypt, pour l'installer il faut utiliser la commande suivante :
npm install bcrypt --save
Pour mettre en place le cryptage du mot de passe, il faut suivre le code suivant :
const UserModel = require('../models/user')
const bcrypt = require('bcrypt')
const User = UserModel(sequelize, DataTypes)
//...
bcrypt.hash('pikachu', 10)
.then(hash => User.create({ username: 'pikachu', password: hash }))
.then(user => console.log(user.toJSON()))
Pour en savoir plus sur Bcrypt, consultez la documentation officielle.
7.3 Login
Pour créer une route de connexion, utilisez le code suivant dans le fichier /src/routes/login.js :
const { User } = require('../db/sequelize')
const bcrypt = require('bcrypt')
module.exports = (app) => {
app.post('/api/login', (req, res) => {
User.findOne({ where: { username: req.body.username } }).then(user => {
if (!user) {
const message = 'Erreur, l'utilisateur n'existe pas !'
return res.status(404).json({message})
}
bcrypt.compare(req.body.password, user.password).then(isPasswordValid => {
if(!isPasswordValid) {
const message = `Erreur, le mot de passe est incorrect !`;
return res.status(401).json({ message })
}
const message = `L'utilisateur a été connecté avec succès`;
return res.json({ message, data: user })
})
})
.catch(error => {
const message = `Erreur, la connexion a échoué. Réessayez dans quelques instants.`;
return res.json({ message, data: error })
})
})
}
Puis le code suivant dans le fichier app.js :
require('./src/routes/login')(app)
7.4 JWT
Le module bcrypt est utilisé pour convertir les mots de passe des utilisateurs en un hash sécurisé, que l'on peut ensuite stocker en toute sécurité dans une base de données. L'authentification entre une application web et une API Rest repose souvent sur l'utilisation des « JSON Web Tokens » (JWT). Un JWT est une clé cryptée qui a une durée de validité définie et qui se présente sous la forme d'une chaîne de caractères. Le module jsonwebtoken permet de créer et de valider ces jetons JWT à l'aide des méthodes sign et verify.
Pour mettre en place les tokens JWT, nous devons utiliser la commande suivante :
npm install jsonwebtoken --save
Pour en savoir plus sur Json Web Token, consultez la documentation officielle.
Il nous faut créer un fichier /src/auth/private_key.js avec le contenu suivant :
module.exports = 'CUSTOM_PRIVATE_KEY'
Puis, il faut utiliser le code suivant dans le fichier /src/routes/login.js :
const { User } = require('../db/sequelize')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const privateKey = require('../auth/private_key')
module.exports = (app) => {
app.post('/api/login', (req, res) => {
User.findOne({ where: { username: req.body.username } }).then(user => {
if (!user) {
const message = 'Erreur, l'utilisateur n'existe pas !'
return res.status(404).json({message})
}
bcrypt.compare(req.body.password, user.password).then(isPasswordValid => {
if(!isPasswordValid) {
const message = `Erreur, le mot de passe est incorrect !`;
return res.status(401).json({ message })
}
const token = jwt.sign(
{userId: user.id},
privateKey,
{expiresIn: '24h'}
)
const message = `L'utilisateur a été connecté avec succès`;
return res.json({ message, data: user, token })
})
})
.catch(error => {
const message = `Erreur, la connexion a échoué. Réessayez dans quelques instants.`;
return res.json({ message, data: error })
})
})
}
Pour générer un jeton JWT valide, trois informations différentes sont nécessaires : les informations de l’utilisateur, une clé secrète, et une date de validité pour le jeton. Pour vérifier la validité du token JWT, il faut utiliser le code suivant dans le fichier auth.js :
const jwt = require('jsonwebtoken')
const privateKey = require('../auth/private_key')
module.exports = (req, res, next) => {
const authorizationHeader = req.headers.authorization
if(!authorizationHeader) {
const message = `Vous n'avez pas fourni de jeton d'authentification. Ajoutez-en un dans l'en-tête de la requête.`
return res.status(401).json({ message })
}
const token = authorizationHeader.split(' ')[1]
const decodedToken = jwt.verify(token, privateKey, (error, decodedToken) => {
if(error) {
const message = `L'utilisateur n'est pas autorisé à accèder à cette ressource.`
return res.status(401).json({ message, data: error })
}
const userId = decodedToken.userId
if (req.body.userId && req.body.userId !== userId) {
const message = `L'identifiant de l'utilisateur est invalide.`
res.status(401).json({ message })
} else {
next()
}
})
}
Le jeton JWT transite dans l’en-tête HTTP authorization, avec pour valeur "Bearer <JWT>".
7.5 Sécuriser
Pour sécuriser des routes, nous utilisons le code suivant dans le fichier findAllPokemon.js par exemple :
const auth = require('../auth/auth')
module.exports = (app) => {
app.get('/api/pokemons', auth, (req, res) => {
//...
8. Sécurité Express
8.1 Bonnes pratiques
Pour en savoir plus les meilleurs pratiques en terme de sécurité, consultez la documentation officielle.
Pour en savoir plus les meilleurs pratiques en terme de performance, consultez la documentation officielle.
Pour en savoir plus les meilleurs pratiques en terme de santé du système, consultez la documentation officielle.
8.2 Environnement production
Pour définir le port en production, il faut utiliser le code suivant dans le fichier app.js :
const port = process.env.PORT || 3000
Les variables d’environnement permettent de configurer votre projet Node.js en fonction de l’environnement sur lequel il sera exécuté. Pour correctement lancer l'API Node.js en production, nous devons ajouter cette commande script dans package.json :
"scripts": {
"start": "NODE_ENV=production node app.js",
"dev": "NODE_ENV=development nodemon app.js"
},
//...
Nous pouvons supprimer la dépendance Morgan pour l'environnement de production.
Il faut faire en sorte de synchroniser les données en production, grâce au code suivant dans sequelize.js :
//...
return sequelize.sync().then(_ => {
//...
8.3 CORS
Une page Web standard récupère souvent des ressources de différents sites. Pour assurer la sécurité des utilisateurs, les navigateurs modernes appliquent diverses politiques de sécurité. La politique du Same Origin est l'une des plus répandues, exigeant que toutes les ressources chargées proviennent du même serveur, ce qui n'est pas toujours le cas. Pour surmonter cette limite, la politique CORS (Cross-Origin Resource Sharing) a été instaurée. CORS permet de faire des requêtes pour des ressources externes à l'origin d'origine. Elle autorise les serveurs à spécifier qui peut accéder à leurs ressources et comment, en utilisant des en-têtes HTTP spécifiques comme Access-Control-Allow-Origin, Access-Control-Allow-Headers, et Access-Control-Request-Method. Les requêtes HTTP standards comme GET ou POST sont simples. Les requêtes plus complexes peuvent nécessiter une requête de contrôle préliminaire, appelée pre-flight, avec l'en-tête OPTIONS, pour s'assurer que l'accès est sécurisé. Si le résultat est favorable, la requête initiale est autorisée ; sinon, elle est bloquée.
Pour mettre en place le module CORS, nous utilisons la commande suivante :
npm install cors --save
Pour le mettre en place, cela se déroule dans le fichier app.js, où l'on doit ajouter le code suivant :
cconst cors = require('cors')
//...
app
.use(favicon(__dirname + '/favicon.ico'))
.use(bodyParser.json())
.use(cors())