TP2e année26 livrables
AppliFrais, comprendre chaque choix
Le guide de l'étudiant : on construit toute l'application — MongoDB Atlas, l'API Express, l'authentification, les rôles, le front React, l'OCR, le stockage S3, la sécurité — et surtout, on explique pourquoi on fait chaque chose. C'est cette compréhension que tu défendras devant le jury E6.
Par Josselyn C., prof de BTS SIO. Publié le , mis à jour le .
- Pour qui
- toi, qui construis AppliFrais et devras l'expliquer et la modifier seul
- Objectif
- ne jamais taper une ligne sans savoir à quoi elle sert et pourquoi c'est ce choix-là
- Pré-requis
- les bases de JavaScript ; avoir suivi la construction au tableau
- Pour qui
- SLAM, 2e année
- Durée
- à ton rythme (l'équivalent d'une quinzaine de séances)
- Matériel
- Un PC où tu peux installer Node.js (20 ou plus), Docker, Git et VS Code, plus un client HTTP comme Thunder Client ou Postman.
- Outils
- Node.js, Express, MongoDB, Mongoose, React
Compétences : C1.4 Travailler en mode projet, C1.5 Mettre à disposition des utilisateurs un service informatique, C2.1 Concevoir et développer une solution applicative, C2.2 Assurer la maintenance corrective ou évolutive d'une application, C2.3 Gérer les données, C3.1 Protéger les données à caractère personnel, C3.5 Assurer la cybersécurité d'une solution applicative
Ce guide suit le même projet que le script de construction, mais il prend le temps : chaque notion est définie depuis zéro, chaque décision est justifiée, chaque piège est anticipé. Tu peux t'en servir en autonomie, et y revenir quand tu prépares ton oral.
Avant de commencer
Ce que tu vas construire — et pourquoi ça compte
GSB AppliFrais, c'est l'application de gestion des frais des visiteurs médicaux de GSB. Mais pour toi, c'est surtout ton projet AP2 : celui que tu présentes à l'épreuve E6. Le jury ne va pas seulement regarder si « ça marche » — il va te demander pourquoi tu as fait tel choix, ce que tu ferais autrement, comment tu garantis la sécurité et l'intégrité des données. Ce guide existe pour que tu aies une réponse à chaque « pourquoi ».
Comment te servir de ce guide (en autonomie)
La feuille de route
| Chapitre | Ce que tu comprends & construis | La grande question |
|---|---|---|
| 0 · Le décor | le besoin GSB, l'architecture 3 tiers, pourquoi le cloud | « à quoi sert chaque morceau ? » |
| 1 · Les données | MongoDB, modéliser, embarquer vs référencer, index, validation | « pourquoi ce modèle, et pas un autre ? » |
| 2 · L'API | Express, REST, les verbes et codes HTTP | « comment le front parle au back ? » |
| 3 · L'identité | bcrypt, JWT, pourquoi signé et pas chiffré | « comment je sais qui tu es, sans danger ? » |
| 4 · Les droits | CRUD, rôles, anti-IDOR, cycle de vie | « qui a le droit de faire quoi ? » |
| 5 · Les requêtes | l'agrégation, les tableaux de bord | « comment je croise et je totalise ? » |
| 6 · L'écran | React, composants, état, appels à l'API | « comment l'utilisateur voit tout ça ? » |
| 7 · Le reçu | OCR, stockage S3, liens signés, RGPD | « où vont les fichiers, et est-ce légal ? » |
| 8 · La sécurité | OWASP appliqué, on attaque notre propre appli | « qu'est-ce qui me protège, concrètement ? » |
Chapitre É — Les écrans d'AppliFrais (les maquettes)
Avant de coder le front, il faut voir ce qu'on construit. Voici les maquettes de chaque écran, annotées : à quoi sert chaque zone, et surtout quelle route de l'API elle appelle. Garde ce chapitre sous les yeux quand tu construiras le front (chapitre 6) : chaque écran du code correspond à une maquette ici.
Écran 1 — La connexion
1 Le bouton envoie POST /api/v1/auth/login avec l'email + mot de passe. En retour : un jeton JWT qu'on range dans le navigateur, puis on redirige vers « Mes fiches ». Si c'est faux → message d'erreur rouge sous le formulaire (401).
Écran 2 — Mes fiches (vue visiteur)
1 Saisir un mois + « Nouvelle fiche » → POST /api/v1/fiches (crée une fiche en état Créée). 2 « Ouvrir » → écran 3 (détail). Le tableau vient de GET /api/v1/fiches : l'API ne renvoie que les fiches du visiteur connecté — la colonne « Visiteur » n'existe donc pas ici.
Écran 3 — Détail & saisie d'une fiche (vue visiteur)
1 Saisir les quantités forfait → PUT /api/v1/fiches/:id/forfait (le total se recalcule). 2 Ajouter un frais libre → POST /api/v1/fiches/:id/lignes. 3 « Scanner un reçu » → écran 5 (OCR). 4 « Clôturer » → PUT /api/v1/fiches/:id/cloturer (passe en état Clôturée, le visiteur ne peut plus modifier).
Écran 4 — Toutes les fiches (vue comptable)
1 « Valider » → PUT /api/v1/fiches/:id/valider, réservé au comptable (un visiteur obtiendrait un 403). La même page React que l'écran 2, mais le rôle change le titre, ajoute la colonne Visiteur et le bouton Valider. Un seul code, deux métiers.
Écran 5 — Scanner un reçu (OCR)
1 La photo part en POST /api/v1/fiches/:id/analyse : l'API la range dans S3, lit le texte (OCR) et propose montant/date (vert = sûr, orange = à vérifier). Rien n'est encore enregistré. 2 Après vérification/correction par le visiteur, « Confirmer » → POST /api/v1/fiches/:id/lignes (la route de l'écran 3 !) enregistre le frais. La machine propose, l'humain valide.
La correspondance maquette → route → chapitre
| Écran | Route(s) API | Construit au… |
|---|---|---|
| 1 · Connexion | POST /auth/login | chap. 6 (+ auth chap. 3) |
| 2 · Mes fiches | GET /fiches, POST /fiches | chap. 6 (+ CRUD chap. 4) |
| 3 · Détail & saisie | GET /fiches/:id, /forfait, /lignes, /cloturer | chap. 6 (+ chap. 4) |
| 4 · Toutes les fiches | GET /fiches, /valider | chap. 6 (+ rôles chap. 4) |
| 5 · Scanner un reçu | /analyse puis /lignes | chap. 6 (+ OCR chap. 7) |
Livrable É
Relie l'écran à l'API
Pour l'écran 3, liste chaque bouton/champ et la route exacte qu'il appelle, avec le verbe HTTP. Puis dessine (à la main ou sur un outil) un 6ᵉ écran qui manque selon toi (ex. l'historique des états d'une fiche, ou le tableau de bord des statistiques) et indique quelle(s) route(s) il appellerait.
Rends : ton tableau + ton croquis.
0. Le décor : comprendre avant de coder
0.1 — Le besoin de GSB (pourquoi cette appli existe)
Le laboratoire Galaxy Swiss Bourdin emploie des centaines de visiteurs médicaux qui sillonnent la France. Chacun engage des frais (route, hôtel, repas, péages). Aujourd'hui, c'est géré au tableur envoyé par mail : lent, source d'erreurs, impossible à suivre, et chaque comptable a sa version du fichier.
GSB veut une application web unique où le visiteur saisit ses frais, et le comptable contrôle, valide et met en paiement. Tout tracé, rapide, accessible de partout.
0.2 — Les deux acteurs et la règle d'or
| Acteur | Ce qu'il fait | Ce qu'il ne fait JAMAIS |
|---|---|---|
| Visiteur médical | saisit ses frais, clôture sa fiche, consulte son historique | valider ou payer une fiche |
| Comptable | consulte les fiches clôturées, valide, met en paiement | saisir des frais à la place d'un visiteur |
On garde tout au long du projet le jeu de test officiel : Bunisset, Cottet, Tusseau (visiteurs) et Lacombe (comptable).
0.3 — L'architecture 3 tiers (les trois morceaux qui se parlent)
Toute application web moderne se découpe en trois couches (« tiers »), chacune avec un rôle unique. C'est comme un restaurant : la salle (ce que voit le client), la cuisine (qui prépare), le garde-manger (qui stocke).
0.4 — Pourquoi le cloud (Atlas, S3) et pas « sur ma machine » ?
La base (MongoDB) vit sur MongoDB Atlas, et les fichiers (reçus) dans un stockage S3. Deux services cloud. Pourquoi ne pas tout mettre sur ton PC ?
| Sur ta machine | Dans le cloud (Atlas / S3) |
|---|---|
| chaque coéquipier a SA base, différente | toute l'équipe pointe vers la même base |
| si ton PC est éteint, plus rien ne marche | accessible 24/7, de n'importe où |
| sauvegarde à faire soi-même | sauvegardes et réplication gérées |
| « ça marche chez moi » le jour du jury | démo reproductible, pro |
Livrable 0
Le trajet d'une donnée
Pour l'action « le visiteur consulte sa fiche de mai », décris le trajet complet à travers les 3 tiers : qui demande, qui traite, qui stocke, puis le chemin retour. Cite la techno et le rôle de chaque étape.
Rends : ta description (4 à 6 lignes). C'est un excellent entraînement pour l'oral.
Chapitre M — Mettre en place l'atelier
Avant de coder, on prépare le terrain : la base dans le cloud (Atlas), le stockage des fichiers (S3), et le projet Node. Fais-le une fois, tranquillement — idéalement la veille d'une grosse séance.
M.1 — Créer la base : MongoDB Atlas
Sur mongodb.com/atlas :
| Étape | Ce que tu fais | Pourquoi |
|---|---|---|
| Compte + cluster | offre M0 (gratuite), région Paris | 512 Mo, suffisant pour l'AP2 ; données en Europe (RGPD) |
| Database Access | un utilisateur gsb + mot de passe | c'est le compte que l'API utilisera |
| Network Access | autoriser ton IP (0.0.0.0/0 en salle) | sinon la connexion part dans le vide (timeout) |
| Connect → Drivers | copier la chaîne de connexion | c'est l'URL que l'API mettra dans son .env |
mongodb+srv://gsb:MOTDEPASSE@cluster0.xxxxx.mongodb.net/gsb_applifrais?retryWrites=true&w=majority
M.2 — Créer le coffre à fichiers : un bucket S3
Un bucket S3 (AWS, ou un équivalent compatible comme Cloudflare R2) stockera les reçus. Sur la console AWS : crée un bucket gsb-applifrais-justificatifs, privé (Block all public access ON), région Paris ; crée un utilisateur avec une clé d'accès + secret.
M.3 — Initialiser le projet
mkdir gsb-applifrais && cd gsb-applifrais
mkdir backend frontend
cd backend
npm init -y
npm install express cors helmet dotenv mongoose jsonwebtoken bcryptjs zod \
express-rate-limit multer tesseract.js @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
npm install --save-dev nodemon
M.4 — Les secrets : backend/.env
MONGODB_URI=mongodb+srv://gsb:...@cluster0.xxxxx.mongodb.net/gsb_applifrais?retryWrites=true&w=majority
JWT_SECRET=une-longue-chaine-aleatoire-de-32-caracteres-minimum
PORT=3000
CLIENT_URL=http://localhost:5173
S3_REGION=eu-west-3
S3_BUCKET=gsb-applifrais-justificatifs
S3_KEY=ta-cle-d-acces
S3_SECRET=ton-secret
Chapitre M (suite) — L'outillage : node, npm & les scripts
Tu as tapé npm init et npm install sans qu'on explique ce qui se passe. On comble ça maintenant : ce sont les fondations de tout projet JavaScript, back comme front.
M.5 — C'est quoi Node & npm ?
M.6 — package.json : la carte d'identité du projet
{
"name": "backend",
"scripts": {
"dev": "nodemon src/server.js",
"start":"node src/server.js",
"seed": "node src/seed.js"
},
"dependencies": { "express": "^4.21.2", "mongoose": "^8.8.0" },
"devDependencies": { "nodemon": "^3.1.7" }
}
M.7 — node_modules & package-lock : à comprendre, pas à versionner
M.8 — nodemon : relancer l'API toute seule
npm install --save-dev nodemon
# puis, en dev :
npm run dev # nodemon surveille src/ et redémarre à chaque sauvegarde
Chapitre M (suite) — La configuration complète (back + front)
Une appli se configure par des variables d'environnement : les réglages qui changent d'une machine à l'autre (URL de base, secrets) vivent hors du code. On a vu le .env du back ; voici la vue d'ensemble, front compris.
M.9 — Toutes les variables, d'un coup d'œil
| Où | Variable | Rôle | Secret ? |
|---|---|---|---|
| back | MONGODB_URI | connexion au cluster Atlas | oui |
| back | JWT_SECRET | signe/valide les jetons | oui |
| back | PORT | port de l'API (3000) | non |
| back | CLIENT_URL | origine autorisée par CORS | non |
| back | S3_REGION / BUCKET | emplacement du coffre S3 | non |
| back | S3_KEY / S3_SECRET | accès au bucket | oui |
| front | VITE_API_URL | URL de l'API en production | non — publique ! |
M.10 — Le .env du front & la règle VITE_
# frontend/.env
VITE_API_URL=http://localhost:3000/api/v1
M.11 — .env.example : partager la forme, pas les secrets
# backend/.env.example — committé sur Git (valeurs VIDES)
MONGODB_URI=
JWT_SECRET=
PORT=3000
CLIENT_URL=http://localhost:5173
S3_REGION=
S3_BUCKET=
S3_KEY=
S3_SECRET=
Chapitre M (suite) — L'arborescence complète du projet
Voici la carte complète du dépôt, telle qu'elle sera à la fin. Garde-la sous les yeux : chaque fichier des chapitres suivants a sa place ici.
gsb-applifrais/
├── package.json # scripts racine (concurrently)
├── .gitignore
│
├── backend/ # l'API Express (port 3000)
│ ├── .env # secrets — IGNORÉ par Git
│ ├── .env.example # modèle committé
│ ├── package.json
│ └── src/
│ ├── server.js # démarrage
│ ├── app.js # middlewares + routes
│ ├── seed.js # jeu de test
│ ├── config/ # db.js · bareme.js · s3.js · validation.js
│ ├── models/ # visiteur · fiche · typeFrais
│ ├── controllers/ # auth · fiches · ocr · stats
│ ├── routes/ # index + auth + fiches + stats
│ ├── middlewares/ # auth (requireAuth/requireRole) · errorHandler
│ ├── validators/ # schemas.js (Zod)
│ ├── services/ # stockage.js (S3)
│ └── utils/ # AppError · asyncHandler
│
└── frontend/ # le front React/Vite (port 5173)
├── .env # VITE_API_URL — IGNORÉ par Git
├── index.html
├── vite.config.js
├── package.json
└── src/
├── main.jsx # point d'entrée + routeur + garde
├── services/api.js # appels réseau centralisés
└── components/ # Login · Liste · Detail · Layout
1. Les données : le chapitre qui fait la différence
C'est ici que beaucoup de projets se font reprocher un « modèle trop simple » au jury. On va donc prendre le temps de tout justifier. À la fin de ce chapitre, tu sauras défendre chaque décision de ta base.
1.1 — C'est quoi une base « document » (NoSQL) ?
En 1re année, tu as utilisé MySQL : des tables, des lignes, des colonnes, reliées par des clés étrangères. C'est le modèle relationnel. MongoDB, lui, est une base documentaire : elle range des documents qui ressemblent à des objets JSON.
// un document "fiche" dans MongoDB
{
_id: ObjectId("665f..."),
visiteur: ObjectId("665a..."), // un lien vers un autre document
mois: "2026-05",
etat: "CL",
lignesHorsForfait: [ // un TABLEAU, directement dans le document
{ libelle: "Train Paris", montant: 47.5 }
]
}
Le vocabulaire, côte à côte
| MySQL (1re année) | MongoDB (2e année) | La nuance |
|---|---|---|
| Table | Collection | pas de colonnes imposées : deux documents peuvent différer |
| Ligne (row) | Document (JSON) | peut contenir tableaux et sous-objets |
| Colonne | Champ (field) | — |
| Clé primaire auto | _id (ObjectId) | 12 octets, contient la date de création |
| Clé étrangère (FK) | référence par _id | pas imposée par Mongo — c'est à toi de la gérer |
| JOIN | populate() / $lookup | plus coûteux → on cherche souvent à l'éviter |
1.2 — SQL ou NoSQL : lequel est « mieux » ? (la vraie réponse)
1.3 — LA décision centrale : embarquer ou référencer ?
En NoSQL, la question n°1 n'est pas « comment je normalise » (découper en tables sans redondance, le réflexe SQL). C'est : « qu'est-ce que je lis toujours ensemble ? »
| Technique | Idée | Quand l'utiliser |
|---|---|---|
| Embarquer (embedding) | ranger les données dans le document parent (un tableau) | données toujours lues avec le parent, jamais seules, taille bornée |
| Référencer (referencing) | stocker juste l'_id de l'autre document (comme une FK) | données partagées, mutables, volumineuses, ou interrogées seules |
Appliqué à AppliFrais, décision par décision
Les lignes de frais → EMBARQUÉES. Pourquoi ? On affiche toujours une fiche en entier (« montre-moi la fiche de mai de Bunisset »). On ne demande jamais « la 3ᵉ ligne de frais toute seule ». Donc : un document = une fiche complète, lue ou écrite en une seule opération, sans JOIN. C'est l'inverse du réflexe SQL, et c'est volontaire.
L'historique des états → EMBARQUÉ. Petit, lu avec la fiche, jamais seul. Il donne la traçabilité : qui a clôturé/validé, et quand.
Le visiteur → RÉFÉRENCÉ. Pourquoi pas embarqué ? Parce qu'un visiteur a plusieurs fiches (données partagées) et qu'il change (il déménage, change d'email : donnée mutable). Si on recopiait son nom dans chaque fiche, le jour où il change de nom, on aurait des fiches incohérentes. On garde donc un lien (ObjectId) qu'on « rapproche » avec populate() quand on a besoin du détail.
Le barème → RÉFÉRENCÉ (dans sa propre collection). C'est une donnée de référence partagée par toutes les fiches, et qui évolue (GSB change ses tarifs). On la met dans une collection à part, typesFrais, et on la joint par $lookup.
Livrable 1
Justifie ton modèle
Pour chacune des 4 données (lignes de frais, historique, visiteur, barème), écris en une phrase : embarqué ou référencé, et pourquoi. C'est littéralement une question d'oral — entraîne-toi à la dire à voix haute.
Rends : tes 4 justifications.
Chapitre 1 (suite) — Écrire et blinder le modèle
1.4 — Le barème dans sa collection — typesFrais
Première décision concrète : le barème (combien coûte un km, un repas…) n'est pas écrit en dur dans le code. C'est une donnée, donc une collection.
const mongoose = require("mongoose");
const typeFraisSchema = new mongoose.Schema({
code: { type: String, required: true, unique: true }, // "KM","EF","NUI","REP"
libelle: { type: String, required: true },
montant: { type: Number, required: true, min: 0 }, // € par unité
unite: { type: String },
actif: { type: Boolean, default: true },
dateDebut: { type: Date, default: Date.now },
}, { timestamps: true });
module.exports = mongoose.model("TypeFrais", typeFraisSchema, "typesFrais");
1.5 — Le modèle Visiteur (un seul pour tout le monde)
const visiteurSchema = new mongoose.Schema({
nom: { type: String, required: true },
prenom: { type: String, required: true },
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
motDePasseHash: { type: String, required: true },
role: { type: String, enum: ["visiteur", "comptable"], default: "visiteur" },
ville: { type: String },
}, { timestamps: true });
module.exports = mongoose.model("Visiteur", visiteurSchema);
1.6 — Le modèle Fiche (le cœur)
const bareme = require("../config/bareme");
const ligneForfaitSchema = new mongoose.Schema({
codeType: { type: String, enum: ["KM", "EF", "NUI", "REP"], required: true },
quantite: { type: Number, required: true, min: 0 },
}, { _id: false });
const ligneHorsForfaitSchema = new mongoose.Schema({
date: { type: Date, required: true },
libelle: { type: String, required: true, maxlength: 100 },
montant: { type: Number, required: true, min: 0 },
justificatif: { type: String }, // clé du reçu dans le stockage S3
});
const evenementSchema = new mongoose.Schema({
etat: { type: String, enum: ["CR", "CL", "VA", "MP", "RB"], required: true },
par: { type: mongoose.Schema.Types.ObjectId, ref: "Visiteur" },
date: { type: Date, default: Date.now },
}, { _id: false });
const ficheSchema = new mongoose.Schema({
visiteur: { type: mongoose.Schema.Types.ObjectId, ref: "Visiteur", required: true },
mois: { type: String, required: true, match: /^\d{4}-\d{2}$/ },
etat: { type: String, enum: ["CR", "CL", "VA", "MP", "RB"], default: "CR" },
lignesForfait: [ligneForfaitSchema],
lignesHorsForfait: [ligneHorsForfaitSchema],
historiqueEtats: [evenementSchema],
schemaVersion: { type: Number, default: 1 },
}, { timestamps: true, toJSON: { virtuals: true } });
ficheSchema.index({ visiteur: 1, mois: 1 }, { unique: true });
ficheSchema.index({ etat: 1 });
ficheSchema.virtual("total").get(function () {
const forfait = (this.lignesForfait || []).reduce(
(s, l) => s + l.quantite * bareme.montant(l.codeType), 0);
const hors = (this.lignesHorsForfait || []).reduce((s, l) => s + l.montant, 0);
return Math.round((forfait + hors) * 100) / 100;
});
Décodons chaque idée, et son pourquoi
1.7 — Les index : la règle métier ET la performance
Deux index, deux raisons différentes :
1.8 — Blinder : la validation au niveau de la base
Mongoose valide côté application. Mais si demain un script, un collègue ou une erreur écrit directement dans la base en contournant ton code ? On pose une 2ᵉ barrière, dans MongoDB lui-même :
await db.command({
collMod: "fiches",
validator: {
$jsonSchema: {
bsonType: "object",
required: ["visiteur", "mois", "etat"],
properties: {
mois: { bsonType: "string", pattern: "^[0-9]{4}-[0-9]{2}$" },
etat: { enum: ["CR", "CL", "VA", "MP", "RB"] },
},
},
},
validationLevel: "strict",
});
Livrable 2
Casser la règle exprès
Dans mongosh (ou l'interface Atlas), tente d'insérer deux fiches du même visiteur pour le même mois, puis une fiche avec etat: "ZZ". Note les deux messages d'erreur exacts.
Rends : les deux erreurs, et une phrase expliquant quelle barrière a bloqué (index unique / validateur $jsonSchema).
Chapitre 1 (fin) — Le modèle face au jury
1.9 — « Votre modèle est trop simple » : la réponse
C'est le reproche classique — et souvent injuste. Voici comment le désamorcer.
Le MLD relationnel équivalent — ta preuve que c'est un choix
Montre le modèle que tu aurais fait en SQL. Ça prouve que tu connais les deux mondes et que ton choix documentaire est délibéré.
En relationnel : 5 tables, 4 clés étrangères, un JOIN à chaque affichage de fiche. En documentaire : les 2 tables LIGNE_* disparaissent (embarquées dans la fiche), VISITEUR et TYPE_FRAIS restent des références. 5 tables → 3 collections, par conception.
La grille qui prouve que tu couvres tout (réf. C2.3)
| Ce que le référentiel demande | Ce que tu montres |
|---|---|
| Concevoir le modèle de données | décisions embedding/référence + le MLD équivalent |
| Écrire des requêtes pour extraire/manipuler | les agrégations du chapitre 5 |
| Assurer l'intégrité et la cohérence | index unique, enum, required, validation $jsonSchema |
| Optimiser les performances | index {etat} prouvé par explain() (chapitre 5) |
Livrable 3
Ta fiche de défense « modélisation »
Rédige une demi-page « Choix de modélisation » pour ton dossier E6 : le besoin, la décision embedding/référence pour chaque entité (avec pourquoi), le MLD relationnel équivalent, et les garanties d'intégrité. C'est une pièce maîtresse de ton dossier.
Rends : ta demi-page.
Chapitre 1 ter — Manipuler les données à la main
Avant que l'API ne fasse les requêtes pour toi, il faut savoir les écrire toi-même. On se connecte à la base et on manipule directement, comme un administrateur. C'est exactement la sous-compétence « écrire des requêtes pour manipuler ou extraire des données » (C2.3) — et ça aide énormément à déboguer.
Se connecter à la base (Atlas)
Depuis l'interface Atlas, onglet Browse Collections, tu vois tes documents. Pour écrire des requêtes, l'onglet mongosh (ou l'app Compass) ouvre un terminal connecté à ta base.
use gsb_applifrais // choisir la base
show collections // visiteurs, fiches, typesFrais
Lire : find() et les filtres
db.fiches.find() // toutes les fiches
db.fiches.find({ etat: "CL" }) // seulement les clôturées
db.fiches.findOne({ mois: "2026-05" }) // la première qui correspond
Les opérateurs de comparaison
| Opérateur | Sens | Exemple |
|---|---|---|
$gt / $lt | supérieur / inférieur | { "lignesHorsForfait.montant": { $gt: 50 } } |
$in | dans une liste | { etat: { $in: ["CL", "VA"] } } |
$ne | différent de | { etat: { $ne: "CR" } } |
$exists | champ présent | { dateEmbauche: { $exists: true } } |
$regex | motif texte (comme LIKE) | { mois: { $regex: /^2026/ } } |
Combiner plusieurs conditions = un ET implicite ; pour un OU, $or :
db.fiches.find({ etat: "CR", mois: "2026-06" }) // CR ET juin
db.fiches.find({ $or: [ { etat: "CL" }, { etat: "VA" } ] }) // CL OU VA
Choisir les champs (projection) & trier
db.fiches.find({ etat: "CL" }, { mois: 1, etat: 1, _id: 0 }) // ne garder que mois+etat
db.fiches.find().sort({ mois: -1 }).limit(5) // 5 fiches, mois décroissant
db.fiches.countDocuments({ etat: "CR" }) // combien de fiches créées ?
Écrire : insérer, modifier, supprimer
// modifier l'état d'une fiche
db.fiches.updateOne(
{ _id: ObjectId("665f...") },
{ $set: { etat: "VA" } }
)
// ajouter une ligne dans le tableau embarqué
db.fiches.updateOne(
{ _id: ObjectId("665f...") },
{ $push: { lignesHorsForfait: { date: new Date(), libelle: "Taxi", montant: 18 } } }
)
L'anatomie d'un ObjectId (une question d'oral fréquente)
L'_id ressemble à 665fa4c2e3b8a2d91c000042 : 24 caractères hexadécimaux = 12 octets. Il n'est pas aléatoire : les 4 premiers octets sont un horodatage.
ObjectId("665fa4c2e3b8a2d91c000042").getTimestamp()
// → ISODate("2026-06-04T...") ← la date de création, gratuite !
Livrable 1 ter
Dix requêtes
Écris (et teste) 10 requêtes mongosh sur ta base : au moins une avec $gt, une avec $in, une projection, un tri, un countDocuments, un $set et un $push. Note le résultat de chacune.
Rends : tes 10 requêtes + leurs résultats. (Excellente pièce de portfolio « gestion des données ».)
2. L'API : comment le front parle au back
2.1 — C'est quoi une API, vraiment ?
Une API (Application Programming Interface), c'est un ensemble d'adresses (des URL, qu'on appelle routes ou endpoints) que le front peut appeler pour demander ou modifier des données. L'analogie : le menu d'un restaurant. Tu ne vas pas en cuisine ; tu commandes via le menu (des entrées bien définies), et on te sert. Le client (React) ne touche jamais la base : il passe par les « plats » offerts par l'API.
GET /api/v1/fiches → "donne-moi la liste des fiches"
POST /api/v1/fiches → "crée une nouvelle fiche"
GET /api/v1/fiches/123 → "donne-moi la fiche 123"
PUT /api/v1/fiches/123/cloturer → "clôture la fiche 123"
2.2 — REST : la grammaire des verbes
| Verbe | Intention | Exemple AppliFrais |
|---|---|---|
GET | lire (ne change rien) | lister les fiches, lire une fiche |
POST | créer | créer une fiche, ajouter une ligne |
PUT | modifier / remplacer | clôturer, valider une fiche |
DELETE | supprimer | supprimer une fiche |
2.3 — Les codes de réponse HTTP (le statut de chaque réponse)
| Code | Sens | Quand |
|---|---|---|
200 / 201 | OK / Créé | tout s'est bien passé |
400 | mauvaise requête | données invalides (ex. mois mal formé) |
401 | non authentifié | pas de jeton, ou jeton expiré |
403 | interdit | connecté, mais pas le droit (mauvais rôle) |
404 | introuvable | la fiche demandée n'existe pas |
409 | conflit | action impossible dans cet état (ex. valider une fiche déjà validée) |
2.4 — Pourquoi versionner l'API (/api/v1) ?
Livrable 4
Le bon verbe, le bon code
Pour ces 3 actions, donne le verbe HTTP, l'adresse, et le code de succès attendu : (a) ajouter un frais à une fiche, (b) consulter l'historique d'une fiche, (c) valider une fiche déjà validée (quel code d'erreur ?).
Rends : tes 3 réponses.
Chapitre 2 (suite) — Construire le squelette de l'API
On monte maintenant l'ossature Express : la connexion, l'application, le serveur, et la gestion d'erreurs. Tout le reste (auth, fiches) viendra se brancher dessus.
2.5 — La connexion — config/db.js
const mongoose = require("mongoose");
async function connectDB() {
await mongoose.connect(process.env.MONGODB_URI);
console.log("MongoDB connecté");
}
module.exports = connectDB;
2.6 — Deux utilitaires qui simplifient tout
// utils/AppError.js — une erreur qui porte son code HTTP
class AppError extends Error {
constructor(message, statusCode = 500) { super(message); this.statusCode = statusCode; }
}
module.exports = AppError;
// utils/asyncHandler.js — évite d'écrire try/catch partout
module.exports = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
2.7 — Le gestionnaire d'erreurs central — middlewares/errorHandler.js
const { ZodError } = require("zod");
module.exports = (err, req, res, next) => {
if (err instanceof ZodError)
return res.status(400).json({ error: "Données invalides",
champs: err.issues.map((e) => ({ champ: e.path.join("."), message: e.message })) });
if (err.code === 11000)
return res.status(409).json({ error: "Doublon : une fiche existe déjà pour ce mois" });
res.status(err.statusCode || 500).json({ error: err.message || "Erreur serveur" });
};
2.8 — L'application & le serveur — app.js + server.js
// app.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const app = express();
app.disable("x-powered-by"); // ne pas annoncer Express
app.use(helmet()); // en-têtes de sécurité
app.use(cors({ origin: process.env.CLIENT_URL })); // seul le front a le droit
app.use(express.json()); // lire le corps JSON
app.get("/api/health", (req, res) => res.json({ status: "ok" }));
app.use("/api/v1", require("./routes")); // toute l'API versionnée
app.use(require("./middlewares/errorHandler")); // dernier maillon
module.exports = app;
// server.js
require("dotenv").config();
const app = require("./app");
const connectDB = require("./config/db");
connectDB().then(() => app.listen(process.env.PORT || 3000,
() => console.log("API prête")));
Livrable 2 bis
L'API répond
Lance npm run dev, puis teste GET /api/health dans Postman. Capture les deux lignes vertes du terminal (« MongoDB connecté », « API prête ») et la réponse { "status": "ok" }.
Rends : tes 2 captures.
3. L'identité : t'authentifier sans danger
3.1 — Le problème : prouver qui tu es
Quand le visiteur Bunisset appelle « donne-moi mes fiches », comment l'API sait-elle que c'est bien lui, et pas quelqu'un qui se fait passer pour lui ? C'est le problème de l'authentification. Deux étapes : se connecter (prouver son identité une fois), puis rester reconnu sur les appels suivants.
3.2 — Pourquoi on ne stocke JAMAIS le mot de passe en clair
Si ta base stocke motdepasse: "gsb123" et qu'elle fuite un jour (ça arrive), tous les comptes sont compromis — et pire, les gens réutilisent leurs mots de passe ailleurs. La solution : stocker une empreinte (un hash) irréversible.
const hash = await bcrypt.hash("gsb123", 12);
// → "$2a$12$N9qo8uLOickgx2ZMRZoMy..." (impossible à "déchiffrer")
3.3 — Le jeton JWT : rester reconnu
Après la connexion, l'API te donne un jeton JWT (JSON Web Token) : une sorte de « bracelet de festival » que tu présentes à chaque appel. Il contient ton identité, et il est signé par le serveur.
eyJhbGci... . eyJzdWIiOiI2Nj... . 3x9fR2...
(en-tête) (données: sub, role) (signature)
3.4 — Ralentir les attaques : le rate limiting
Un pirate peut essayer des milliers de mots de passe sur la page de connexion (attaque « brute force »). On limite : 5 essais ratés par quart d'heure, puis on bloque (429 Too Many Requests).
Livrable 5
Explique à un non-informaticien
En 3-4 phrases simples (comme à un parent), explique : pourquoi on ne stocke pas les mots de passe en clair, et pourquoi un JWT signé n'a pas besoin d'être chiffré. Savoir vulgariser = savoir vraiment.
Rends : ton explication.
Chapitre 3 (suite) — La connexion, pas à pas
On a vu les concepts (hash, JWT). Voici le déroulé complet d'une connexion, puis d'un appel protégé — c'est ce que tu dois savoir raconter au jury.
3.5 — La séquence d'une connexion
① Valider le type (une chaîne, pas un objet). ② Comparer le mot de passe saisi au hash stocké. ③ Signer un JWT (id + rôle). ④ Le front range le jeton. Désormais, chaque appel protégé enverra ce jeton.
3.6 — Un appel protégé : le jeton fait foi
Sur GET /fiches, le middleware requireAuth s'exécute avant le contrôleur :
function requireAuth(req, res, next) {
const token = (req.headers.authorization || "").replace("Bearer ", "");
if (!token) return next(new AppError("Authentification requise", 401));
try {
const payload = jwt.verify(token, process.env.JWT_SECRET); // vérifie la signature
req.user = { id: payload.sub, role: payload.role }; // on "sait" qui appelle
next(); // → le contrôleur
} catch {
next(new AppError("Jeton invalide ou expiré", 401));
}
}
Livrable 5 bis
Falsifie un jeton
Récupère ton jeton, va sur jwt.io, change « visiteur » en « comptable » dans la partie données, et rappelle GET /auth/me dans Postman avec ce jeton modifié. Que se passe-t-il, et pourquoi ?
Rends : le code HTTP obtenu + l'explication (la signature).
Chapitre 3 (fin) — Le contrôleur d'authentification
On a vu la séquence et la chaîne de middleware. Voici le code qui connecte vraiment : le contrôleur login (celui qu'appelle AuthAPI.login côté front) et ses routes.
3.7 — controllers/auth.controller.js
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { z } = require("zod");
const Visiteur = require("../models/visiteur.model");
const AppError = require("../utils/AppError");
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
exports.login = async (req, res) => {
const { email, password } = loginSchema.parse(req.body); // ① valide -> bloque l'injection
const v = await Visiteur.findOne({ email }); // ② cherche par email
if (!v || !(await bcrypt.compare(password, v.motDePasseHash)))
throw new AppError("Identifiants invalides", 401); // ③ même message dans les 2 cas
const token = jwt.sign({ sub: v._id, role: v.role }, // ④ jeton signé
process.env.JWT_SECRET, { expiresIn: "8h" });
res.json({ token, visiteur: { id: v._id, nom: v.nom, prenom: v.prenom, role: v.role } });
};
exports.moi = async (req, res) => res.json(req.user); // qui suis-je ? (jeton -> req.user)
3.8 — routes/auth.routes.js
const router = require("express").Router();
const rateLimit = require("express-rate-limit");
const ctrl = require("../controllers/auth.controller");
const { requireAuth } = require("../middlewares/auth");
const limiteLogin = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }); // 5 essais / 15 min
router.post("/login", limiteLogin, ctrl.login); // public, mais ralenti
router.get("/me", requireAuth, ctrl.moi); // besoin d'un jeton valide
module.exports = router;
Livrable 4 ter
La connexion de bout en bout
Avec Postman : POST /api/v1/auth/login (Bunisset / gsb123) → copie le token reçu. Rappelle ensuite GET /api/v1/auth/me avec l'en-tête Authorization: Bearer <token>.
Rends : les 2 réponses (le token, puis ton profil).
4. Les droits : qui a le droit de faire quoi
4.1 — Le cycle de vie d'une fiche
Une fiche n'est pas figée : elle passe par des états, et chaque transition a son acteur. C'est un petit workflow.
Le visiteur clôture (CR→CL). Le comptable valide (CL→VA) puis met en paiement (VA→MP→RB). On « garde la porte » à chaque étape : on ne valide que ce qui est clôturé.
4.2 — Les trois défenses de chaque contrôleur
Regarde le contrôleur « clôturer ». Trois vérifications reviennent, et c'est volontaire :
exports.cloturer = asyncHandler(async (req, res) => {
assertId(req.params.id); // ① l'id a-t-il la bonne forme ?
const fiche = await Fiche.findById(req.params.id);
if (!fiche) throw new AppError("Fiche introuvable", 404);
if (String(fiche.visiteur) !== req.user.id) // ② est-ce bien SA fiche ?
throw new AppError("Accès refusé", 403);
if (fiche.etat !== "CR") // ③ l'état permet-il l'action ?
throw new AppError("Seule une fiche créée peut être clôturée", 409);
fiche.etat = "CL";
fiche.historiqueEtats.push({ etat: "CL", par: req.user.id });
await fiche.save();
res.json(fiche);
});
4.3 — Anti-IDOR : le trou de sécurité n°1 des projets étudiants
4.4 — La séparation des rôles, dans les routes
router.use(requireAuth); // tout /fiches exige d'être connecté
router.post("/", requireRole("visiteur"), ctrl.creer);
router.put("/:id/cloturer", requireRole("visiteur"), ctrl.cloturer);
router.put("/:id/valider", requireRole("comptable"), ctrl.valider); // ← réservé
Livrable 6
Prouve l'étanchéité
Avec Postman, connecte-toi en visiteur et tente : (a) valider une fiche, (b) lire la fiche d'un autre visiteur. Capture les deux réponses 403. Puis explique en une phrase quelle défense a bloqué chaque cas (rôle / propriété).
Rends : les 2 captures + tes 2 phrases.
Chapitre 4 (suite) — Les contrôleurs CRUD en entier
Voici le code complet du contrôleur des fiches. Chaque fonction suit le même motif de défenses (4.2). À lire comme un tout cohérent.
const mongoose = require("mongoose");
const { z } = require("zod");
const Fiche = require("../models/fiche.model");
const asyncHandler = require("../utils/asyncHandler");
const AppError = require("../utils/AppError");
function assertId(id) {
if (!mongoose.isValidObjectId(id)) throw new AppError("Identifiant invalide", 400);
}
// GET /fiches — visiteur : SES fiches ; comptable : toutes
exports.lister = asyncHandler(async (req, res) => {
const filtre = req.user.role === "comptable" ? {} : { visiteur: req.user.id };
const fiches = await Fiche.find(filtre).populate("visiteur", "nom prenom").sort({ mois: -1 });
res.json(fiches);
});
// GET /fiches/:id — contrôle de propriété (anti-IDOR)
exports.lire = asyncHandler(async (req, res) => {
assertId(req.params.id);
const fiche = await Fiche.findById(req.params.id).populate("visiteur", "nom prenom");
if (!fiche) throw new AppError("Fiche introuvable", 404);
if (req.user.role !== "comptable" && String(fiche.visiteur._id) !== req.user.id)
throw new AppError("Accès refusé", 403);
res.json(fiche);
});
// POST /fiches — le visiteur crée SA fiche du mois
exports.creer = asyncHandler(async (req, res) => {
const { mois } = z.object({ mois: z.string().regex(/^\d{4}-\d{2}$/) }).parse(req.body);
const fiche = await Fiche.create({ visiteur: req.user.id, mois, etat: "CR",
historiqueEtats: [{ etat: "CR", par: req.user.id }] });
res.status(201).json(fiche);
});
// POST /fiches/:id/lignes — ajouter un frais hors forfait
exports.ajouterLigne = asyncHandler(async (req, res) => {
assertId(req.params.id);
const ligne = z.object({ date: z.coerce.date(), libelle: z.string().min(1).max(100),
montant: z.coerce.number().positive(), justificatif: z.string().optional() }).parse(req.body);
const fiche = await Fiche.findById(req.params.id);
if (!fiche) throw new AppError("Fiche introuvable", 404);
if (String(fiche.visiteur) !== req.user.id) throw new AppError("Accès refusé", 403);
if (fiche.etat !== "CR") throw new AppError("Fiche non modifiable", 409);
fiche.lignesHorsForfait.push(ligne);
await fiche.save();
res.status(201).json(fiche);
});
4.5 — Les routes, où la sécurité se déclare
const router = require("express").Router();
const ctrl = require("../controllers/fiches.controller");
const { requireAuth, requireRole } = require("../middlewares/auth");
router.use(requireAuth); // tout exige un compte
router.get("/", ctrl.lister);
router.post("/", requireRole("visiteur"), ctrl.creer);
router.get("/:id", ctrl.lire);
router.post("/:id/lignes", requireRole("visiteur"), ctrl.ajouterLigne);
router.put("/:id/cloturer", requireRole("visiteur"), ctrl.cloturer);
router.put("/:id/valider", requireRole("comptable"), ctrl.valider);
module.exports = router;
Livrable 4 bis
Lis le code comme le jury
Choisis 2 contrôleurs et, pour chacun, numérote les vérifications (id / existence / propriété / état) et dis quel code HTTP chacune renvoie si elle échoue. Puis explique pourquoi valider ne vérifie pas la propriété.
Rends : ton analyse.
5. Les requêtes : croiser et totaliser (agrégation)
5.1 — Pourquoi l'agrégation ?
Lire une fiche, c'est facile. Mais « combien a-t-on remboursé par type de frais ce trimestre ? » — ça demande de parcourir toutes les fiches, déplier leurs lignes, les regrouper et les totaliser. C'est le rôle du pipeline d'agrégation : l'équivalent documentaire des JOIN + GROUP BY du SQL. Et c'est ici que vit la vraie puissance (et la vraie « complexité ») du NoSQL.
5.2 — Un pipeline se lit comme une chaîne de montage
Chaque étape reçoit des documents, les transforme, et passe le résultat à la suivante.
const data = await Fiche.aggregate([
{ $match: { etat: { $in: ["VA", "MP", "RB"] } } }, // ① ne garder que les fiches validées+
{ $unwind: "$lignesForfait" }, // ② "déplier" : 1 ligne → 1 document
{ $lookup: { // ③ JOINDRE la collection barème
from: "typesFrais", localField: "lignesForfait.codeType",
foreignField: "code", as: "type" } },
{ $unwind: "$type" },
{ $group: { // ④ REGROUPER et calculer
_id: "$type.libelle",
total: { $sum: { $multiply: ["$lignesForfait.quantite", "$type.montant"] } } } },
{ $sort: { total: -1 } }, // ⑤ trier du plus gros au plus petit
]);
Résultat (dans Postman, jeton comptable, GET /api/v1/stats/par-type) :
[
{ "_id": "Repas restaurant", "total": 200 },
{ "_id": "Frais kilométrique", "total": 186 },
{ "_id": "Forfait étape", "total": 110 }
]
5.3 — Prouver la performance : explain()
On a posé un index sur etat. On prouve qu'il sert :
db.fiches.find({ etat: "CL" }).explain("executionStats")
winningPlan: { stage: "IXSCAN", indexName: "etat_1" } ← index utilisé ✓
Livrable 7
Ton tableau de bord
Explique avec tes mots, étape par étape, ce que fait le pipeline par-type (une phrase par étape $match/$unwind/$lookup/$group/$sort). Puis propose une autre statistique utile au comptable et esquisse son pipeline.
Rends : ton explication + ta proposition.
Chapitre 6 (amont) — Créer le front avec Vite
Avant d'écrire un seul composant, on monte l'atelier du front. Côté back, tu avais npm init ; côté front, on utilise Vite, l'outil standard pour démarrer un projet React en 2026.
6.a — C'est quoi Vite ?
Vite est un outil de développement pour les applis front. Il fait trois choses :
6.b — Créer le projet front
# à la racine du dépôt (à côté de backend/)
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm install react-router-dom
6.c — Ce que Vite a créé (la structure du front)
frontend/
├── index.html ← LA page HTML unique (la "coquille")
├── package.json ← dépendances + scripts (dev / build / preview)
├── vite.config.js ← config de Vite
└── src/
├── main.jsx ← le point d'entrée : accroche React au HTML
├── App.jsx ← le 1er composant (on le remplacera)
└── components/ ← nos écrans (Login, Liste, Detail…) — à créer
6.d — Les scripts du front & le lancement
# dans frontend/, pendant le développement :
npm run dev # serveur Vite → http://localhost:5173 (HMR actif)
# pour la mise en ligne (chapitre D) :
npm run build # fabrique frontend/dist/ (fichiers statiques)
npm run preview # teste localement le build de prod
6.e — Comment le front trouve l'API
Le front doit connaître l'adresse de l'API. On ne l'écrit pas en dur : on la lit dans VITE_API_URL (config, chapitre M), ce qui donne API_URL dans api.js.
// frontend/vite.config.js — rien de spécial, juste le plugin React
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()] });
Chapitre 6 (amont) — React depuis zéro (1) : composant, JSX, props
Toute la BDD, tu l'as comprise depuis zéro. On fait pareil pour React : on ne recopie pas, on comprend le mécanisme. Trois idées suffisent — le composant, l'état, l'effet. On attaque la première.
6.f — Un composant, c'est une fonction qui renvoie de l'écran
function Bonjour() {
return <h1>Bonjour, visiteur GSB</h1>;
}
6.g — JSX : du HTML dans du JavaScript
function Total({ montant }) {
const classe = montant > 100 ? "gros" : "normal";
return (
<p className={classe}>
Total : {montant.toFixed(2)} €
</p>
);
}
6.h — Les props : les données qu'un parent passe à son enfant
// le parent décide, l'enfant affiche
function Liste() {
return (
<>
<Total montant={333.5} />
<Total montant={88} />
</>
);
}
6.i — Le re-rendu : pourquoi l'écran se redessine tout seul
Le cycle de React : donnée → ré-exécution de la fonction → nouvel écran.
Chapitre 6 (amont) — React depuis zéro (2) : useState
Les props viennent du parent et ne changent pas. Mais une fiche qu'on ouvre, un total qui bouge, un message d'erreur : ça, c'est de l'état — une donnée qui vit dans le composant et qui, quand elle change, redessine l'écran.
6.j — Déclarer un état avec useState
import { useState } from "react";
function Compteur() {
const [n, setN] = useState(0); // valeur initiale : 0
return (
<button onClick={() => setN(n + 1)}>
Cliqué {n} fois
</button>
);
}
6.k — La règle qui casse tout quand on l'ignore : on ne mute JAMAIS l'état
// ❌ FAUX — modifier directement : React ne voit rien, l'écran ne bouge pas
fiche.etat = "CL";
// ✅ JUSTE — passer une NOUVELLE valeur au setter
setFiche({ ...fiche, etat: "CL" });
6.l — Appliqué à GSB : l'état d'un écran de connexion
const [email, setEmail] = useState("");
const [erreur, setErreur] = useState(null);
const [enCours, setEnCours] = useState(false); // bouton désactivé pendant l'appel
Livrable 6 bis
Le total réactif
Écris un mini-composant Addition avec deux champs nombre en état et un <p> qui affiche leur somme. Vérifie que la somme se met à jour à chaque frappe, sans bouton.
Rends : ton code + une capture montrant la somme qui change.
Chapitre 6 (amont) — React depuis zéro (3) : useEffect
Le composant se dessine à partir de son état. Mais d'où viennent les fiches ? De l'API. Il faut donc, au bon moment, aller les chercher et les ranger dans l'état. Ce « au bon moment », c'est le rôle de useEffect — le hook que tu dois vraiment maîtriser.
6.m — Le problème que useEffect résout
Aller chercher des données, c'est un effet de bord : ça sort du composant (ça parle au réseau). Si on le faisait directement dans le corps de la fonction, ça se relancerait à chaque re-rendu → boucle infinie. useEffect dit à React : « exécute ce code après l'affichage, et seulement quand je te le dis ».
6.n — La forme, et la clé : le tableau de dépendances
import { useEffect, useState } from "react";
useEffect(() => {
// le code de l'effet
}, []); // ← LE TABLEAU DE DÉPENDANCES
Le tableau de dépendances : vide = une fois ; avec valeurs = à chaque changement de ces valeurs ; absent = à chaque rendu (presque toujours un bug).
6.o — Le pattern incontournable : charger au montage (loading / erreur / données)
function Liste() {
const [fiches, setFiches] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
FichesAPI.lister()
.then((data) => setFiches(data))
.catch((e) => setErreur(e.message))
.finally(() => setChargement(false));
}, []); // [] → au montage seulement
if (chargement) return <p>Chargement…</p>;
if (erreur) return <p className="err">{erreur}</p>;
if (fiches.length === 0) return <p>Aucune fiche pour l'instant.</p>;
return <ul>{/* …la liste… */}</ul>;
}
6.p — Le nettoyage (cleanup) : quand l'effet doit « ranger derrière lui »
useEffect(() => {
const id = setInterval(() => rafraichir(), 10000);
return () => clearInterval(id); // nettoyage au démontage
}, []);
Chapitre 6 (amont) — React depuis zéro (4) : formulaires, listes, lever l'état
Trois réflexes qui reviennent dans tous les écrans d'AppliFrais : les champs contrôlés (saisie), les listes avec clés (afficher N fiches), et « lever l'état » (le total qui bouge pendant la saisie).
6.q — Le champ contrôlé : l'état est la seule source de vérité
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="email" />
<input
type="password"
value={mdp}
onChange={(e) => setMdp(e.target.value)} />
6.r — Afficher une liste : .map() et la clé
{fiches.map((f) => (
<li key={f._id}>
{f.mois} — {f.etat} — {f.total.toFixed(2)} €
</li>
))}
6.s — Lever l'état (lifting state up) : le total qui bouge
Dans l'écran de saisie, plusieurs champs (quantités de frais) doivent alimenter un même total. La règle : l'état vit chez le parent commun, qui le descend en props et donne aux enfants de quoi le modifier.
function Saisie() {
const [lignes, setLignes] = useState([{ codeType: "KM", quantite: 0 }]);
const BAREME = { KM: 0.62, EF: 110, NUI: 80, REP: 25 }; // mêmes valeurs que le barème serveur
const total = lignes.reduce(
(s, l) => s + l.quantite * BAREME[l.codeType], 0); // recalculé à chaque rendu
function changer(i, quantite) {
setLignes(lignes.map((l, j) => (j === i ? { ...l, quantite } : l)));
}
return (
<>
{lignes.map((l, i) => (
<input key={i} type="number" value={l.quantite}
onChange={(e) => changer(i, Number(e.target.value))} />
))}
<p>Total estimé : {total.toFixed(2)} €</p>
</>
);
}
Livrable 6 ter
Le total qui suit la saisie
À partir du modèle ci-dessus, fais fonctionner la saisie avec le barème (KM 0,62 / EF 110 / NUI 80 / REP 25). Saisis 2 lignes et vérifie que le total change à chaque frappe.
Rends : ton code + une capture où le total affiché correspond au calcul à la main.
6. L'écran : React côté utilisateur
6.1 — Pourquoi React (et pas du HTML classique) ?
En HTML classique, chaque action recharge toute la page (lent, clignotant). React construit une SPA (Single Page Application) : la page ne se recharge jamais ; seules les parties qui changent sont redessinées. Résultat : une appli fluide, comme une application de bureau, dans le navigateur.
6.2 — Un seul point d'entrée réseau — services/api.js
On centralise tous les appels à l'API dans un fichier. Pourquoi ? Pour ajouter le jeton une seule fois, et gérer les erreurs au même endroit.
const API_URL = import.meta.env.VITE_API_URL; // ex. http://localhost:3000/api/v1
// --- session : jeton + profil, rangés dans le navigateur ---
export function getToken() { return localStorage.getItem("token"); }
export function getVisiteur() { try { return JSON.parse(localStorage.getItem("visiteur")); } catch { return null; } }
export function setSession(token, visiteur) {
localStorage.setItem("token", token);
localStorage.setItem("visiteur", JSON.stringify(visiteur));
}
export function logout() { localStorage.clear(); location.href = "/login"; }
// --- appel générique : colle le jeton, centralise les erreurs ---
async function http(path, options = {}) {
const token = getToken();
const res = await fetch(`${API_URL}${path}`, {
headers: { "Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}) },
...options,
});
if (res.status === 401) { logout(); throw new Error("Session expirée"); }
if (!res.ok) { const b = await res.json().catch(() => ({})); throw new Error(b.error); }
return res.json();
}
// --- actions métier (chaque fonction = une route de l'API) ---
export const AuthAPI = {
login: (email, password) => http("/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }),
};
export const FichesAPI = {
lister: () => http("/fiches"),
lire: (id) => http(`/fiches/${id}`),
creer: (mois) => http("/fiches", { method: "POST", body: JSON.stringify({ mois }) }),
ajouterLigne: (id, ligne) => http(`/fiches/${id}/lignes`, { method: "POST", body: JSON.stringify(ligne) }),
cloturer: (id) => http(`/fiches/${id}/cloturer`, { method: "PUT" }),
valider: (id) => http(`/fiches/${id}/valider`, { method: "PUT" }),
};
6.3 — L'affichage dépend du rôle… mais ce n'est pas de la sécurité
{fiche.etat === "CR" && role === "visiteur" &&
<button onClick={() => cloturer(fiche._id)}>Clôturer</button>}
{fiche.etat === "CL" && role === "comptable" &&
<button onClick={() => valider(fiche._id)}>Valider</button>}
Livrable 8
Le trajet d'un clic
Décris ce qui se passe, étape par étape, quand le comptable clique « Valider » : du clic React jusqu'à la base et retour (quel appel, quelle route, quels contrôles côté serveur, quelle mise à jour de l'écran).
Rends : ta description (5-6 étapes).
Chapitre 6 (suite) — Construire les écrans, un par un
On a les concepts (6.1-6.3) et les maquettes (chapitre É). On assemble maintenant le front, écran par écran. Chaque écran = un composant React qui appelle des fonctions de api.js. Voici d'abord l'arbre des composants : qui contient quoi.
6.4 — Le routeur & la garde d'accès — main.jsx
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { getToken } from "./services/api";
// garde : pas de jeton → on renvoie au login
function Privee({ children }) {
return getToken() ? children : <Navigate to="/login" replace />;
}
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<Privee><Layout /></Privee>}>
<Route path="/" element={<Liste />} />
<Route path="/fiches/:id" element={<Detail />} />
</Route>
</Routes>
</BrowserRouter>
);
6.4 bis — La coquille privée — Layout.jsx
Dans le routeur, les écrans privés sont enveloppés par Layout : une barre commune (nom de l'appli, qui est connecté, bouton Déconnexion) + l'emplacement où s'affiche l'écran courant.
import { Outlet, Link } from "react-router-dom";
import { getVisiteur, logout } from "./services/api";
function Layout() {
const v = getVisiteur();
return (
<>
<header className="barre">
<Link to="/">GSB AppliFrais</Link>
<span>{v?.prenom} {v?.nom} — {v?.role}</span>
<button onClick={logout}>Déconnexion</button>
</header>
<main><Outlet /></main>
</>
);
}
6.5 — Construire l'écran 1 : la connexion (Login.jsx)
Objectif (maquette écran 1) : deux champs, un bouton, et au clic on appelle l'API puis on redirige. On construit par étapes.
Étape 1 — l'état du formulaire. Un composant a besoin de « se souvenir » de ce que l'utilisateur tape : c'est le rôle de useState.
const [email, setEmail] = useState("bunisset@gsb.fr");
const [password, setPassword] = useState("gsb123");
const [erreur, setErreur] = useState(null);
Étape 2 — la soumission.
async function soumettre(e) {
e.preventDefault(); // empêche le rechargement de page par défaut
setErreur(null);
try {
const { token, visiteur } = await AuthAPI.login(email, password);
setSession(token, visiteur); // range le jeton dans le navigateur
nav("/"); // redirige vers « Mes fiches »
} catch (err) {
setErreur(err.message); // ex. « Identifiants invalides » (401)
}
}
Étape 3 — l'affichage (le JSX, ce que React dessine).
return (
<form className="login" onSubmit={soumettre}>
<h1>GSB AppliFrais</h1>
<label>Email
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</label>
<label>Mot de passe
<input type="password" value={password}
onChange={(e) => setPassword(e.target.value)} />
</label>
{erreur && <p className="erreur">{erreur}</p>}
<button>Se connecter</button>
</form>
);
Livrable 6a
Le cas d'erreur
Connecte-toi avec un mauvais mot de passe. Capture l'écran avec le message rouge. Puis explique le trajet complet de l'erreur : de l'API (quel code HTTP ?) jusqu'au setErreur qui l'affiche.
Rends : la capture + ton explication.
Chapitre 6 (suite) — Les fiches à l'écran
6.6 — Construire l'écran 2 : la liste (Liste.jsx)
Objectif (maquette écran 2) : afficher les fiches dans un tableau, et un formulaire « nouvelle fiche ». La grande nouveauté : aller chercher des données au chargement.
Étape 1 — charger les fiches avec useEffect.
const [fiches, setFiches] = useState([]);
const [erreur, setErreur] = useState(null);
function charger() {
FichesAPI.lister().then(setFiches).catch((e) => setErreur(e.message));
}
useEffect(() => { charger(); }, []); // [] = « une seule fois, au montage »
Étape 2 — créer une fiche.
const [mois, setMois] = useState("");
async function creer(e) {
e.preventDefault();
try { await FichesAPI.creer(mois); setMois(""); charger(); } // recharge la liste
catch (err) { setErreur(err.message); } // ex. 409 doublon du mois
}
Étape 3 — l'affichage, piloté par le rôle.
const role = getVisiteur()?.role;
const ETAT = { CR:"Créée", CL:"Clôturée", VA:"Validée", MP:"Mise en paiement", RB:"Remboursée" };
return (
<div>
<h1>{role === "comptable" ? "Toutes les fiches" : "Mes fiches"}</h1>
{erreur && <div className="banniere-erreur">{erreur}</div>}
{role === "visiteur" && (
<form onSubmit={creer}>
<input placeholder="2026-06" value={mois} onChange={(e) => setMois(e.target.value)} />
<button>Nouvelle fiche</button>
</form>
)}
<table>
<thead><tr><th>Mois</th>{role === "comptable" && <th>Visiteur</th>}<th>État</th><th>Total</th><th></th></tr></thead>
<tbody>
{fiches.map((f) => (
<tr key={f._id}>
<td>{f.mois}</td>
{role === "comptable" && <td>{f.visiteur?.prenom} {f.visiteur?.nom}</td>}
<td>{ETAT[f.etat]}</td>
<td>{f.total?.toFixed(2)} €</td>
<td><Link to={`/fiches/${f._id}`}>Ouvrir</Link></td>
</tr>
))}
</tbody>
</table>
</div>
);
Livrable 6b
Le même composant, deux rôles
Connecte-toi en visiteur, capture « Mes fiches ». Déconnecte-toi, connecte-toi en comptable (Lacombe), capture « Toutes les fiches ». Entoure les 3 différences visuelles et explique quelle ligne de code produit chacune.
Rends : les 2 captures annotées.
Chapitre 6 (suite) — Le détail, la saisie & les actions
6.7 — Construire l'écran 3 : le détail (Detail.jsx)
Objectif (maquette écran 3) : afficher une fiche, ses lignes, et les boutons d'action selon l'état et le rôle.
const { id } = useParams(); // l'id vient de l'URL /fiches/:id
const [fiche, setFiche] = useState(null);
const role = getVisiteur()?.role;
function charger() { FichesAPI.lire(id).then(setFiche).catch(() => {}); }
useEffect(() => { charger(); }, [id]); // recharge si l'id change
async function action(promesse) { // factorise clôturer/valider
try { const maj = await promesse(); setFiche(maj); }
catch (err) { alert(err.message); } // ex. 409 si mauvais état
}
if (!fiche) return <p>Chargement…</p>; // tant que l'API n'a pas répondu
return (
<div>
<h1>Fiche {fiche.mois} — {fiche.visiteur?.prenom} {fiche.visiteur?.nom}</h1>
<p>État : <b>{fiche.etat}</b> · Total : <b>{fiche.total?.toFixed(2)} €</b></p>
<h2>Frais forfaitisés</h2>
<ul>{fiche.lignesForfait.map((l, i) => <li key={i}>{l.codeType} × {l.quantite}</li>)}</ul>
<h2>Frais hors forfait</h2>
<ul>{fiche.lignesHorsForfait.map((l) =>
<li key={l._id}>{new Date(l.date).toLocaleDateString()} — {l.libelle} — {l.montant} €</li>)}
</ul>
{fiche.etat === "CR" && role === "visiteur" &&
<button onClick={() => action(() => FichesAPI.cloturer(fiche._id))}>Clôturer</button>}
{fiche.etat === "CL" && role === "comptable" &&
<button onClick={() => action(() => FichesAPI.valider(fiche._id))}>Valider</button>}
</div>
);
6.8 — Le formulaire de saisie & le total qui bouge
Pour ajouter un frais hors forfait (maquette écran 3, point 2), un petit formulaire local :
const [ligne, setLigne] = useState({ date: "", libelle: "", montant: "" });
async function ajouter(e) {
e.preventDefault();
const maj = await FichesAPI.ajouterLigne(id, ligne); // POST /fiches/:id/lignes
setFiche(maj); // le total se met à jour tout seul
setLigne({ date: "", libelle: "", montant: "" }); // on vide le formulaire
}
6.9 — Bien gérer les erreurs cross-stack (l'expérience utilisateur)
Quand l'API renvoie un 409 (« fiche déjà clôturée ») ou un 403, il faut un message clair, pas un plantage. Grâce au http() centralisé (6.2) qui transforme chaque erreur en message, il suffit d'afficher err.message :
catch (err) {
if (err.message.includes("409")) setInfo("Action impossible dans cet état.");
else setInfo(err.message);
}
Livrable 6c
Le parcours complet à l'écran
Filme (ou capture étape par étape) le parcours : connexion → créer une fiche → ajouter un frais (le total change) → clôturer → se reconnecter en comptable → valider. C'est ta démo E6 : entraîne-toi à la commenter en nommant, à chaque clic, la route appelée et le contrôle côté serveur.
Rends : ta séquence de captures + le script de ta démo.
7. Le reçu : OCR, stockage S3 & RGPD
7.1 — L'idée : la machine aide, l'humain décide
Le visiteur photographie un reçu. L'appli le stocke, en lit le montant et la date (OCR), et lui propose un frais pré-rempli. Il vérifie, corrige si besoin, et confirme. Deux routes : une qui propose (sans rien enregistrer), une qui enregistre (après confirmation).
7.2 — C'est quoi l'OCR ?
OCR = Optical Character Recognition : transformer une image de texte (la photo d'un ticket) en texte exploitable. On utilise tesseract.js, qui lit l'image et renvoie le texte + un indice de confiance (0-100). On en extrait le montant (le plus gros « xx,xx » du ticket) et la date par de petites expressions régulières.
7.3 — Pourquoi stocker les fichiers dans S3, et pas dans MongoDB ?
| Mettre l'image dans MongoDB | Mettre l'image dans S3 |
|---|---|
| gonfle la base, ralentit les lectures | la base reste légère (elle ne garde qu'une clé) |
| limite des 16 Mo par document | fichiers de n'importe quelle taille |
| coûteux à sauvegarder | stockage objet pensé pour ça, pas cher |
7.4 — Garder les reçus privés : les liens signés
Un reçu est une donnée personnelle. Le bucket S3 est donc privé : aucun fichier n'est accessible publiquement. Pour qu'un visiteur (ou le comptable) voie un reçu, on génère à la demande un lien signé temporaire (valable 5 min) :
const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: cle }),
{ expiresIn: 300 });
// → https://...?X-Amz-Signature=... (cliquable pendant 5 minutes, puis mort)
Livrable 9
Données ou fichiers ?
Pour chacun, dis où tu le stockes (MongoDB ou S3) et pourquoi : (a) le montant d'un frais, (b) la photo du reçu, (c) l'email du visiteur, (d) un export PDF de 4 Mo. Puis explique en une phrase ce qu'apporte un lien signé pour le RGPD.
Rends : tes réponses.
Chapitre 7 (suite) — Construire le stockage S3 & l'OCR
On relie le bucket (créé au chapitre M) à l'API : un client S3, deux fonctions (déposer, relire), puis la route d'analyse OCR.
7.5 — Le client & le service — config/s3.js + services/stockage.js
// config/s3.js
const { S3Client } = require("@aws-sdk/client-s3");
const s3 = new S3Client({
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT || undefined, // vide = AWS ; rempli = R2/MinIO
credentials: { accessKeyId: process.env.S3_KEY, secretAccessKey: process.env.S3_SECRET },
forcePathStyle: !!process.env.S3_ENDPOINT,
});
module.exports = s3;
// services/stockage.js
const crypto = require("crypto");
const { PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const s3 = require("../config/s3");
async function televerser(buffer, contentType) { // dépose, renvoie la CLÉ
const cle = `justificatifs/${crypto.randomUUID()}.jpg`;
await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: cle,
Body: buffer, ContentType: contentType }));
return cle;
}
async function lienTemporaire(cle) { // lien signé 5 min
return getSignedUrl(s3, new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: cle }),
{ expiresIn: 300 });
}
module.exports = { televerser, lienTemporaire };
7.6 — La route d'analyse — controllers/ocr.controller.js
const Tesseract = require("tesseract.js");
const Fiche = require("../models/fiche.model");
const { televerser } = require("../services/stockage");
const asyncHandler = require("../utils/asyncHandler");
const AppError = require("../utils/AppError");
function extraireMontant(t) {
const m = t.match(/\d+[.,]\d{2}/g);
return m ? Math.max(...m.map((s) => parseFloat(s.replace(",", ".")))) : null;
}
// POST /fiches/:id/analyse — stocke la photo sur S3, PROPOSE un frais (n'enregistre rien)
exports.analyser = asyncHandler(async (req, res) => {
if (!req.file) throw new AppError("Aucune image reçue", 400);
const fiche = await Fiche.findById(req.params.id);
if (!fiche || String(fiche.visiteur) !== req.user.id) throw new AppError("Accès refusé", 403);
const cle = await televerser(req.file.buffer, req.file.mimetype); // 1. sur S3
const { data } = await Tesseract.recognize(req.file.buffer, "fra"); // 2. OCR
res.json({ proposition: { montant: extraireMontant(data.text), // 3. proposer
justificatif: cle }, confiance: Math.round(data.confidence) });
});
7.7 — Revoir un reçu : le lien signé
// GET /fiches/:id/justificatif?cle=... → { url } (lien S3 temporaire)
exports.justificatif = asyncHandler(async (req, res) => {
const fiche = await Fiche.findById(req.params.id);
if (!fiche) throw new AppError("Fiche introuvable", 404);
const aLeDroit = String(fiche.visiteur) === req.user.id || req.user.role === "comptable";
if (!aLeDroit) throw new AppError("Accès refusé", 403);
if (!fiche.lignesHorsForfait.some((l) => l.justificatif === req.query.cle))
throw new AppError("Justificatif introuvable", 404); // la clé doit être SUR cette fiche
res.json({ url: await lienTemporaire(req.query.cle) });
});
8. La sécurité : on attaque notre propre appli
Bonne nouvelle : on a sécurisé en construisant, pas après coup. On relie maintenant chaque défense au référentiel OWASP (la liste des 10 risques web les plus courants), et on prouve que ça tient en s'attaquant soi-même.
8.1 — La carte des défenses
| Risque OWASP | Notre défense | Vu au chapitre |
|---|---|---|
| A01 — Contrôle d'accès cassé | rôles + contrôle de propriété (anti-IDOR) | 4 |
| A02 — Défauts cryptographiques | mot de passe en bcrypt (coût 12) | 3 |
| A03 — Injection | validation du type des entrées (Zod) | ci-dessous |
| A05 — Mauvaise configuration | helmet, CORS liste blanche, X-Powered-By coupé | ci-dessous |
| A07 — Authentification faible | JWT signé + expiration + rate limiting | 3 |
8.2 — L'injection NoSQL (A03) : comprendre l'attaque
Sur un login naïf, on cherche User.findOne({ email: req.body.email, password: req.body.password }). Un pirate envoie, au lieu d'un email, l'objet {"$gt": ""} (« supérieur à la chaîne vide ») :
{ "email": { "$gt": "" }, "password": { "$gt": "" } }
8.3 — helmet & CORS (A05)
8.4 — Le discours de clôture sur la sécurité
Livrable 10
Ton mini-rapport de sécurité
Pour 3 risques OWASP (au choix parmi A01, A02, A03, A05, A07), écris : le risque en une phrase, comment tu l'exploiterais, et comment ton appli le bloque (avec le code/la route concernée).
Rends : ton tableau de 3 lignes. (Base d'un excellent document portfolio E5.)
Chapitre 8 (suite) — Attaquer pour mieux défendre
La meilleure preuve qu'une appli est sûre, c'est de l'attaquer soi-même et de montrer que ça tient. Voici 4 attaques, à rejouer dans Postman, avec le résultat attendu. C'est la base d'un excellent mini-rapport de sécurité (portfolio E5).
8.5 — Le tableau des attaques (à rejouer)
| Attaque | Ce que tu envoies | Résultat | Défense |
|---|---|---|---|
| Injection NoSQL (A03) | login avec {"email":{"$gt":""}} | 400 | validation Zod du type |
| Rôle (A01) | visiteur appelle PUT /:id/valider | 403 | requireRole("comptable") |
| IDOR (A01) | visiteur lit la fiche d'un autre | 403 | contrôle de propriété |
| Brute force (A07) | 6 logins faux d'affilée | 429 | rate limiting |
8.6 — Les outils des pros (culture)
Au-delà des tests manuels, les professionnels utilisent des outils d'audit : OWASP ZAP (scanne une appli web et liste les failles), Semgrep (analyse ton code source à la recherche de motifs dangereux). Tu n'es pas obligé de les maîtriser pour l'AP2, mais savoir qu'ils existent et à quoi ils servent montre ta culture sécurité.
Livrable 10 bis
Ton rapport d'audit
Rejoue les 4 attaques du tableau dans Postman, capture chaque réponse. Rédige un mini-rapport d'une page : pour chaque faille, le risque, la preuve (ta capture), la défense, et le code/route concerné. C'est une pièce maîtresse de portfolio E5.
Rends : ton rapport + les 4 captures.
Chapitre L — Lancer, livrer & défendre (dossier E6)
Ton code marche : maintenant il faut le lancer proprement, le documenter, et préparer ton oral. Cette partie fait souvent la différence de note.
L.1 — Tout démarrer d'une commande
Deux serveurs à lancer (API + front). Pour n'en taper qu'un, à la racine :
npm install -D concurrently
// package.json racine :
"scripts": {
"dev:api": "npm --prefix backend run dev",
"dev:front": "npm --prefix frontend run dev",
"dev": "concurrently -n api,web -c green,magenta \"npm:dev:api\" \"npm:dev:front\""
}
npm run dev // API (3000) + front (5173) ensemble ; la base est déjà dans le cloud
L.2 — Le README (la première chose que lit un relecteur)
# GSB AppliFrais
Gestion des frais des visiteurs médicaux. React · Express · MongoDB Atlas · S3.
## Lancer
1. cp backend/.env.example backend/.env (URI Atlas + clés S3)
2. npm --prefix backend install && npm --prefix frontend install
3. npm --prefix backend run seed (jeu de test)
4. npm run dev → http://localhost:5173
Comptes : bunisset@gsb.fr / gsb123 (visiteur) · lacombe@gsb.fr / gsb123 (comptable)
## Architecture
React (5173) → API Express /api/v1 (3000) → MongoDB Atlas + bucket S3
L.3 — Ce que le jury regarde (et comment t'y préparer)
| Le jury demande… | Ta réponse s'appuie sur… |
|---|---|
| « Pourquoi ce modèle de données ? » | chapitre 1 : embedding/référence justifiés + MLD équivalent |
| « Comment gères-tu la sécurité ? » | chapitres 3, 4, 8 : bcrypt, JWT, rôles, anti-IDOR, validation |
| « Et les données personnelles (RGPD) ? » | chapitre 7 : bucket privé + liens signés, hébergement UE |
| « Montre une requête complexe. » | chapitre 5 : agrégation $lookup + explain() |
| « Fais une démo. » | le parcours du Livrable 6c, commandé en nommant les routes |
L.4 — Les preuves à rassembler (portfolio E5 & dossier E6)
Tes Livrables (0 à 10) tout au long de ce guide forment déjà l'ossature de ton dossier : la justification de modélisation (L1-L3), les captures de parcours (L6c), le mini-rapport de sécurité (L10/L10bis), les requêtes mongosh (L1ter). Rassemble-les, légende-les, relie chacun à une compétence du référentiel.
Livrable L
Ta fiche d'oral
Prépare une page recto : à gauche, 5 questions probables du jury ; à droite, ta réponse en 3 points chacune, avec la preuve à montrer (quel écran, quel bout de code). Entraîne-toi à la dérouler en 5 minutes.
Rends : ta fiche d'oral.
Chapitre L (suite) — Versionner proprement avec Git
Un projet E6 qui n'est pas sur Git, ça n'existe pas pour le jury. Git garde l'historique de ton travail, protège contre la perte, et prouve que tu sais travailler en équipe (C1.4, C2.2).
L.5 — Démarrer le dépôt
cd gsb-applifrais
git init
git add .
git commit -m "Base du projet : API + front + modèles"
L.6 — Le .gitignore (avant le premier add !)
# .gitignore — à créer AVANT git add .
node_modules/
.env
frontend/dist/
*.log
.DS_Store
L.7 — Des commits lisibles & des branches
git switch -c feature/ocr # une branche par fonctionnalité
# …travail…
git add . && git commit -m "feat: pré-remplissage OCR du formulaire"
git switch main && git merge feature/ocr
Livrable 12 bis
Ton historique raconte le projet
Fais au moins 5 commits au fil de ta construction, avec des messages d'intention clairs, sur au moins une branche de fonctionnalité.
Rends : une capture de git log --oneline (ou du graphe de commits).
Chapitre D — Mettre l'appli en ligne (déploiement)
L'E6 se défend en local, mais savoir mettre en ligne est un vrai plus (C1.3, C1.5) et une belle preuve de portfolio E5. Voici le trajet, sans se noyer.
D.1 — Qui héberge quoi ?
D.2 — Déployer l'API (ex. Render)
D.3 — Déployer le front (ex. Netlify)
# réglages de build sur l'hébergeur statique :
# base directory : frontend
# build command : npm run build
# publish directory: frontend/dist
# variable d'env : VITE_API_URL = https://ton-api.onrender.com/api/v1
Livrable 13
Ton appli, en ligne (défi type E)
Déploie au moins l'API + la base, idéalement aussi le front. Connecte-toi avec un compte de test depuis l'URL publique.
Rends : l'URL publique + une capture d'une connexion réussie depuis Internet. Note un risque de sécurité que le passage en ligne introduit (indice : 0.0.0.0/0, HTTPS, secrets).
Pour finir
Ce que tu sais faire maintenant
| Domaine | Tu sais… |
|---|---|
| Modéliser | choisir embarquer/référencer et le justifier, poser index & validation |
| Défendre ton modèle | répondre à « trop simple » avec le MLD équivalent et les preuves C2.3 |
| Concevoir une API | REST, verbes, codes HTTP, versionnement, idempotence |
| Sécuriser l'accès | bcrypt, JWT, rôles, anti-IDOR, rate limiting — et expliquer pourquoi |
| Exploiter les données | agrégation ($lookup, $group), prouver un index avec explain() |
| Construire le front | composants, état, appels API centralisés, UX selon le rôle |
| Gérer les fichiers | stockage S3, liens signés, et l'argument RGPD |
Mini-quiz d'auto-vérification
- En NoSQL, quelle est la question n°1 pour décider d'embarquer ou référencer ?
- Pourquoi le visiteur est-il référencé et pas embarqué dans la fiche ?
- Un JWT est-il chiffré ou signé ? Quelle conséquence pratique ?
- Différence entre un code
401et un code403? - Pourquoi la sécurité des rôles doit-elle être côté serveur et pas côté React ?
- À quoi sert
$lookupdans un pipeline d'agrégation ? - Pourquoi stocke-t-on la photo du reçu dans S3 et pas dans MongoDB ?
- Qu'est-ce qu'une injection NoSQL, et comment Zod la bloque-t-elle ?
Corrigé du quiz
| Q. | Réponse |
|---|---|
| 1 | « Qu'est-ce que je lis toujours ensemble ? » On embarque ce qui est lu avec le parent ; on référence le reste. |
| 2 | Il est partagé (plusieurs fiches) et mutable (il change). Le recopier créerait des incohérences ; on garde un lien qu'on rapproche avec populate(). |
| 3 | Signé, pas chiffré : tout le monde peut le lire, personne ne peut le falsifier. Donc on n'y met jamais de secret, et la clé serveur doit rester secrète. |
| 4 | 401 = « je ne sais pas qui tu es » (non authentifié). 403 = « je sais qui tu es mais tu n'as pas le droit » (non autorisé). |
| 5 | Le front est contournable (Postman, DevTools) : cacher un bouton ne protège rien. Seul le serveur refuse réellement (403). |
| 6 | À faire une jointure avec une autre collection (ici typesFrais) — l'équivalent du JOIN SQL. |
| 7 | Pour ne pas gonfler la base (limite 16 Mo, lectures lentes). La base ne garde qu'une clé ; le fichier vit dans le stockage objet. |
| 8 | Envoyer un objet Mongo (ex. {"$gt":""}) au lieu d'une valeur pour détourner la requête. Zod exige une chaîne → l'objet est rejeté (400) avant la base. |
Tu es bloqué ?
Glossaire
| API | ensemble d'adresses (routes) que le front appelle pour lire/écrire des données. |
| Agrégation | pipeline d'étapes ($match, $unwind, $lookup, $group…) pour croiser et totaliser des données. |
| bcrypt | fonction de hachage des mots de passe, lente par conception (paramètre « coût »). |
| Bucket | « dossier » de stockage objet (S3) où l'on dépose des fichiers sous une clé. |
| CORS | mécanisme qui autorise (ou non) une origine à appeler une API depuis le navigateur. |
| DCP | donnée à caractère personnel (RGPD) — ex. un reçu, un email. |
| Embedding / référence | embarquer = ranger les données dans le document ; référencer = garder un lien (_id). |
| enum | liste fermée de valeurs autorisées pour un champ (ex. les états CR/CL/VA…). |
| IDOR | faille où l'on accède à la ressource d'autrui en changeant juste l'id dans l'URL. |
| Index | structure qui accélère les recherches sur un champ (IXSCAN vs COLLSCAN). |
| Idempotent | action qu'on peut refaire sans changer le résultat (GET, PUT) — pas POST. |
| JWT | jeton signé (pas chiffré) portant l'identité et le rôle, présenté à chaque appel. |
| $lookup | étape d'agrégation qui joint une autre collection (le JOIN du NoSQL). |
| OCR | reconnaissance de texte dans une image (lire le montant d'un ticket photographié). |
| OWASP | référentiel des 10 risques de sécurité web les plus courants. |
| Lien signé | URL temporaire donnant accès à un fichier privé S3 pour une courte durée. |
| SPA | Single Page Application : l'appli ne recharge jamais la page entière (React). |
| Virtual | champ calculé à la lecture (ex. le total), jamais stocké, donc toujours juste. |
| Zod | bibliothèque de validation : vérifie type et forme des entrées côté serveur. |
Compétences mobilisées (référentiel BTS SIO SLAM)
Bloc 2 · Conception et développement d'applications
→ Développer des applications en respectant les standards de programmation
→ Intégrer et tester les fonctionnalités avec des bases de données
→ Écrire des requêtes pour manipuler ou extraire des données
Bloc 3 · Cybersécurité des services informatiques
→ Prendre en compte la sécurité dans un projet de développement d'une solution applicative
→ Prévenir les attaques et analyser les incidents de sécurité