Skip to content

feat(api): signaler un parcours en ligne (POST /routes/:id/reports) - #592

Merged
PaulThiberville merged 2 commits into
mainfrom
issue/437-route-reports
Oct 21, 2026
Merged

PaulThiberville merged 2 commits into
mainfrom
issue/437-route-reports

Conversation

@Nascimento95

@Nascimento95 Nascimento95 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Résultat et problème

L'app n'avait aucune route pour signaler un parcours : la table route_reports et la règle des 7 jours existaient (#184, PR #432), mais rien ne permettait d'écrire un signalement depuis un client. Cette PR expose POST /v1/routes/:id/reports, que « Signaler ce parcours » (E-05, ticket #50) appellera et que l'onglet Parcours de l'admin (A-03, ticket #54) lit.

  • Route (apps/api/src/routes/route-reports.ts) : authentifiée, jamais en cache. Corps { reason, stepId?, comment? }, strict. Elle repose sur createRouteReport (feat(api): ajouter les tables route_reports et notifications #432) : la règle des 7 jours glissants, la vérification de l'étape et le verrou contre deux envois simultanés (la file hors connexion de l'app qui renvoie) sont les siens ; rien n'est dupliqué.
  • Réponses : 201 avec le signalement créé (identifiant, motif, statut open, date : rien du signaleur) ; 409 conflict avec details.reportedAt du premier signalement sous 7 jours ; 403 sur son propre parcours ; 404 pour un parcours qui n'est pas en ligne, retiré ou inconnu ; 401 sans jeton ; 422 validation_error pour un corps invalide ; 400 pour un identifiant de parcours qui n'est pas un UUID.
  • Règles du corps (packages/shared/src/schemas/route-report.ts, d'après reports.ts) : un motif parmi ceux que l'utilisateur choisit (jamais low_rating) ; stepId pour place_changed et aucun autre motif ; commentaire de 80 caractères en texte brut, facultatif pour place_changed, obligatoire pour other, refusé pour les autres motifs.
  • Pas de migration, aucune dépendance.

Écarts avec le ticket (annoncés dans mon commentaire de démarrage sur le ticket)

  • 422 au lieu de 400 pour l'étape manquante, l'étape d'un autre parcours, le commentaire manquant ou de trop : la convention de l'API (D-071) répond 422 à un corps invalide et 400 à une query ou des params invalides. Le ticket dit 400 ; je suis la convention.
  • L'événement route_reported n'est pas émis par cette PR. L'API n'émet aucun événement PostHog (aucun client PostHog côté serveur), Analytics ne range pas route_reported parmi les événements « (serveur) », et le ticket dit lui-même que l'envoi hors connexion est côté app (Afficher et saisir la vérification des lieux dans la fiche et le parcours en cours #50). Le critère « route_reported est émis avec route_id, reason, step_index » n'est donc pas tenu ici : l'événement est celui de l'app, à émettre avec le ticket Afficher et saisir la vérification des lieux dans la fiche et le parcours en cours #50.
  • Un parcours « à corriger » (needs_fix) reste signalable : il est toujours en ligne (onlineRouteStatuses, comme la fiche et les avis). Le ticket dit « parcours non publié » pour le 404 ; un brouillon, un parcours refusé ou retiré, et un identifiant inconnu répondent 404, indiscernables les uns des autres.

Fixes #437
Écrans : aucun (l'écran « Signaler ce parcours » est E-05, ticket #50)
Dépendance : aucune (base main)

Comment vérifier

Il n'y a pas d'écran. Les mêmes requêtes se rejouent contre l'API locale avec un compte de l'émulateur d'authentification Firebase (firebase emulators:start --only auth --project demo-widoo, l'API lancée avec FIREBASE_PROJECT_ID=demo-widoo et FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099) :

  1. Un signalement {"reason":"inappropriate"} sur un parcours publié répond 201 avec "status":"open" ; une ligne open apparaît dans route_reports.
  2. Le même utilisateur, même parcours, {"reason":"false_info"} : 409 avec la date du premier.
  3. {"reason":"place_changed"} sans étape, {"reason":"other"} sans commentaire, et place_changed avec une étape d'un autre parcours : 422.
  4. Sur son propre parcours : 403 ; sur un identifiant inconnu : 404 ; sans jeton : 401.

Tests : 13 tests d'intégration (route-reports.test.ts : 201 et la ligne écrite, l'étape et le commentaire gardés, le 409 avec reportedAt et myReport de la fiche, un nouveau signalement permis une fois le premier décidé (D-057) et par un autre lecteur, 403, 404 (brouillon, retiré, inconnu), 401, needs_fix signalable, neuf corps refusés en 422 sans rien écrire, 400 pour un identifiant invalide, quatre envois simultanés qui n'écrivent qu'un signalement, chaque motif sans étape ni commentaire) et 4 sur le schéma côté shared.

Vérification humaine : aucune. Marco n'a pas rejoué les requêtes à la main (décision du 2026-10-21) et s'appuie sur les tests d'intégration et sur un passage des mêmes requêtes contre l'API locale, relevé par l'agent le 2026-10-21 (201, 409, 403, 404, trois 422).

  • pnpm lint && pnpm typecheck && pnpm test verts en local (chaque commit passe lint et types)

Formatage mécanique : non

Risque

Niveau : 2 — nouvel endpoint authentifié qui écrit à partir d'une entrée non fiable, sans migration.

Taille : tous les commits sous 400 lignes utiles (123 et 321, tests compris). Aucun fichier généré.

Revue OWASP (Top 10, édition en cours)

  • A01 Contrôle d'accès : requireAuth (401 sans jeton) ; l'auteur d'un parcours ne peut pas le signaler (403) ; un parcours qui n'est pas en ligne répond 404, sans distinguer un brouillon, un refus, un retrait ou un identifiant inconnu. Le signaleur est celui du jeton, jamais une valeur du corps.
  • A03 Injection : requêtes paramétrées ; corps strict (champ inconnu refusé), motif d'une liste fermée, commentaire en texte brut de 80 caractères sans balise ni caractère de contrôle ; stepId revérifié contre les étapes du parcours dans la transaction. Le commentaire n'est lu que par l'équipe et n'est jamais publié.
  • A04 Conception non sécurisée / abus : un signalement par utilisateur et par parcours sur 7 jours glissants, appliqué par la base et par le verrou de la ligne du signaleur (testé avec quatre envois simultanés : un 201, trois 409). Le débit général par IP de l'API s'applique ; aucune limite de plus par compte (un signalement par parcours et par semaine borne déjà l'écriture).
  • A09 Journalisation : pas de ligne admin_actions (ce n'est pas une écriture de l'admin) ; la décision de l'équipe sur le signalement est journalisée par A-03. Ni le commentaire ni le compte ne vont dans les journaux applicatifs.
  • Données personnelles : le signalement relie un compte, un parcours et un motif ; la réponse n'en renvoie rien d'autre ; l'export RGPD d'un compte (users/export) couvre déjà route_reports.

Nouvelle dépendance : aucune.

Retour arrière

Le revert de la PR suffit : la route disparaît (404), le code d'écriture createRouteReport et la table restent. Les signalements déjà écrits restent en base et dans l'admin ; il n'y a aucune migration à annuler.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
widoo-web Ready Ready Preview Oct 21, 2026 5:26pm UTC

@PaulThiberville

Copy link
Copy Markdown
Collaborator

Rebasée sur main par l'équipe pour repasser l'audit vite 8.3.4, aucun changement de code.

@PaulThiberville

Copy link
Copy Markdown
Collaborator

📋 MANAGER, 2026-10-21 — relecture contre #437 : verdict B (fusionnable après rebase sur main et CI verte ; niveau 2 confirmé)

Rebase : la branche part de 134c2b4a, 151 commits derrière main (d95f8793, pile admin fusionnée). Essayé dans un worktree, sans rien pousser : rebase sans conflit, seule et empilée avec #594. Sur le résultat : pnpm lint et pnpm typecheck verts ; route-reports.test.ts vert ; pnpm --filter @widoo/api test vert (980/980 seule, 998/998 avec #594), avec la même instabilité que main sur les tests de recherche (passage rouge sur search.test.ts aussi vu sur main nu → #623).

Critères : tous tenus, après deux corrections du ticket par MANAGER (les écarts annoncés dans la PR sont justes) :

Recouvrement avec la pile admin : aucun. #479 (#54, A-03 onglet Parcours) lit et tranche les route_reports que cette route écrit ; reports/submit.ts est nouveau et repose sur createRouteReport de #432, sans doublon. Le refus des comptes suspendus (403 account_suspended) arrive de main par requireAuth : il couvre cette route une fois rebasée.

Risque : niveau 2 juste (endpoint authentifié, entrée non fiable). Revue OWASP présente et concrète (accès, injection, abus par verrou et règle des 7 jours, journalisation, données personnelles). À suivre ailleurs : le jeton d'un compte anonyme (D-099) devra recevoir 403 account_required ici, comme sur toute route qui demande un compte (#532).

Modèle de PR : complet (lien, écrans, vérification, risque, taille, retour arrière). Il manque les labels type:feature et area:api sur la PR (seul level:2 est posé).

À faire avant la fusion : rebase sur main, CI verte, labels. Fusionnable ensuite sous l'autorisation permanente de Paul (niveau ≤ 2). Ordre conseillé : cette PR, puis #594, puis #595.

Nascimento95 and others added 2 commits October 21, 2026 19:23
…te (D-056)

The body of POST /routes/:id/reports follows the rules of reports.ts: a step for
« place_changed » and no other reason, a comment of 80 characters for
« place_changed » (optional) and « other » (required), never « low_rating ». The
answer holds the report and nothing of the reporter.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ver 7 days (D-056)

POST /routes/:id/reports answers 201 with the open report, 409 with reportedAt
inside the window, 403 on the route of the caller, 404 for a route that is not
online or unknown. It rests on createRouteReport: the rule of the 7 days, the check
of the step and the lock against two reports at once are its own.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@PaulThiberville
PaulThiberville force-pushed the issue/437-route-reports branch from 304324f to 581552e Compare October 21, 2026 17:26
@PaulThiberville PaulThiberville added type:feature Nouvelle fonctionnalité area:api apps/api labels Oct 21, 2026
@PaulThiberville

Copy link
Copy Markdown
Collaborator

Rebasée et complétée par l'équipe : rebase sur main (d95f8793) sans conflit, labels type:feature et area:api posés ; lint, typecheck et tests API (980/980) verts en local.

@PaulThiberville
PaulThiberville merged commit 8cef700 into main Oct 21, 2026
6 checks passed
@PaulThiberville
PaulThiberville deleted the issue/437-route-reports branch October 21, 2026 17:31
@PaulThiberville

Copy link
Copy Markdown
Collaborator

Accord de niveau 2 consigné par MANAGER : Paul (opérateur) a donné son accord en session le 2026-10-21 (« on fusionne »), PR fusionnée le même jour. Cet accord vaut accord de l'opérateur avant production au sens de la règle du niveau 2 ; le déploiement en production reste manuel, par Ilan ou Paul, après un staging vert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:api apps/api level:2 Risque niveau 2 (SECURITY.md) type:feature Nouvelle fonctionnalité

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exposer POST /routes/:id/reports pour signaler un parcours

2 participants