Node.js
JavaScript côté serveur — event loop, npm, Express, API REST, fichiers, bases de données, JWT et déploiement. Pour étudiants qui connaissent déjà JS basique.
C'est quoi Node.js ?
Node.js est un environnement d'exécution JavaScript hors du navigateur, construit sur le moteur V8 de Chrome. Il permet d'écrire du code serveur en JavaScript — lire des fichiers, ouvrir des sockets, répondre à des requêtes HTTP — tout ce qu'un navigateur ne peut pas faire.
Comparaison Python : Node.js est à JavaScript ce que CPython est à Python — l'interpréteur qui fait tourner le code. La différence clé : Node est non-bloquant par défaut grâce à son event loop.
L'event loop — le cœur de Node
// Synchrone — s'exécute immédiatement
console.log('1 — début');
// Asynchrone — délégué à libuv, callback mis en file
setTimeout(() => console.log('3 — après 0ms'), 0);
// Synchrone — s'exécute avant le callback
console.log('2 — fin du code sync');
// Affichage : 1 → 2 → 3
// "0ms" ne signifie pas "immédiat" — la pile doit être vide d'abord
// Conséquence : Node peut gérer des milliers de connexions
// simultanées sans créer un thread par connexion (contrairement
// à Apache). C'est pourquoi il est idéal pour les API REST.
// Installer Node.js : https://nodejs.org (LTS recommandé)
// Vérifier :
node --version // v20.x.x
npm --version // 10.x.x
npm & package.json
# Créer un nouveau projet
npm init -y # -y = accepter tous les défauts
# Installer une dépendance (production)
npm install express
npm install express mysql2 jsonwebtoken dotenv
# Installer une dépendance de développement uniquement
npm install --save-dev nodemon jest
# Installer toutes les dépendances du projet (après git clone)
npm install
# Lancer un script défini dans package.json
npm start
npm run dev
npm test
# Voir les packages installés
npm list --depth=0
# Mettre à jour
npm update express
# Supprimer
npm uninstall express
Ne jamais committer node_modules/ sur Git. Ajouter node_modules dans .gitignore. Le dossier peut peser plusieurs centaines de MB.
{
"name": "mon-api",
"version": "1.0.0",
"description": "API REST Express",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js", // ← rechargement auto
"test": "jest"
},
"dependencies": {
"express": "^4.19.2",
"mysql2": "^3.9.4",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.1.0", // rechargement auto en dev
"jest": "^29.7.0" // tests
}
}
// package-lock.json = versions exactes installées
// → TOUJOURS committer ce fichier sur Git
Modules CommonJS & ESM
// math.js — exporter
function additionner(a, b) { return a + b; }
function multiplier(a, b) { return a * b; }
module.exports = { additionner, multiplier };
// ou : module.exports = { additionner, multiplier };
// main.js — importer
const { additionner, multiplier } = require('./math');
const express = require('express'); // module installé
const fs = require('fs'); // module natif Node
const path = require('path'); // module natif Node
console.log(additionner(2, 3)); // 5
// Dans package.json : "type": "esm"
// OU renommer les fichiers en .mjs
// math.mjs — exporter
export function additionner(a, b) { return a + b; }
export default function calculer() { /* ... */ }
// main.mjs — importer
import { additionner } from './math.mjs';
import calculer from './math.mjs'; // export default
import express from 'express';
import fs from 'fs';
import { readFile } from 'fs/promises';
À la HEH, les cours utilisent souvent CommonJS (require) car il est encore majoritaire dans les tutos et projets existants. ESM est la direction de l'avenir mais les deux coexistent.
Serveur HTTP natif
const http = require('http');
const serveur = http.createServer((req, res) => {
// req = requête entrante (url, method, headers, body...)
// res = réponse à envoyer
console.log(`${req.method} ${req.url}`);
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Bonjour !' }));
} else if (req.url === '/utilisateurs') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ id: 1, nom: 'Alice' }]));
} else {
res.writeHead(404);
res.end(JSON.stringify({ erreur: 'Route introuvable' }));
}
});
serveur.listen(3000, () => {
console.log('Serveur démarré sur http://localhost:3000');
});
// Lancer : node index.js
// Tester : curl http://localhost:3000
// ou navigateur / Postman / Thunder Client (VS Code)
Le module http natif est bas niveau — parfait pour comprendre ce qui se passe, mais vite verbeux pour des vraies API. En pratique on utilise Express qui simplifie tout ça. Comprendre le module http aide à déboguer Express.
// Le body arrive par morceaux (streams) → collecter
const serveur = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString(); // accumuler
});
req.on('end', () => {
const data = JSON.parse(body);
console.log(data); // objet JS
res.writeHead(201);
res.end(JSON.stringify({ recu: data }));
});
}
});
// Express fait ça automatiquement avec express.json()
Express — routing
const express = require('express');
const app = express();
// Middleware global — parser le JSON automatiquement
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // formulaires HTML
// Routes simples
app.get('/', (req, res) => {
res.json({ message: 'Bienvenue sur l\'API' });
});
// Paramètre de route — :id
app.get('/utilisateurs/:id', (req, res) => {
const { id } = req.params; // URL : /utilisateurs/42
res.json({ id: Number(id), nom: 'Alice' });
});
// Query string — ?page=2&limite=10
app.get('/articles', (req, res) => {
const { page = 1, limite = 10 } = req.query;
res.json({ page, limite });
});
// POST avec body JSON
app.post('/utilisateurs', (req, res) => {
const { nom, email } = req.body; // grâce à express.json()
if (!nom || !email)
return res.status(400).json({ erreur: 'Champs manquants' });
res.status(201).json({ id: 1, nom, email });
});
app.put('/utilisateurs/:id', (req, res) => { /* modifier */ });
app.delete('/utilisateurs/:id', (req, res) => { /* supprimer */ });
app.listen(3000, () => console.log('Port 3000'));
// routes/utilisateurs.js — router dédié
const router = require('express').Router();
router.get('/', listerUtilisateurs);
router.get('/:id', voirUtilisateur);
router.post('/', creerUtilisateur);
router.put('/:id', modifierUtilisateur);
router.delete('/:id', supprimerUtilisateur);
module.exports = router;
// index.js — monter le router
const utilisateursRouter = require('./routes/utilisateurs');
app.use('/api/utilisateurs', utilisateursRouter);
// Résultat :
// GET /api/utilisateurs → listerUtilisateurs
// GET /api/utilisateurs/42 → voirUtilisateur
// POST /api/utilisateurs → creerUtilisateur
// PUT /api/utilisateurs/42 → modifierUtilisateur
// DELETE /api/utilisateurs/42 → supprimerUtilisateur
Middleware
Un middleware est une fonction qui s'exécute entre la réception de la requête et l'envoi de la réponse. Chaque middleware peut modifier req, modifier res, ou passer au suivant avec next().
// Structure d'un middleware : (req, res, next)
function logger(req, res, next) {
const debut = Date.now();
console.log(`→ ${req.method} ${req.url}`);
res.on('finish', () => {
const duree = Date.now() - debut;
console.log(`← ${res.statusCode} (${duree}ms)`);
});
next(); // ← OBLIGATOIRE pour continuer la chaîne
}
// Middleware de validation
function validerUtilisateur(req, res, next) {
const { nom, email } = req.body;
if (!nom || !email) {
return res.status(400).json({ erreur: 'nom et email requis' });
// Si on return ici → next() n'est PAS appelé → chaîne stoppée
}
next();
}
// Appliquer globalement
app.use(logger);
// Appliquer sur une route spécifique
app.post('/utilisateurs', validerUtilisateur, creerUtilisateur);
const cors = require('cors');
// CORS — autoriser le front React/Vue à appeler l'API
app.use(cors({
origin: 'http://localhost:5173', // URL du front Vite/Vue
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Gestion d'erreurs globale — 4 paramètres obligatoires !
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(err.status || 500).json({
erreur: err.message || 'Erreur interne du serveur',
});
});
// Déclencher depuis une route :
app.get('/erreur', (req, res, next) => {
const err = new Error('Quelque chose a planté');
err.status = 503;
next(err); // passer l'erreur au middleware d'erreur
});
L'ordre des app.use() est important — les middlewares s'exécutent dans l'ordre de déclaration. Le middleware d'erreurs (4 params) doit toujours être en dernier.
API REST complète — architecture MVC
require('dotenv').config(); // ← toujours en premier
const express = require('express');
const cors = require('cors');
const articlesRouter = require('./routes/articles');
const utilisateursRouter = require('./routes/utilisateurs');
const app = express();
// Middlewares globaux
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/articles', articlesRouter);
app.use('/api/utilisateurs', utilisateursRouter);
// Middleware 404 — route non trouvée
app.use((req, res) => res.status(404).json({ erreur: 'Non trouvé' }));
// Middleware d'erreur global
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ erreur: err.message });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API sur http://localhost:${PORT}`));
const Article = require('../models/Article');
exports.lister = async (req, res, next) => {
try {
const articles = await Article.findAll();
res.json(articles);
} catch (err) { next(err); } // déléguer au middleware
};
exports.creer = async (req, res, next) => {
try {
const { titre, contenu } = req.body;
const article = await Article.create({ titre, contenu,
auteur_id: req.user.id }); // req.user injecté par JWT
res.status(201).json(article);
} catch (err) { next(err); }
};
Lire/écrire des fichiers — module fs
const fs = require('fs/promises');
const path = require('path');
// Lire un fichier texte
async function lireFichier() {
const contenu = await fs.readFile('data.txt', 'utf-8');
console.log(contenu);
}
// Écrire (remplace le fichier existant)
await fs.writeFile('sortie.txt', 'Bonjour !', 'utf-8');
// Ajouter en fin de fichier
await fs.appendFile('log.txt', `${new Date().toISOString()} — action\n`);
// Lire un JSON
async function lireJson(fichier) {
const brut = await fs.readFile(fichier, 'utf-8');
return JSON.parse(brut);
}
// Écrire un JSON
async function ecrireJson(fichier, data) {
await fs.writeFile(fichier, JSON.stringify(data, null, 2));
}
// Chemin absolu — indépendant du répertoire courant
const chemin = path.join(__dirname, 'data', 'users.json');
// Vérifier l'existence
try {
await fs.access(chemin);
console.log('existe');
} catch { console.log('inexistant'); }
// Lister un dossier
const fichiers = await fs.readdir('./uploads');
// Créer un dossier
await fs.mkdir('./uploads', { recursive: true }); // recursive = pas d'erreur si existe
// Supprimer
await fs.unlink('./temp/fichier.tmp');
Toujours utiliser fs/promises (ou util.promisify) plutôt que fs synchrone (readFileSync) dans un serveur. Les méthodes sync bloquent l'event loop et empêchent le serveur de traiter d'autres requêtes pendant la lecture.
// npm install multer
const multer = require('multer');
const stockage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const nom = Date.now() + ext;
cb(null, nom);
},
});
const upload = multer({
storage: stockage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
fileFilter: (req, file, cb) => {
const ok = ['image/jpeg', 'image/png'].includes(file.mimetype);
cb(null, ok);
},
});
app.post('/upload', upload.single('photo'), (req, res) => {
res.json({ url: `/uploads/${req.file.filename}` });
});
Variables d'environnement & .env
# .env — NE JAMAIS committer ce fichier !
# L'ajouter dans .gitignore
PORT=3000
NODE_ENV=development
# Base de données
DB_HOST=localhost
DB_PORT=3306
DB_NAME=mon_app
DB_USER=root
DB_PASSWORD=mot_de_passe_secret
# JWT
JWT_SECRET=une_chaine_tres_longue_et_aleatoire_32chars
JWT_EXPIRES_IN=7d
# API externe
OPENWEATHER_API_KEY=abc123xyz
# .env.example — modèle sans les vraies valeurs
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_PORT=3306
DB_NAME=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
JWT_EXPIRES_IN=7d
// npm install dotenv
// TOUJOURS charger en premier dans index.js
require('dotenv').config();
// Accéder aux variables
const port = process.env.PORT || 3000;
const dbHost = process.env.DB_HOST;
const jwtSecret = process.env.JWT_SECRET;
const isDev = process.env.NODE_ENV === 'development';
// Bonne pratique : centraliser dans config/
// config/index.js
module.exports = {
port: process.env.PORT || 3000,
db: {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT) || 3306,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
};
// Utilisation dans le code
const config = require('./config');
app.listen(config.port);
En production, ne pas déployer le fichier .env — configurer les variables directement dans l'environnement du serveur (PM2 ecosystem, variables d'environnement système, secrets du service cloud).
Connexion base de données — MySQL & MongoDB
// npm install mysql2
// config/db.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
waitForConnections: true,
connectionLimit: 10, // max 10 connexions simultanées
queueLimit: 0,
});
module.exports = pool;
// models/Article.js
const db = require('../config/db');
const Article = {
async findAll() {
const [rows] = await db.query('SELECT * FROM articles');
return rows;
},
async findById(id) {
const [rows] = await db.query(
'SELECT * FROM articles WHERE id = ?', [id]
);
return rows[0] || null;
},
async create({ titre, contenu, auteur_id }) {
const [result] = await db.query(
'INSERT INTO articles (titre, contenu, auteur_id) VALUES (?, ?, ?)',
[titre, contenu, auteur_id] // ← paramètres préparés = protection SQL injection
);
return { id: result.insertId, titre, contenu, auteur_id };
},
async update(id, data) {
await db.query(
'UPDATE articles SET titre=?, contenu=? WHERE id=?',
[data.titre, data.contenu, id]
);
},
async delete(id) {
await db.query('DELETE FROM articles WHERE id=?', [id]);
},
};
module.exports = Article;
// npm install mongoose
// config/db.js
const mongoose = require('mongoose');
async function connecterBDD() {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB connecté');
}
module.exports = connecterBDD;
// Dans index.js
await connecterBDD();
// models/Article.js — schéma Mongoose
const { Schema, model } = require('mongoose');
const articleSchema = new Schema({
titre: { type: String, required: true, trim: true },
contenu: { type: String, required: true },
auteur: { type: Schema.Types.ObjectId, ref: 'User' },
publie: { type: Boolean, default: false },
tags: [String],
}, { timestamps: true }); // createdAt, updatedAt auto
module.exports = model('Article', articleSchema);
// CRUD avec Mongoose
const Article = require('./models/Article');
const tous = await Article.find();
const un = await Article.findById(id);
const filtre = await Article.find({ publie: true }).sort('-createdAt').limit(10);
const cree = await Article.create({ titre, contenu });
await Article.findByIdAndUpdate(id, { titre: 'Nouveau' });
await Article.findByIdAndDelete(id);
| MySQL | MongoDB / Mongoose | Quand choisir |
|---|---|---|
| Tables & colonnes | Collections & documents | MySQL si données relationnelles |
| Schéma fixe | Schéma flexible | MongoDB si structure variable |
| SQL (JOIN, etc.) | Aggregation pipeline | MySQL si beaucoup de jointures |
mysql2 | mongoose | MongoDB si démo/prototype |
Authentification JWT
Un JSON Web Token est un jeton signé que le serveur délivre à la connexion. Le client le stocke et l'envoie dans chaque requête. Le serveur vérifie la signature — sans consulter la base de données.
// npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// Inscription — hacher le mot de passe
exports.inscription = async (req, res, next) => {
try {
const { nom, email, motDePasse } = req.body;
const hash = await bcrypt.hash(motDePasse, 12); // coût = 12
const user = await User.create({ nom, email, motDePasse: hash });
res.status(201).json({ message: 'Compte créé', id: user.id });
} catch (err) { next(err); }
};
// Connexion — vérifier et signer
exports.connexion = async (req, res, next) => {
try {
const { email, motDePasse } = req.body;
const user = await User.findByEmail(email);
if (!user) return res.status(401).json({ erreur: 'Identifiants incorrects' });
const ok = await bcrypt.compare(motDePasse, user.motDePasse);
if (!ok) return res.status(401).json({ erreur: 'Identifiants incorrects' });
const token = jwt.sign(
{ userId: user.id, role: user.role }, // payload (visible !)
process.env.JWT_SECRET, // clé secrète
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
res.json({ token, user: { id: user.id, nom: user.nom, email: user.email } });
} catch (err) { next(err); }
};
// middleware/auth.js
const jwt = require('jsonwebtoken');
function verifierToken(req, res, next) {
const authHeader = req.headers['authorization'];
// Header attendu : "Authorization: Bearer eyJhbGci..."
if (!authHeader?.startsWith('Bearer '))
return res.status(401).json({ erreur: 'Token manquant' });
const token = authHeader.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // { userId, role, iat, exp }
next();
} catch (err) {
res.status(401).json({ erreur: 'Token invalide ou expiré' });
}
}
// Middleware de rôle
function exigerRole(role) {
return (req, res, next) => {
if (req.user?.role !== role)
return res.status(403).json({ erreur: 'Accès interdit' });
next();
};
}
module.exports = { verifierToken, exigerRole };
// Utilisation dans les routes
const { verifierToken, exigerRole } = require('../middleware/auth');
router.get('/profil', verifierToken, voirProfil);
router.delete('/:id', verifierToken, exigerRole('admin'), supprimer);
Le payload JWT est encodé en base64, pas chiffré — ne jamais y mettre de mot de passe ou de donnée sensible. Seule la signature garantit l'intégrité.
Déploiement & PM2
# Installer PM2 globalement
npm install -g pm2
# Lancer l'application
pm2 start src/index.js --name mon-api
# Lancer avec rechargement auto (équiv. nodemon en prod)
pm2 start src/index.js --name mon-api --watch
# Voir les applications en cours
pm2 list
# Logs en temps réel
pm2 logs mon-api
# Redémarrer
pm2 restart mon-api
# Arrêter
pm2 stop mon-api
# Démarrer au boot du serveur
pm2 startup
pm2 save
module.exports = {
apps: [{
name: 'mon-api',
script: 'src/index.js',
instances: 'max', // utiliser tous les CPU
exec_mode: 'cluster', // répartition de charge
watch: false, // pas de watch en prod
env: {
NODE_ENV: 'production',
PORT: 3000,
},
}],
};
# Lancer avec ce fichier
pm2 start ecosystem.config.js
# 1. Variables d'environnement sur le serveur
export NODE_ENV=production
export JWT_SECRET=...
export DB_PASSWORD=...
# 2. Installer seulement les dépendances de prod
npm install --production
# 3. Ne jamais exposer Node directement sur le port 80
# → Utiliser un reverse proxy (Nginx)
# nginx.conf
server {
listen 80;
server_name mon-api.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# 4. HTTPS avec Let's Encrypt
sudo certbot --nginx -d mon-api.com
# 5. Vérifier que l'app redémarre après reboot
pm2 startup # génère la commande à exécuter
pm2 save # sauvegarde la liste des apps
NODE_ENV=production active automatiquement les optimisations dans Express (cache des vues, messages d'erreur simplifiés, etc.) et dans la plupart des bibliothèques npm.
Intégration avec React & Vue
// src/services/api.js — centraliser les appels API
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
async function requete(url, options = {}) {
const token = localStorage.getItem('token');
const res = await fetch(BASE_URL + url, {
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
...options,
});
if (!res.ok) throw new Error(`Erreur ${res.status}`);
return res.json();
}
export const api = {
get: (url) => requete(url),
post: (url, data) => requete(url, { method: 'POST', body: JSON.stringify(data) }),
put: (url, data) => requete(url, { method: 'PUT', body: JSON.stringify(data) }),
delete: (url) => requete(url, { method: 'DELETE' }),
};
// Composant Vue — utiliser l'API
// <script setup>
import { ref, onMounted } from 'vue';
import { api } from '../services/api';
const articles = ref([]);
const chargement = ref(true);
const erreur = ref(null);
onMounted(async () => {
try {
articles.value = await api.get('/articles');
} catch (e) { erreur.value = e.message; }
finally { chargement.value = false; }
});
// vite.config.js — proxy en développement
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000', // ton API Express
changeOrigin: true,
},
},
},
});
// Avec ce proxy, fetch('/api/articles') en dev passe par
// le serveur Vite → redirige vers Express → pas de CORS
projet-fullstack/
├── backend/ ← API Express
│ ├── src/
│ │ └── index.js
│ └── package.json
├── frontend/ ← Vite + Vue/React
│ ├── src/
│ │ └── App.vue
│ └── package.json
└── README.md
# Lancer les deux en parallèle (package racine)
# npm install concurrently --save-dev
# "dev": "concurrently \"npm run dev -w backend\" \"npm run dev -w frontend\""
En production, le front est buildé (npm run build → dossier dist/). Express peut servir ces fichiers statiques directement, ou les deux sont déployés séparément (front sur Netlify/Vercel, back sur VPS).
Cheat Sheet Node.js & Express
⌨️ Commandes
node fichier.js | Exécuter un script |
npm init -y | Nouveau projet |
npm install X | Installer un package |
npm run dev | Mode dev (nodemon) |
npm start | Mode production |
pm2 start app.js | Déploiement PM2 |
pm2 logs | Logs en direct |
🛣️ Express routing
app.get(url, fn) | GET lire |
app.post(url, fn) | POST créer |
app.put(url, fn) | PUT remplacer |
app.patch(url, fn) | PATCH modifier |
app.delete(url, fn) | DELETE supprimer |
req.params.id | :id dans l'URL |
req.query.page | ?page=2 |
req.body | Body JSON (express.json()) |
📦 Packages essentiels
express | Serveur web |
dotenv | Variables .env |
cors | Cross-Origin |
mysql2 | MySQL |
mongoose | MongoDB ODM |
jsonwebtoken | JWT |
bcryptjs | Hachage mots de passe |
multer | Upload fichiers |
nodemon | Reload auto (dev) |
🔐 JWT flow
| Connexion | jwt.sign(payload, secret) |
| Vérifier | jwt.verify(token, secret) |
| Header | Authorization: Bearer TOKEN |
| Hacher mdp | bcrypt.hash(mdp, 12) |
| Comparer | bcrypt.compare(mdp, hash) |
| 401 | Pas de token / expiré |
| 403 | Token ok, pas les droits |