Commit bf592864 authored by Kourser's avatar Kourser
Browse files

Navigation : les boutons du rail ne menaient nulle part

Signalé à l'usage : « Rejoindre » sur une crise déclenchée ne faisait rien.

Ce qui s'était passé
  En préfixant l'API par « /api » pour lever la collision entre chemins
  d'écran et chemins de données, la réécriture de masse a aussi touché les
  navigations. Seize appels de « aller() » et « Lien vers » pointaient vers
  « /api/crises/… », « /api/preparation », « /api/administration ».

  Le routeur ne reconnaît pas ces chemins : il retombe sur l'espace
  d'organisation. L'URL changeait, l'écran restait le même, rien n'apparaissait
  en console. Tout le rail de crise était concerné, plus la sortie du
  déclenchement — c'est-à-dire tout ce qu'on fait après avoir ouvert une crise.

Pourquoi je ne l'avais pas vu
  J'avais vérifié chaque écran en m'y rendant par son adresse. Chacun
  fonctionnait. Ce sont les chemins *entre* les écrans qui étaient cassés, et
  on ne les emprunte qu'en cliquant.

Deux garde-fous, parce que la correction seule ne protège de rien
  « aller() » refuse désormais un chemin commençant par « /api/ » et le dit,
  au lieu de laisser un bouton muet. C'est l'unique point de passage de la
  navigation : le contrôle y est complet, y compris pour un chemin calculé.

  Une suite statique relit les 35 sources de l'interface sans navigateur :
  aucune navigation vers l'espace du serveur, tous les appels de données
  préfixés, et le garde-fou du routeur toujours en place. Un lien « <a href> »
  vers l'API reste licite — c'est une vraie navigation vers le serveur, comme
  le tirage papier du dossier.

La vérification n'efface plus la base de développement
  « pnpm verif » remettait à zéro le schéma de la base courante. Lancé pendant
  qu'on travaille sur l'instance, il détruit le compte, l'organisation et la
  crise en cours. C'est arrivé aujourd'hui, à l'utilisateur, de ma main.

  Elle a maintenant sa propre base — le nom courant suffixé de « _verif »,
  créée au besoin, ou celle que désigne KASTELL_DB_VERIF — et refuse de
  travailler dans celle de développement. Un avertissement dans la
  documentation ne suffisait pas : l'outil devait rendre l'accident impossible.

Un 404 de bon fonctionnement, en moins
  L'interface sondait la console root et se servait du refus comme réponse,
  ce qui inscrivait une erreur dans la console de chaque compte non root, à
  chaque ouverture. Le droit est désormais porté par la session. Sur un outil
  qu'on ouvre quand plus rien n'est sûr, une erreur qui n'en est pas une est
  une erreur de trop.

463 → 467 garanties. Éprouvé en cliquant, cette fois : inscription, connexion,
déclenchement, puis les dix entrées du rail une à une, chemin et écran relevés.

Signed-off-by: default avatarKourser <contact@kourser.bzh>
Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 75247cda
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -9,6 +9,10 @@ KASTELL_URL_PUBLIQUE=http://localhost:8080
# avez modifié Kastell, faites pointer cette adresse vers votre dépôt : elle
# s'affiche dans l'interface et la licence le demande.
KASTELL_SOURCE_URL=https://github.com/kourser/kastell

# Base utilisée par « pnpm verif », qui la réinitialise à chaque exécution.
# À défaut : le nom de la base courante suffixé de « _verif », créée au besoin.
# KASTELL_DB_VERIF=kastell_verif
KASTELL_PORT=8080
KASTELL_ENV=developpement

+4 −2
Original line number Diff line number Diff line
@@ -68,8 +68,10 @@ Avant toute demande de fusion, ces trois commandes doivent passer&nbsp;:
pnpm typecheck && pnpm verif && pnpm accords
```

`pnpm verif` **remet le schéma à zéro** et rejoue plus de quatre cent cinquante
garanties. Il refuse de s'exécuter si `KASTELL_ENV=production`.
`pnpm verif` travaille dans **sa propre base** (`…_verif`, créée au besoin), la
remet à zéro et rejoue plus de quatre cent soixante garanties. Il ne touche
jamais votre base de développement, et refuse de s'exécuter si
`KASTELL_ENV=production`.

## Ce qu'on attend d'un correctif

+9 −5
Original line number Diff line number Diff line
@@ -69,11 +69,15 @@ pnpm dev # API sur 8080, interface sur 5173 avec rechargement
pnpm verif
```

**Ce contrôle réinitialise le schéma** et refuse de s'exécuter si
`KASTELL_ENV=production` : certaines garanties ne s'observent que sur une
instance vierge, l'amorçage du premier root notamment — et les journaux
refusent d'être vidés, ce qui est tout leur intérêt. Sur une instance en
service, utilisez la vérification d'intégrité de la console.
**Le contrôle a sa propre base** — le nom de la base courante suffixé de
`_verif`, créée au besoin, ou celle que désigne `KASTELL_DB_VERIF`. Il la
réinitialise à chaque exécution et ne touche jamais celle de développement :
certaines garanties ne s'observent que sur une instance vierge, l'amorçage du
premier root notamment — et les journaux refusent d'être vidés, ce qui est tout
leur intérêt.

Il refuse en outre de s'exécuter si `KASTELL_ENV=production`. Sur une instance
en service, utilisez la vérification d'intégrité de la console.

Il applique les migrations, joue une crise complète, puis vérifie une
à une les garanties de la boîte noire : refus de modification et de suppression
+9 −1
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import { ErreurAuth } from "../auth/comptes.js";
import { QuotaAtteint } from "../administration/quotas.js";
import { TropDeTentatives } from "../auth/limites.js";
import * as sessions from "../auth/sessions.js";
import { estRoot } from "../noyau/administration.js";

/**
 * Routes d'authentification.
@@ -199,9 +200,15 @@ export function enregistrerRoutesAuth(app: FastifyInstance, env: Env, sql: Sql):
  // ── Session courante (EF-109, EF-110) ────────────────────────────────────
  app.get("/auth/moi", { preHandler: requerirSession(false) }, async (req, reply) => {
    const auth = req.auth!;
    const [organisations, codesRestants] = await Promise.all([
    const [organisations, codesRestants, root] = await Promise.all([
      comptes.organisationsDe(sql, auth.compte.id),
      comptes.codesSecoursRestants(sql, auth.compte.id),
      // Dit ici plutôt que deviné ailleurs : l'interface sondait la console
      // root et se servait du 404 comme réponse, ce qui inscrivait une erreur
      // dans la console de chaque compte non root, à chaque ouverture. Une
      // erreur de bon fonctionnement est une erreur de trop sur un outil
      // qu'on ouvre quand plus rien n'est sûr.
      estRoot(sql, auth.compte.id),
    ]);
    return reply.send({
      compte: {
@@ -209,6 +216,7 @@ export function enregistrerRoutesAuth(app: FastifyInstance, env: Env, sql: Sql):
        emailVerifie: auth.compte.email_verifie,
        secondFacteurActif: auth.compte.totp_actif_at !== null,
        codesSecoursRestants: codesRestants,
        root,
      },
      session: {
        id: auth.session.id,
+46 −2
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ import { Rapport } from "./verification/rapport.js";
import { verifierAuthentification } from "./verification/auth.js";
import { verifierAdministration } from "./verification/administration.js";
import { verifierLicence } from "./verification/licence.js";
import { verifierInterface } from "./verification/interface.js";
import { interceptet } from "./verification/boite.js";
import { verifierPreparation } from "./verification/preparation.js";
import { verifierConduite } from "./verification/conduite.js";
@@ -35,7 +36,6 @@ const verifie = (exigence: string, libelle: string, ok: boolean, detail = "") =>
  rapport.verifie(exigence, libelle, ok, detail);

const env = chargerEnv();
const sql = ouvrirBase(env.DATABASE_URL);

// La vérification doit être reproductible : certaines garanties ne s'observent
// que sur une instance vierge — l'amorçage du premier root, notamment. Et les
@@ -48,7 +48,48 @@ if (env.KASTELL_ENV === "production") {
  );
  process.exit(2);
}
console.log("Réinitialisation du schéma (KASTELL_ENV=" + env.KASTELL_ENV + ")");

/**
 * La vérification a sa propre base.
 *
 * Elle partageait celle de développement, et la remettait à zéro à chaque
 * exécution : lancer `pnpm verif` détruisait l'instance sur laquelle on était
 * en train de travailler — compte, organisation, crise en cours. Le mot de la
 * documentation ne suffisait pas ; l'outil doit rendre l'accident impossible.
 *
 * `KASTELL_DB_VERIF` permet d'en désigner une autre. À défaut, le nom de la
 * base courante suffixé de `_verif`, créée au besoin.
 */
const cible = new URL(env.DATABASE_URL);
if (process.env["KASTELL_DB_VERIF"]) {
  cible.pathname = `/${process.env["KASTELL_DB_VERIF"]}`;
} else {
  cible.pathname = `${cible.pathname.replace(/\/$/, "")}_verif`;
}
const nomBase = decodeURIComponent(cible.pathname.slice(1));

if (nomBase === decodeURIComponent(new URL(env.DATABASE_URL).pathname.slice(1))) {
  console.error("La base de vérification ne peut pas être la base de développement.");
  process.exit(2);
}

// Création si besoin, depuis la base d'administration : `create database` ne
// s'exécute pas depuis la base qu'il crée.
const administration = new URL(env.DATABASE_URL);
administration.pathname = "/postgres";
const admin = ouvrirBase(administration.toString());
try {
  const existe = await admin`select 1 from pg_database where datname = ${nomBase}`;
  if (existe.length === 0) {
    await admin.unsafe(`create database "${nomBase.replace(/"/g, '""')}"`);
    console.log(`Base de vérification « ${nomBase} » créée.`);
  }
} finally {
  await admin.end();
}

const sql = ouvrirBase(cible.toString());
console.log(`Réinitialisation du schéma de « ${nomBase} » (KASTELL_ENV=${env.KASTELL_ENV})`);
await sql.unsafe("drop schema public cascade; create schema public;");

interceptet();
@@ -205,6 +246,9 @@ await verifierAdministration(sql, app, rapport, env);
// ── Licence et clause réseau (§12) ────────────────────────────────────────
await verifierLicence(sql, app, rapport, env);

// ── Chemins de l'interface (§11) ──────────────────────────────────────────
await verifierInterface(rapport);

// ── Dossier de préparation (§6.2) ─────────────────────────────────────────
await verifierPreparation(sql, app, rapport);

Loading