Skip to content

Retirer les coordonnées des étapes d'un parcours Premium de la recherche #161

Closed

Description

@PaulThiberville

Contexte

Epic : #8
Prérequis : #25 (recherche, PR #138)
Maquette : E-04 · validée le 2026-09-23 (D-014), révisée le 2026-09-28 (D-071) · tap Premium sans abonnement
API › Conventions · API › Découverte · API › Card de recherche · Compte-et-Monetisation › Freemium · E-04 · D-014 · D-075

Découverte de CODE pendant #26 (Inbox) : GET /v1/routes/search renvoie, dans steps[] de chaque card, la catégorie et les coordonnées de toutes les étapes, y compris pour un parcours access = premium. L'app les masque (D-014 : ni tracé ni étapes, départ seul), mais elles transitent en clair vers un client non abonné. Or le wiki est explicite : sans abonnement, les étapes 2 et suivantes n'ont « ni nom, ni lieu, ni coordonnées, ni conseil, ni tracé, dans la fiche comme sur la carte (départ seul). Le filtrage se fait dans l'API, pas seulement dans l'app ». C'est un défaut de contrôle d'accès (OWASP A01) sur un endpoint public.

Règle révisée le 2026-09-29 par D-075 : le filtrage ne doit pas dégrader l'abonné. E-04 (D-014, validée par Ilan le 2026-09-23) : « Abonné, le tap validé ne change pas. » Un abonné garde sur la carte l'expérience d'un parcours gratuit ; la voie abonné se livre dans ce ticket.

Comportement attendu

  • GET /v1/routes/search reste publique, avec authentification facultative (API › Conventions) : un jeton Firebase est accepté sans être exigé.
  • Card verrouillée (isLocked = true), pour un parcours access = premium appelé sans jeton ou par un compte qui n'a pas droit au parcours : steps ne porte que le départ (catégorie et coordonnées, name et durationMin à null) ; les étapes 2 et suivantes ne quittent pas l'API, ni nom, ni lieu, ni coordonnées, ni catégorie, ni photo. stepCount donne le nombre exact d'étapes (tooltip « Parcours Premium · N étapes en X h »).
  • Card déverrouillée (isLocked = false), avec le jeton valide d'un compte qui a droit au parcours : toutes les étapes, comme un parcours gratuit.
  • Droit au parcours : aujourd'hui users.plan = premium et users.plan_expires_at non passé ; plan expiré = pas de droit. Règle calculée en un seul endroit du code, que Appliquer les parcours Premium, le déblocage à l'unité et la limite anti-spam dans l'API #66 étend (pass 7 jours, déblocage à l'unité) et que Exposer GET /routes/:id et /routes/:id/timeline, stocker computed #36 réutilise pour GET /routes/:id.
  • Cache : sans en-tête Authorization, Cache-Control: public (30 s, ETag), comme aujourd'hui ; avec un en-tête Authorization, Cache-Control: private et Vary: Authorization, jamais servie à un autre appelant.
  • Jeton présent mais invalide ou expiré : 401 unauthorized, sans repli silencieux sur la réponse anonyme.
  • Parcours access = free : isLocked = false, toutes les étapes, stepCount = longueur de steps ; aucun autre changement.
  • GET /v1/routes/search/count ne change pas : le comptage ne dépend pas de l'appelant.
  • Schéma partagé RouteCard (packages/shared) : isLocked et stepCount ; l'app lit isLocked, jamais access seul, pour choisir l'affichage.
  • Carte (E-04) : card verrouillée → recadrage sans tracé ni étapes, départ en pastille encre à couronne, tooltip « Parcours Premium · N étapes en X h » ; card Premium déverrouillée → affichée comme un parcours gratuit (tracé, étapes, tooltips), avec sa pastille Premium.
  • packages/api-client : la fonction de recherche accepte un jeton facultatif et l'envoie en Authorization: Bearer quand il est fourni.

Critères d'acceptation

  • Test d'intégration : parcours premium sans jeton → card isLocked: true, départ seul (name et durationMin à null), stepCount exact ; aucune coordonnée, aucun nom ni aucune catégorie des étapes 2+ dans le JSON brut de la réponse
  • Test d'intégration : même résultat verrouillé avec le jeton d'un compte gratuit (recherche des valeurs dans le JSON brut)
  • Test d'intégration : jeton d'un compte premium dont plan_expires_at n'est pas passé → card isLocked: false avec toutes les étapes
  • Test d'intégration : jeton d'un compte premium dont plan_expires_at est passé → card verrouillée
  • Test d'intégration : jeton invalide ou expiré → 401 unauthorized
  • Tests des en-têtes : sans jeton Cache-Control: public avec ETag ; avec jeton Cache-Control: private et Vary: Authorization
  • Parcours free inchangé, isLocked: false ; /routes/search/count inchangé
  • isLocked dans RouteCard (packages/shared), lu par l'app pour choisir l'affichage (aucune décision d'affichage sur access seul)
  • Règle de droit au parcours en un seul endroit du code (fonction unique, testée), appelée par la recherche
  • Carte : parcours Premium verrouillé conforme à la maquette tap Premium sans abonnement ; parcours Premium déverrouillé affiché comme un parcours gratuit, avec sa pastille Premium (test de la couche carte sur les deux cas)
  • packages/api-client accepte un jeton facultatif pour la recherche
  • PR de niveau 2, revue OWASP A01 (et A07 pour la vérification du jeton) dans la section Risque

Notes techniques

Activity

  1. added this to the v0.2 milestone on Sep 25, 2026
  2. PaulThiberville commented on Sep 28, 2026

    @PaulThiberville
    CollaboratorAuthor

    🔨 Démarrage — plan :

    • API (toCard) : pour un parcours access = premium, la card ne porte plus que son départ dans steps (catégorie et coordonnées, sans nom ni durée, comme aujourd'hui) ; les étapes 2 et suivantes ne sortent plus du tout de la recherche. Nouveau champ stepCount sur toutes les cards pour garder le nombre d'étapes exact (tooltip « N étapes en X h »).
    • Schéma partagé RouteCard : stepCount et une règle qui refuse une card Premium portant plus que son départ (la réponse est sérialisée par ce schéma : filet côté serveur).
    • Mobile : la tooltip lit stepCount ; la carte ne dessine que les étapes reçues.
    • Tests : intégration sur le seed (parcours passé en premium dans un test isolé, aucune coordonnée ni catégorie d'étape 2+ dans le JSON), schéma, tests mobiles adaptés.

    Branche issue/161-premium-search-steps, PR empilée sur #192.

  3. PaulThiberville commented on Sep 29, 2026

    @PaulThiberville
    CollaboratorAuthor

    📋 Correction du ticket (MANAGER, 2026-09-29) — @PaulThiberville @IlanAMG

    Ce qui était faux. Le corps écrit le 2026-09-25 tranchait seul une question de spec : « La réduction s'applique à tout appelant de la recherche […] Un abonné obtiendra le détail par GET /routes/:id », avec une note « À signaler à Ilan ». Ce choix contredit Ecrans › E-04 (D-014, validée par Ilan le 2026-09-23) : « Abonné, le tap validé ne change pas. » Il aurait fallu un needs-design, pas une note.

    Décision du 2026-09-29. Paul, au nom d'Ilan, a refusé ce choix : l'abonné garde sur la carte l'expérience d'un parcours gratuit, et la voie abonné se livre dans la même PR (#208). Règle publiée par DESIGN : D-075 (API › Conventions, API › Card de recherche).

    Corps réécrit. Le contexte du défaut d'accès ne change pas. « Comportement attendu », critères et notes suivent D-075 : authentification facultative, card verrouillée (isLocked = true, départ seul, stepCount exact) sans jeton ou sans droit, card complète avec le jeton d'un compte premium non expiré, règle de droit en un seul endroit, cache public / private + Vary: Authorization, 401 sur jeton invalide, /routes/search/count inchangé. Hors ticket : envoi du jeton par l'app et vidage du cache (#38), relance après achat (#67).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions