Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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

ChapitreCe que tu comprends & construisLa grande question
0 · Le décorle besoin GSB, l'architecture 3 tiers, pourquoi le cloud« à quoi sert chaque morceau ? »
1 · Les donnéesMongoDB, modéliser, embarquer vs référencer, index, validation« pourquoi ce modèle, et pas un autre ? »
2 · L'APIExpress, 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 droitsCRUD, rôles, anti-IDOR, cycle de vie« qui a le droit de faire quoi ? »
5 · Les requêtesl'agrégation, les tableaux de bord« comment je croise et je totalise ? »
6 · L'écranReact, composants, état, appels à l'API« comment l'utilisateur voit tout ça ? »
7 · Le reçuOCR, 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

Schéma : GSB AppliFrais · Email · bunisset@gsb.fr · Mot de passe · •••••• · Se connecter · 1

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)

Schéma : GSB AppliFrais · Denis Bunisset · visiteur · Déco. · Mes fiches · 2026-06 · Nouvelle fiche · 1 · Mois · État · Total · Action · 2026-06 · Créée · 74,30 € · Ouvrir · 2026-05 · Validée · 333,50 € · Ouvrir · 2 · Le visiteur ne voit QUE ses fiches (l'API filtre côté serveur).

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)

Schéma : GSB AppliFrais · Fiche 2026-06 — Denis Bunisset · État : Créée · Total : 74,30 € · Frais forfaitisés · Frais kilométrique ( 100 ) km · Repas ( 0 ) · Nuitée / Forfait étape ( 0 ) · Enregistrer · 1 · Frais hors forfait · 03/06 · Péage A7 · 12,30 € · libellé · montant · date · + Ajouter ·…

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)

Schéma : GSB AppliFrais · Sylvie Lacombe · comptable · Toutes les fiches · Mois · Visiteur · État · Total · Action · 2026-05 · Denis Bunisset · Clôturée · 333,50 € · Valider · 1 · 2026-05 · Sylvain Cottet · Créée · 160,00 € · Le comptable voit TOUTES les fiches (colonne Visiteur en plus), et…

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)

Schéma : la photo du reçu · RESTAURANT · 12/06/2026 · TOTAL 25,00 · → · Proposition (à vérifier) · Montant : 25,00 € ✓ lecture fiable · Date : 12/06/2026 · Libellé : ( à compléter ) ⚠ à vérifier · Confirmer le frais · 1 · 2

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

ÉcranRoute(s) APIConstruit au…
1 · ConnexionPOST /auth/loginchap. 6 (+ auth chap. 3)
2 · Mes fichesGET /fiches, POST /ficheschap. 6 (+ CRUD chap. 4)
3 · Détail & saisieGET /fiches/:id, /forfait, /lignes, /cloturerchap. 6 (+ chap. 4)
4 · Toutes les fichesGET /fiches, /validerchap. 6 (+ rôles chap. 4)
5 · Scanner un reçu/analyse puis /ligneschap. 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 sur 26 dans cette étape

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

ActeurCe qu'il faitCe qu'il ne fait JAMAIS
Visiteur médicalsaisit ses frais, clôture sa fiche, consulte son historiquevalider ou payer une fiche
Comptableconsulte les fiches clôturées, valide, met en paiementsaisir 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).

Schéma : React (la salle) · affiche, capte les clics · navigateur · :5173 · Express (la cuisine) · applique les règles · serveur · :3000 · MongoDB (le stock) · stocke durablement · Atlas · cloud · demande · réponse JSON · requête · données · Chaque couche ignore comment les autres sont faites :…

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 machineDans le cloud (Atlas / S3)
chaque coéquipier a SA base, différentetoute l'équipe pointe vers la même base
si ton PC est éteint, plus rien ne marcheaccessible 24/7, de n'importe où
sauvegarde à faire soi-mêmesauvegardes et réplication gérées
« ça marche chez moi » le jour du jurydé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.

0 sur 26 dans cette étape

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 :

ÉtapeCe que tu faisPourquoi
Compte + clusteroffre M0 (gratuite), région Paris512 Mo, suffisant pour l'AP2 ; données en Europe (RGPD)
Database Accessun utilisateur gsb + mot de passec'est le compte que l'API utilisera
Network Accessautoriser ton IP (0.0.0.0/0 en salle)sinon la connexion part dans le vide (timeout)
Connect → Driverscopier la chaîne de connexionc'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

powershell
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ùVariableRôleSecret ?
backMONGODB_URIconnexion au cluster Atlasoui
backJWT_SECRETsigne/valide les jetonsoui
backPORTport de l'API (3000)non
backCLIENT_URLorigine autorisée par CORSnon
backS3_REGION / BUCKETemplacement du coffre S3non
backS3_KEY / S3_SECRETaccès au bucketoui
frontVITE_API_URLURL de l'API en productionnon — 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.

Ce que tu dois voir
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.

Ce que tu dois voir
// 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
TableCollectionpas de colonnes imposées : deux documents peuvent différer
Ligne (row)Document (JSON)peut contenir tableaux et sous-objets
ColonneChamp (field)—
Clé primaire auto_id (ObjectId)12 octets, contient la date de création
Clé étrangère (FK)référence par _idpas imposée par Mongo — c'est à toi de la gérer
JOINpopulate() / $lookupplus 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 ? »

TechniqueIdéeQuand 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.

0 sur 26 dans cette étape

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).

0 sur 26 dans cette étape

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é.

Schéma : VISITEUR · id, nom, email, role · FICHE · id, mois, etat, #visiteur · LIGNE_FORFAIT · #fiche, #type, qte · LIGNE_HORS_FORFAIT · #fiche, date, montant · TYPE_FRAIS · code, libelle, montant · 1,N

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 demandeCe que tu montres
Concevoir le modèle de donnéesdécisions embedding/référence + le MLD équivalent
Écrire des requêtes pour extraire/manipulerles agrégations du chapitre 5
Assurer l'intégrité et la cohérenceindex unique, enum, required, validation $jsonSchema
Optimiser les performancesindex {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.

0 sur 26 dans cette étape

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érateurSensExemple
$gt / $ltsupérieur / inférieur{ "lignesHorsForfait.montant": { $gt: 50 } }
$indans une liste{ etat: { $in: ["CL", "VA"] } }
$nedifférent de{ etat: { $ne: "CR" } }
$existschamp présent{ dateEmbauche: { $exists: true } }
$regexmotif 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 ».)

0 sur 26 dans cette étape

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

VerbeIntentionExemple AppliFrais
GETlire (ne change rien)lister les fiches, lire une fiche
POSTcréercréer une fiche, ajouter une ligne
PUTmodifier / remplacerclôturer, valider une fiche
DELETEsupprimersupprimer une fiche

2.3 — Les codes de réponse HTTP (le statut de chaque réponse)

CodeSensQuand
200 / 201OK / Créétout s'est bien passé
400mauvaise requêtedonnées invalides (ex. mois mal formé)
401non authentifiépas de jeton, ou jeton expiré
403interditconnecté, mais pas le droit (mauvais rôle)
404introuvablela fiche demandée n'existe pas
409conflitaction 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.

0 sur 26 dans cette étape

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.

0 sur 26 dans cette étape

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.

Ce que tu dois voir
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.

0 sur 26 dans cette étape

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

Schéma : React · API Express · MongoDB · POST /login {email, password} · ① Zod valide le type (anti-injection) · findOne({ email }) · le visiteur (avec son hash) · ② bcrypt.compare(saisi, hash) · ③ jwt.sign({ sub, role }, SECRET) · 200 { token, visiteur } · ④ setSession(token) · le jeton est…

① 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).

0 sur 26 dans cette étape

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).

0 sur 26 dans cette étape

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.

Schéma : CR · Créée · CL · Clôturée · VA · Validée · MP · Payée → RB · visiteur · comptable · comptable

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.

0 sur 26 dans cette étape

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.

0 sur 26 dans cette étape

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) :

Ce que tu dois voir
[
  { "_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")
Ce que tu dois voir
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.

0 sur 26 dans cette étape

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

powershell
# à 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)

Ce que tu dois voir
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

powershell
# 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.

python
// 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.

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

python
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.

0 sur 26 dans cette étape

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

python
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).

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é

html
<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.

0 sur 26 dans cette étape

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).

0 sur 26 dans cette étape

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.

Schéma : main.jsx (routeur) · Login (/login) · Layout (privé) · Liste (/) · Detail (/fiches/:id) · ScanRecu (OCR, dans Detail) · Login est public ; tout le reste est sous Layout, protégé par la garde « connecté ? ».

6.4 — Le routeur & la garde d'accès — main.jsx

python
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.

python
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.

0 sur 26 dans cette étape

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.

0 sur 26 dans cette étape

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.

0 sur 26 dans cette étape

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 MongoDBMettre l'image dans S3
gonfle la base, ralentit les lecturesla base reste légère (elle ne garde qu'une clé)
limite des 16 Mo par documentfichiers de n'importe quelle taille
coûteux à sauvegarderstockage 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.

0 sur 26 dans cette étape

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 OWASPNotre défenseVu au chapitre
A01 — Contrôle d'accès cassérôles + contrôle de propriété (anti-IDOR)4
A02 — Défauts cryptographiquesmot de passe en bcrypt (coût 12)3
A03 — Injectionvalidation du type des entrées (Zod)ci-dessous
A05 — Mauvaise configurationhelmet, CORS liste blanche, X-Powered-By coupéci-dessous
A07 — Authentification faibleJWT signé + expiration + rate limiting3

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.)

0 sur 26 dans cette étape

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)

AttaqueCe que tu envoiesRésultatDéfense
Injection NoSQL (A03)login avec {"email":{"$gt":""}}400validation Zod du type
Rôle (A01)visiteur appelle PUT /:id/valider403requireRole("comptable")
IDOR (A01)visiteur lit la fiche d'un autre403contrôle de propriété
Brute force (A07)6 logins faux d'affilée429rate 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.

0 sur 26 dans cette étape

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\""
}
powershell
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.

0 sur 26 dans cette étape

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

powershell
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 !)

Ce que tu dois voir
# .gitignore — à créer AVANT git add .
node_modules/
.env
frontend/dist/
*.log
.DS_Store

L.7 — Des commits lisibles & des branches

powershell
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).

0 sur 26 dans cette étape

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 ?

Schéma : Front (dist/) · Netlify / Vercel · API Express · Render / Railway · MongoDB Atlas · Bucket S3 · La base et le stockage sont DÉJÀ dans le cloud : seuls le front et l'API restent à déployer.

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).

0 sur 26 dans cette étape

Pour finir

Ce que tu sais faire maintenant

DomaineTu sais…
Modéliserchoisir embarquer/référencer et le justifier, poser index & validation
Défendre ton modèlerépondre à « trop simple » avec le MLD équivalent et les preuves C2.3
Concevoir une APIREST, verbes, codes HTTP, versionnement, idempotence
Sécuriser l'accèsbcrypt, JWT, rôles, anti-IDOR, rate limiting — et expliquer pourquoi
Exploiter les donnéesagrégation ($lookup, $group), prouver un index avec explain()
Construire le frontcomposants, état, appels API centralisés, UX selon le rôle
Gérer les fichiersstockage S3, liens signés, et l'argument RGPD

Mini-quiz d'auto-vérification

  1. En NoSQL, quelle est la question n°1 pour décider d'embarquer ou référencer ?
  2. Pourquoi le visiteur est-il référencé et pas embarqué dans la fiche ?
  3. Un JWT est-il chiffré ou signé ? Quelle conséquence pratique ?
  4. Différence entre un code 401 et un code 403 ?
  5. Pourquoi la sécurité des rôles doit-elle être côté serveur et pas côté React ?
  6. À quoi sert $lookup dans un pipeline d'agrégation ?
  7. Pourquoi stocke-t-on la photo du reçu dans S3 et pas dans MongoDB ?
  8. 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.
2Il est partagé (plusieurs fiches) et mutable (il change). Le recopier créerait des incohérences ; on garde un lien qu'on rapproche avec populate().
3Signé, 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.
4401 = « je ne sais pas qui tu es » (non authentifié). 403 = « je sais qui tu es mais tu n'as pas le droit » (non autorisé).
5Le 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.
7Pour ne pas gonfler la base (limite 16 Mo, lectures lentes). La base ne garde qu'une clé ; le fichier vit dans le stockage objet.
8Envoyer 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

APIensemble d'adresses (routes) que le front appelle pour lire/écrire des données.
Agrégationpipeline d'étapes ($match, $unwind, $lookup, $group…) pour croiser et totaliser des données.
bcryptfonction 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é.
CORSmécanisme qui autorise (ou non) une origine à appeler une API depuis le navigateur.
DCPdonnée à caractère personnel (RGPD) — ex. un reçu, un email.
Embedding / référenceembarquer = ranger les données dans le document ; référencer = garder un lien (_id).
enumliste fermée de valeurs autorisées pour un champ (ex. les états CR/CL/VA…).
IDORfaille où l'on accède à la ressource d'autrui en changeant juste l'id dans l'URL.
Indexstructure qui accélère les recherches sur un champ (IXSCAN vs COLLSCAN).
Idempotentaction qu'on peut refaire sans changer le résultat (GET, PUT) — pas POST.
JWTjeton 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).
OCRreconnaissance de texte dans une image (lire le montant d'un ticket photographié).
OWASPré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.
SPASingle Page Application : l'appli ne recharge jamais la page entière (React).
Virtualchamp calculé à la lecture (ex. le total), jamais stocké, donc toujours juste.
Zodbibliothè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é

TP terminé ?

Coche chaque livrable quand tu l'as : c'est la preuve que l'étape est acquise.

livrables
–
de série
–
Voir tous les TP