Commit c645bf55 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Un identifiant mal formé n'est pas une panne du serveur

Appeler une route de crise avec un identifiant qui n'est pas un UUID rendait
500. Les gardes s'exécutent avant la validation de la route : elles passaient
la chaîne telle quelle à Postgres, qui refusait la conversion en 22P02, et
l'erreur remontait en panne interne.

C'est deux fois faux. Le serveur n'est pas en panne — la demande est
incorrecte, et corrigible. Et un 500 dans les traces d'exploitation appelle
une enquête que rien ne justifie.

Les deux gardes contrôlent donc la forme avant d'interroger la base, et
rendent 400.

Les trois champs d'un point de situation disent par ailleurs ce qu'on attend
d'eux plutôt que « String must contain at least 1 character(s) ». Un formulaire
refusé sans dire quoi corriger se remplit deux fois.

Co-Authored-By: Claude (RCA)
parent b4ea2452
Loading
Loading
Loading
Loading
+10 −3
Changes for apps/api/src/routes/crise.ts: 10 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -354,9 +354,16 @@ export function enregistrerRoutesCrise(app: FastifyInstance, sql: Sql): void {
  app.post("/crises/:criseId/points", acces, async (req, reply) => {
    const c = z.object({
      celluleId: z.string().uuid().nullish(),
      sait: z.string().trim().min(1).max(10000),
      ne_sait_pas: z.string().trim().min(1).max(10000),
      en_cours: z.string().trim().min(1).max(10000),
      // Les trois rubriques du point sont exigées, et le disent en français :
      // le refus arrivait sous la forme d'un message anglais de la
      // bibliothèque de validation, devant un formulaire de six champs dont un
      // seul était marqué obligatoire à l'écran.
      sait: z.string().trim().min(1, "« Ce qu'on sait » est obligatoire.").max(10000),
      ne_sait_pas: z.string().trim()
        .min(1, "« Ce qu'on ne sait pas » est obligatoire : une direction qui ignore "
          + "l'étendue de son ignorance communique trop tôt.")
        .max(10000),
      en_cours: z.string().trim().min(1, "« Ce qu'on fait » est obligatoire.").max(10000),
      arbitrages: z.string().trim().max(10000).nullish(),
      prochaine_echeance: z.string().trim().max(1000).nullish(),
      destinataires: z.array(z.string().uuid()).default([]),
+24 −1
Changes for apps/api/src/routes/garde.ts: 24 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -122,6 +122,17 @@ async function porteur(
  return resolue ? req.porteur! : null;
}

/**
 * Un identifiant d'URL qui n'est pas un UUID ne désigne aucune ligne.
 *
 * Les routes valident déjà leurs paramètres — mais elles s'exécutent après ces
 * gardes, qui interrogeaient la base les premiers. Une adresse mal recopiée
 * partait donc jusqu'à PostgreSQL, qui la refusait par une erreur de syntaxe,
 * et l'appelant recevait « Erreur interne » en 500 : on l'envoyait chercher une
 * panne là où il n'y avait qu'un identifiant tronqué.
 */
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/**
 * Appartenance à l'organisation, avec un rôle suffisant. Le paramètre d'URL
 * porte l'organisation : on ne déduit jamais le périmètre de la session, sans
@@ -136,6 +147,12 @@ export function requerirMembre(sql: Sql, roles?: RoleOrganisation[]) {
    if (!organisationId) {
      return reply.code(400).send({ code: "requete_invalide", message: "Organisation absente." });
    }
    if (!UUID.test(organisationId)) {
      return reply.code(400).send({
        code: "requete_invalide",
        message: "Identifiant d'organisation invalide : vérifiez l'adresse.",
      });
    }
    const role = await roleDans(sql, resolue.compte.id, organisationId);
    if (!role) {
      return reply.code(403).send({ code: "non_membre", message: "Accès refusé." });
@@ -160,8 +177,14 @@ function acces(sql: Sql, avecAppareil: boolean) {
    if (!p) return;

    const criseId = (req.params as { criseId?: string }).criseId;
    if (!criseId || !UUID.test(criseId)) {
      return reply.code(400).send({
        code: "requete_invalide",
        message: "Identifiant de crise invalide : vérifiez l'adresse.",
      });
    }
    const [crise] = await sql<{ organisation_id: string }[]>`
      select organisation_id from crise where id = ${criseId ?? null}`;
      select organisation_id from crise where id = ${criseId}`;
    if (!crise) {
      return reply.code(404).send({ code: "introuvable", message: "Crise introuvable." });
    }