Aller au contenu

Thèmes finaux

Une fois le parcours principal (100 énigmes sur 5 niveaux) complété, l'étudiant est redirigé vers la page de succès qui lui propose cinq thèmes pour une ultime énigme. Chaque thème est un composant React autonome chargé dynamiquement et qui valide sa réponse contre un hash SHA-256 côté serveur. La réussite d'un thème débloque l'accès à un hub de mini-jeux pédagogiques.

Cette page décrit les cinq thèmes, leur mécanique commune, et accorde une section dédiée au thème Logique & Mathématiques qui utilise une architecture à deux phases spécifique (V38 et V39).

Vue d'ensemble des cinq thèmes

Thème Id Composant Couleur Icône Thématique
Performance performance ThemePerformance.jsx #34d399 (vert) Métriques, débit, latence
Fonctionnel fonctionnel ThemeFonctionnel.jsx #60a5fa (bleu) 🔧 Exceptions, chaos applicatif
Métier metier ThemeMetier.jsx #f59e0b (orange) 💼 Anomalies commerciales, TVA
Sécurité securite ThemeSecurite.jsx #f87171 (rouge) 🔒 OWASP, vulnérabilités web
Logique & Maths logique ThemeLogique.jsx #a78bfa (violet) 🧠 Pool de 25 questions, tirage déterministe

L'ordre dans la grille de la page de succès est fixé par THEMES_LIST dans frontend/src/pedagogique/themes/index.js — les quatre thèmes « concepts » en premier (performance, fonctionnel, métier, sécurité) et Logique en cinquième. Les libellés et descriptions sont résolus via les clés i18n themes.<id>.label et themes.<id>.description.

Mécanique commune aux quatre thèmes « concepts »

Les thèmes Performance, Fonctionnel, Métier et Sécurité partagent la même structure : un énoncé illustré par un exemple concret, un bouton 💡 qui développe trois indices progressifs, un champ de réponse, et un bouton de validation qui appelle directement POST /pedagogique/finale/validate.

sequenceDiagram
    autonumber
    actor E as Étudiant
    participant UI as Theme{Concept}.jsx
    participant API as sha256hex()
    participant CTR as ChaosStudentController

    E->>UI: Lit l'énoncé et les indices
    E->>UI: Tape une réponse<br/>(1 mot-clé concept)
    E->>UI: Clique "Valider"

    UI->>API: sha256hex(answer)
    API-->>UI: hash hex 64 chars

    UI->>CTR: POST /pedagogique/finale/validate<br/>header X-Student-Token<br/>{theme:"<id>", answerHash:"..."}

    CTR->>CTR: getSession(token)
    CTR->>CTR: switch(theme) { case "<id>" → sha256(keyword) }

    alt hash match
      CTR-->>UI: {valid:true, gameUrl}
      UI->>UI: setTimeout 1800ms
      UI->>UI: onSuccess(gameUrl)
      UI->>E: window.location = gameUrl
    else hash mismatch
      CTR-->>UI: {valid:false}
      UI->>E: "Réponse incorrecte"
    end

Endpoint POST /pedagogique/finale/validate

@PostMapping("/pedagogique/finale/validate")
public ResponseEntity<?> validateFinale(
        @RequestBody Map<String, Object> body,
        @RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
            String studentToken) {
    // vérifs token...
    String theme         = (String) body.getOrDefault("theme", "");
    String submittedHash = (String) body.getOrDefault("answerHash", "");

    String expectedHash = switch (theme) {
        case "performance" -> PedagogiqueSessionService.sha256("<keyword>");
        case "fonctionnel" -> PedagogiqueSessionService.sha256("<keyword>");
        case "metier"      -> PedagogiqueSessionService.sha256("<keyword>");
        case "securite"    -> PedagogiqueSessionService.sha256("<keyword>");
        case "logique"     -> session.logiqueExpectedHash; // ou fallback
        default            -> "";
    };

    if (expectedHash.isEmpty())
        return ResponseEntity.badRequest().body(...);

    if (!expectedHash.equalsIgnoreCase(submittedHash)) {
        return ResponseEntity.ok(Map.of("valid", false));
    }

    String targetUrl = /* URL du hub — lue depuis l'environnement */;
    return ResponseEntity.ok(Map.of("valid", true, "gameUrl", targetUrl));
}
Propriété Valeur
Méthode POST
URL /api/chaos/student/pedagogique/finale/validate
Authentification Header X-Student-Token
Body {"theme": "<id>", "answerHash": "<sha256 hex>"}
Réponse 200 OK {"valid": true, "gameUrl": "..."} ou {"valid": false}
Réponse 400 Thème inconnu
Réponse 401 Token manquant ou invalide

Les quatre mots-clés des thèmes « concepts » sont définis en clair dans le code source du backend (ChaosStudentController). Ils ne sont pas exposés via API et ne sont pas stockés en base ; seul le hash calculé à la volée est utilisé pour la comparaison. Cette documentation ne les divulgue pas : chaque thème a un indice fort dans son énoncé et une série d'indices progressifs accessibles via le bouton 💡.

Le thème Performance

  • Classe React : ThemePerformance.jsx
  • Couleur dominante : vert #34d399
  • Structure : un énoncé en trois paragraphes (illustration, observation, question), trois indices masquables, un champ de réponse à un mot, un bouton de validation.

Le thème invite l'étudiant à réfléchir à un concept central des tests de performance. L'indicateur attendu n'est pas un chiffre mais un mot-clé du vocabulaire métier — le genre de terme qu'on lit dans un dashboard Grafana ou dans une spec de tir de charge.

Les trois indices du bouton 💡 progressent du plus abstrait (« c'est ce qu'on mesure quand on veut savoir combien la plateforme peut encaisser ») au plus spécifique (« le terme anglais qu'on voit sur les métriques Grafana »). Tous les textes sont externalisés en clés i18n theme.perf.*.

Le thème Fonctionnel

  • Classe React : ThemeFonctionnel.jsx
  • Couleur dominante : bleu #60a5fa
  • Structure : énoncé illustrant un incident JVM classique, trois indices, champ libre.

Le thème Fonctionnel renvoie aux chaos fonctionnels de PerfShop (F1 à F4 — voir chaos/functional.md). Il demande à l'étudiant d'identifier l'une des exceptions JVM terminales que peut déclencher le Chaos Fonctionnel. Le contexte fourni (trace réduite, type d'erreur, ligne de log) est conçu pour rappeler ce que l'étudiant a pu observer pendant le parcours s'il a activé le chaos correspondant.

Les clés i18n sont theme.fonc.*.

Le thème Métier

  • Classe React : ThemeMetier.jsx
  • Couleur dominante : orange #f59e0b
  • Structure : énoncé en quatre paragraphes (contexte commercial, exemple chiffré, observation, question), trois indices dont un indice par énumération de lettres (« les lettres sont : T, V, A »).

Le thème Métier exige une réponse courte en trois lettres. Le choix de l'énumération comme indice ultime est volontaire : il garantit que tout étudiant bloqué reçoit suffisamment d'info pour conclure, tout en forçant la réflexion si les deux premiers indices sont ignorés.

Les clés i18n sont theme.metier.*.

Le thème Sécurité

  • Classe React : ThemeSecurite.jsx
  • Couleur dominante : rouge #f87171
  • Structure : énoncé incluant un appel API concret (/api/orders/42), trois indices dont un par énumération de lettres, champ libre.

Le thème Sécurité renvoie à une famille de failles bien connue des listes OWASP. Comme pour le thème Métier, l'un des indices liste les lettres de la réponse dans le désordre — pédagogie : l'étudiant qui les voit ne peut pas deviner la réponse sans reconnaître le concept, il doit assembler les lettres mentalement.

Les clés i18n sont theme.secu.*.

Le thème Logique & Mathématiques

Le thème Logique est le seul thème qui propose cinq questions plutôt qu'une seule, et le seul qui utilise une architecture à deux phases spécifiquement conçue pour améliorer l'expérience utilisateur sans compromettre la sécurité.

Pool de 25 questions et tirage déterministe

Le backend contient un pool de 25 questions logiques et mathématiques externalisé dans i18n/logique/logique_<lang>.json :

[
  {"text": "…", "hint": "…"},
  {"text": "…", "hint": "…"},
   (23 autres)
]

Chaque entrée a deux champs : text (énoncé de la question) et hint (indice court, affichable à la demande). Les réponses attendues ne sont pas dans le fichier JSON — elles sont dans un tableau Java LOGIQUE_ANSWERS de DefaultPedagogiqueSessionService, indexé dans le même ordre que le fichier :

public static final String[] LOGIQUE_ANSWERS = {
    "…", "…", "…", "…", "…",   // [0-4]
    "…", "…", "…", "…", "…",   // [5-9]
    "…", "…", "…", "…", "…",   // [10-14]
    "…", "…", "…", "…", "…",   // [15-19]
    "…", "…", "…", "…", "…"    // [20-24]
};

Chaque étudiant reçoit cinq questions tirées du pool via un générateur pseudo-aléatoire déterministe seedé sur son token UUID. L'algorithme est un LCG (Linear Congruential Generator) identique en Java et en JavaScript :

public static int[] computeLogiqueIndices(String token) {
    int seed = 0;
    for (int i = 0; i < token.length(); i++) {
        seed = (seed << 5) - seed + token.charAt(i);
    }
    LinkedHashSet<Integer> used = new LinkedHashSet<>();
    while (used.size() < 5) {
        seed = (seed * 1103515245 + 12345) & 0x7fffffff;
        used.add(seed % LOGIQUE_ANSWERS.length);
    }
    int[] result = new int[5];
    int i = 0;
    for (int idx : used) result[i++] = idx;
    return result;
}

Propriétés du tirage :

  • Déterministe : deux étudiants avec le même token obtiennent les mêmes 5 questions dans le même ordre.
  • Distinctes : l'usage d'un LinkedHashSet garantit que les 5 indices sont uniques.
  • Uniformément distribuées : sur une classe de 30 étudiants, les cinq tirages couvrent statistiquement la quasi-totalité du pool.
  • Identique en JavaScript : la constante 1103515245 et l'offset 12345 sont ceux du LCG de glibc, reproductibles octet pour octet en JS ((seed * 1103515245 + 12345) & 0x7fffffff).

Architecture V38 — calcul au /join

Avant la version V38, le frontend exécutait le LCG lui-même pour choisir ses 5 questions à partir du token lu dans localStorage. Ce pattern posait un problème subtil : si le token stocké dans localStorage différait du token utilisé pour la session active (par exemple après un reset partiel, un changement d'onglet, un rechargement après expiration), le frontend tirait 5 questions sur une seed, le backend en vérifiait 5 autres sur une autre seed, et la validation finale échouait sans que l'étudiant puisse comprendre pourquoi.

La V38 corrige ce problème en déplaçant le calcul côté backend au moment du POST /pedagogique/join. Les cinq indices sont calculés une seule fois, stockés en base dans la colonne logique_question_indices (format "2,9,15,4,5"), et le hash attendu — concaténation des cinq bonnes réponses dans l'ordre du tirage — est stocké dans logique_expected_hash. Le frontend ne fait plus aucun LCG : il récupère ses cinq questions via l'endpoint GET /pedagogique/logique/questions.

// DefaultPedagogiqueSessionService.createSession()
int[]  logiqueIndices = computeLogiqueIndices(session.token);
String logiqueHash    = computeLogiqueHash(logiqueIndices);
session.logiqueQuestionIndices = indicesToString(logiqueIndices);
session.logiqueExpectedHash    = logiqueHash;
// ... stockés en base via PedagogiqueSessionEntity

Un fallback existe pour les sessions créées avant la migration V38 (colonnes NULL en base) : ThemeLogique.jsx n'en dépend pas, mais les endpoints logique recalculent les indices à la volée si besoin.

Endpoint GET /pedagogique/logique/questions

Retourne les cinq questions — textes et indices uniquement, jamais les réponses.

@GetMapping("/pedagogique/logique/questions")
public ResponseEntity<?> getLogiqueQuestions(
        @RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
            String studentToken) {
    // vérifs token...
    int[] indices = /* depuis session.logiqueQuestionIndices */;
    String[][] pool = loadLogiquePool(); // i18n/logique/logique_XX.json

    List<Map<String, Object>> questions = new ArrayList<>();
    for (int i = 0; i < indices.length; i++) {
        int idx = indices[i];
        Map<String, Object> q = new LinkedHashMap<>();
        q.put("index", idx);
        q.put("text",  pool[idx][0]);
        q.put("hint",  pool[idx][1]);
        questions.add(q);
    }
    return ResponseEntity.ok(Map.of("questions", questions));
}

Réponse type :

{
  "questions": [
    {"index": 2,  "text": "…", "hint": "…"},
    {"index": 9,  "text": "…", "hint": "…"},
    {"index": 15, "text": "…", "hint": "…"},
    {"index": 4,  "text": "…", "hint": "…"},
    {"index": 5,  "text": "…", "hint": "…"}
  ]
}

Le champ index (0-24) permet au frontend de conserver un identifiant stable par question, même si le format ou l'ordre change dans une future version.

Chargement du pool avec double-checked locking

La méthode loadLogiquePool() utilise un cache statique initialisé paresseusement via un double-checked locking pour éviter les chargements concurrents :

private static volatile String[][] logiquePoolCache = null;

static String[][] loadLogiquePool() {
    if (logiquePoolCache != null) return logiquePoolCache;
    synchronized (ChaosStudentController.class) {
        if (logiquePoolCache != null) return logiquePoolCache;
        // charge i18n/logique/logique_<LANG>.json via Jackson
        // ...
        logiquePoolCache = pool;
        return pool;
    }
}

Le chargement se fait une seule fois au premier appel (lazy) — pas au démarrage, contrairement aux énigmes BACx qui sont initialisées en static dans PedagogiqueEnigme. Le volatile garantit la visibilité entre threads, et le double-check évite la synchronisation une fois le cache chaud.

Architecture V39 — validation à deux phases

Le thème Logique est le seul des cinq thèmes à utiliser une validation en deux phases :

  1. Phase 1 — /logique/check : vérifie les cinq réponses individuellement et retourne un booléen par question. L'étudiant voit immédiatement quelles réponses sont correctes (✅) et lesquelles ne le sont pas (❌). Cet endpoint ne mute jamais la session et ne débloque jamais le jeu.
  2. Phase 2 — /finale/validate : vérifie le hash global de la concaténation des cinq réponses. C'est ce seul endpoint qui débloque le hub de jeux.

Cette séparation apporte une vraie amélioration UX — l'étudiant peut corriger uniquement les questions fausses au lieu de tout ressaisir — sans compromettre la sécurité : un attaquant qui brute-forcerait les 25 réponses individuellement ne pourrait toujours pas contourner la phase 2, qui exige le hash global. Le cout d'une attaque brute-force reste lié à la combinatoire des cinq questions ensemble.

Endpoint POST /pedagogique/logique/check

@PostMapping("/pedagogique/logique/check")
public ResponseEntity<?> checkLogiqueAnswers(
        @RequestBody Map<String, Object> body,
        @RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
            String studentToken) {
    // vérifs token...
    List<String> submittedHashes = (List<String>) body.get("answerHashes");
    if (submittedHashes == null || submittedHashes.size() != 5)
        return ResponseEntity.badRequest().body(...);

    int[] indices = /* depuis session */;

    List<Boolean> results = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        int idx = indices[i];
        String correctAnswer = LOGIQUE_ANSWERS[idx];
        String correctHash   = sha256(correctAnswer);
        String submittedHash = submittedHashes.get(i);
        results.add(submittedHash != null
            && correctHash.equalsIgnoreCase(submittedHash));
    }
    return ResponseEntity.ok(Map.of("results", results));
}
Propriété Valeur
Méthode POST
URL /api/chaos/student/pedagogique/logique/check
Auth Header X-Student-Token
Body {"answerHashes": ["hash1","hash2","hash3","hash4","hash5"]}
Réponse 200 {"results": [true, false, true, true, false]}
Effets de bord Aucun — lecture seule, pas de mutation de session

La comparaison est faite hash par hash : les bonnes réponses ne transitent jamais en clair, ni dans la requête, ni dans la réponse. L'attaquant qui intercepte le trafic ne voit que des hashes SHA-256.

Séquence complète V39

sequenceDiagram
    autonumber
    actor E as Étudiant
    participant UI as ThemeLogique.jsx
    participant API as sha256hex()
    participant CTR as ChaosStudentController

    Note over UI: Mount — fetch questions
    UI->>CTR: GET /pedagogique/logique/questions
    CTR-->>UI: {questions: [{text,hint,index}×5]}
    UI->>UI: Render 5 inputs + 5 boutons hint

    E->>UI: Remplit les 5 champs
    E->>UI: Clique "Valider tout"

    Note over UI: Phase 1 — check individuel
    loop Pour chaque réponse
      UI->>API: sha256hex(answer[i])
    end
    UI->>CTR: POST /logique/check<br/>{answerHashes: [h1,...,h5]}
    CTR->>CTR: Pour chaque i :<br/>compare sha256(LOGIQUE_ANSWERS[indices[i]])<br/>avec hash[i]
    CTR-->>UI: {results: [true,false,true,true,false]}

    UI->>UI: setResults([true,false,true,true,false])
    UI->>E: ✅ ❌ ✅ ✅ ❌ affichés

    alt Au moins une réponse fausse
      UI->>E: "2 réponses incorrectes"
      Note over E: L'étudiant corrige<br/>les questions fausses
      E->>UI: Modifie les réponses KO
      UI->>UI: setResults : neutralise ❌ des champs modifiés
      E->>UI: Re-clique "Valider tout"
      Note over UI: Retour au début de la Phase 1
    else Toutes correctes
      Note over UI: Phase 2 — validation finale
      UI->>UI: combined = concat(5 réponses normalisées)
      UI->>API: sha256hex(combined)
      UI->>CTR: POST /finale/validate<br/>{theme:"logique", answerHash:"..."}
      CTR->>CTR: compare session.logiqueExpectedHash
      CTR-->>UI: {valid:true, gameUrl}
      UI->>E: "Excellent !" puis onSuccess(gameUrl)
    end

UX de correction en place

Le composant ThemeLogique.jsx implémente un détail d'UX important : quand l'étudiant modifie une réponse marquée fausse, l'indicateur ❌ disparaît automatiquement et la bordure rouge repasse en neutre. Ce comportement signale clairement que la réponse est « en cours de correction » et n'a pas encore été revérifiée :

const setAnswer = (i, v) => {
  setAnswers(prev => { const n = [...prev]; n[i] = v; return n; });
  setResults(prev => {
    if (!prev || prev[i] !== false) return prev;
    const n = [...prev];
    n[i] = null;  // neutralise l'indicateur
    return n;
  });
};

Les indicateurs des réponses correctes (✅) ne sont pas neutralisés quand l'étudiant modifie d'autres champs — elles restent vertes tant que l'étudiant ne touche pas à ces inputs précis.

Sécurité du thème Logique

L'architecture V39 résiste au brute-force de plusieurs façons :

Attaque Défense
Deviner une seule réponse /logique/check est silencieux mais ne débloque rien
Enumérer toutes les combinaisons de 5 réponses /finale/validate attend le hash de la concaténation exacte — impossible de composer un hash global à partir des hashes individuels
Récupérer les réponses via /logique/questions L'endpoint ne retourne que text et hint, jamais answer
Récupérer les hashes attendus Aucun endpoint n'expose LOGIQUE_ANSWERS — les hashes sont calculés à la volée
Intercepter le trafic Toutes les réponses sont hashées en SHA-256 avant envoi
Rejouer une validation volée Le token est lié à une session unique — une URL /s/:token cesse de fonctionner à la désactivation du parcours

Par construction, la seule façon de débloquer le hub par le thème Logique est de connaître réellement les 5 bonnes réponses et de les ressaisir correctement.

Redirection vers le hub

Quelle que soit le thème choisi, une validation réussie ({"valid": true}) inclut un champ gameUrl que le composant utilise pour rediriger l'étudiant :

setTimeout(() => onSuccess(d.gameUrl), 1800);

// Dans PedagogiqueSucces
const handleSuccess = (gameUrl) => {
  if (gameUrl) window.location.href = gameUrl;
};

Le délai de 1800 ms laisse l'étudiant voir le message de succès avant la navigation. L'URL cible est injectée côté backend via une variable d'environnement — les détails d'hébergement, de nom de domaine et de port du hub ne sont pas documentés dans cette référence technique. Du point de vue de PerfShop, tout ce qui se passe après window.location.href est hors périmètre.

Ajouter un nouveau thème

L'ajout d'un sixième thème consiste à :

  1. Créer frontend/src/pedagogique/themes/ThemeNewTheme.jsx exportant un composant par défaut avec la signature ({ token, onSuccess }) => JSX
  2. Ajouter une entrée dans THEMES du fichier themes/index.js avec id, labelKey, descKey, color, et une fonction load pointant vers l'import() dynamique
  3. Ajouter la constante au tableau THEMES_LIST exporté
  4. Ajouter les clés i18n themes.newtheme.label et themes.newtheme.description dans fr.json et en.json
  5. Ajouter un case "newtheme" -> sha256("…") dans ChaosStudentController.validateFinale()

Aucune migration de base de données n'est nécessaire — le thème est entièrement stateless côté backend (à l'exception du cas Logique qui utilise les colonnes logique_question_indices et logique_expected_hash déjà présentes).


Pages précédentes : ← Page de succès · ← Concept et architecture · Retour au sommaire pédagogique