Repository navigation
Retirer les coordonnées des étapes d'un parcours Premium de la recherche #161
Description
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/searchreste publique, avec authentification facultative (API › Conventions) : un jeton Firebase est accepté sans être exigé.- Card verrouillée (
isLocked = true), pour un parcoursaccess = premiumappelé sans jeton ou par un compte qui n'a pas droit au parcours :stepsne porte que le départ (catégorie et coordonnées,nameetdurationMinànull) ; les étapes 2 et suivantes ne quittent pas l'API, ni nom, ni lieu, ni coordonnées, ni catégorie, ni photo.stepCountdonne 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 = premiumetusers.plan_expires_atnon 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 pourGET /routes/:id. - Cache : sans en-tête
Authorization,Cache-Control: public(30 s, ETag), comme aujourd'hui ; avec un en-têteAuthorization,Cache-Control: privateetVary: 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 desteps; aucun autre changement. GET /v1/routes/search/countne change pas : le comptage ne dépend pas de l'appelant.- Schéma partagé
RouteCard(packages/shared) :isLockedetstepCount; l'app litisLocked, jamaisaccessseul, 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 enAuthorization: Bearerquand il est fourni.
Critères d'acceptation
- Test d'intégration : parcours
premiumsans jeton → cardisLocked: true, départ seul (nameetdurationMinànull),stepCountexact ; 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
premiumdontplan_expires_atn'est pas passé → cardisLocked: falseavec toutes les étapes - Test d'intégration : jeton d'un compte
premiumdontplan_expires_atest passé → card verrouillée - Test d'intégration : jeton invalide ou expiré → 401
unauthorized - Tests des en-têtes : sans jeton
Cache-Control: publicavec ETag ; avec jetonCache-Control: privateetVary: Authorization - Parcours
freeinchangé,isLocked: false;/routes/search/countinchangé -
isLockeddansRouteCard(packages/shared), lu par l'app pour choisir l'affichage (aucune décision d'affichage suraccessseul) - 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-clientaccepte 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
- Hors de ce ticket : l'app n'a pas encore de session (connexion : Implémenter la connexion (E-10), le profil (E-09) et la suppression de compte #38, v0.3). L'envoi du jeton par l'app, le rafraîchissement et le nouvel essai sur 401, et le vidage du cache des résultats quand le droit change (connexion, déconnexion) se branchent avec Implémenter la connexion (E-10), le profil (E-09) et la suppression de compte #38 ; la relance de la recherche après un achat ou une restauration avec Brancher RevenueCat et construire l'écran Premium (E-17) #67. Ici, l'app continue d'appeler la recherche sans jeton ; seul le client accepte un jeton facultatif.
- Personne n'est Premium avant Brancher RevenueCat et construire l'écran Premium (E-17) #67 (v0.4) : la voie abonné se prouve par les tests d'intégration.
steps[].photoUrl(Ajouter la photo du lieu aux étapes de la card de recherche (steps[].photoUrl) #219) suivra les étapes : absente des étapes que la card ne porte pas.
Activity
PaulThiberville commentedon Sep 28, 2026
🔨 Démarrage — plan :
toCard) : pour un parcoursaccess = premium, la card ne porte plus que son départ danssteps(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 champstepCountsur toutes les cards pour garder le nombre d'étapes exact (tooltip « N étapes en X h »).RouteCard:stepCountet 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).stepCount; la carte ne dessine que les étapes reçues.premiumdans 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.PaulThiberville commentedon Sep 29, 2026
📋 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 unneeds-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,stepCountexact) sans jeton ou sans droit, card complète avec le jeton d'un comptepremiumnon expiré, règle de droit en un seul endroit, cachepublic/private+Vary: Authorization, 401 sur jeton invalide,/routes/search/countinchangé. Hors ticket : envoi du jeton par l'app et vidage du cache (#38), relance après achat (#67).test(mobile): give the cards of the filters fixture their lock