refactor(docs): réorganiser la documentation selon principes DDD
Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend. **Structure cible:** ``` docs/domains/ ├── README.md (Context Map) ├── _shared/ (Core Domain) ├── recommendation/ (Supporting Subdomain) ├── content/ (Supporting Subdomain) ├── moderation/ (Supporting Subdomain) ├── advertising/ (Generic Subdomain) ├── premium/ (Generic Subdomain) └── monetization/ (Generic Subdomain) ``` **Changements effectués:** Phase 1: Création de l'arborescence des 7 bounded contexts Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/ Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/ Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/ Phase 5: Création des README.md pour chaque domaine Phase 6: Déplacement des features Gherkin vers domains/*/features/ Phase 7: Création du Context Map (domains/README.md) Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh) Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/) **Configuration des tests:** - Makefile: godog run docs/domains/*/features/ - scripts/generate-bdd-docs.py: features_dir → docs/domains **Avantages:** ✅ Cohésion forte: toute la doc d'un domaine au même endroit ✅ Couplage faible: domaines indépendants, dépendances explicites ✅ Navigabilité améliorée: README par domaine = entrée claire ✅ Alignement code/docs: miroir de backend/internal/ ✅ Onboarding facilité: exploration domaine par domaine ✅ Tests BDD intégrés: features au plus près des règles métier Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
37
docs/domains/_shared/README.md
Normal file
37
docs/domains/_shared/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Domaine : Shared (Core Domain)
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le domaine **Shared** constitue le **Core Domain** de RoadWave. Il contient les fonctionnalités transversales essentielles utilisées par tous les autres bounded contexts de l'application.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- **Authentification et inscription** : Gestion des comptes utilisateurs, connexion, inscription
|
||||
- **Conformité RGPD** : Respect de la vie privée, consentements, suppression des données
|
||||
- **Gestion des erreurs** : Traitement cohérent des erreurs à travers toute l'application
|
||||
|
||||
## Règles métier
|
||||
|
||||
- [Authentification et inscription](rules/authentification.md)
|
||||
- [Conformité RGPD](rules/rgpd.md)
|
||||
- [Gestion des erreurs](rules/gestion-erreurs.md)
|
||||
- [Annexe Post-MVP](rules/ANNEXE-POST-MVP.md)
|
||||
|
||||
## Modèle de données
|
||||
|
||||
- [Diagramme entités globales](entities/../entities/modele-global.md) - Entités centrales : USERS, CONTENTS, SUBSCRIPTIONS, LISTENING_HISTORY
|
||||
|
||||
## Ubiquitous Language
|
||||
|
||||
**Termes métier du domaine partagé** :
|
||||
- **User** : Utilisateur de la plateforme (auditeur, créateur, ou les deux)
|
||||
- **Content** : Tout contenu audio diffusé sur la plateforme
|
||||
- **Subscription** : Abonnement d'un utilisateur à un créateur ou une catégorie
|
||||
- **Listening History** : Historique d'écoute d'un utilisateur
|
||||
- **Authentication** : Processus de vérification de l'identité via Zitadel
|
||||
- **RGPD Consent** : Consentement explicite pour le traitement des données personnelles
|
||||
|
||||
## Dépendances
|
||||
|
||||
- ✅ Utilisé par : **tous les autres domaines**
|
||||
- ⚠️ Dépend de : aucun (Core Domain)
|
||||
69
docs/domains/_shared/entities/modele-global.md
Normal file
69
docs/domains/_shared/entities/modele-global.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Modèle de données - Entités globales
|
||||
|
||||
📖 Entités de base utilisées dans tous les domaines métier
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ CONTENTS : "crée"
|
||||
USERS ||--o{ SUBSCRIPTIONS : "s'abonne à"
|
||||
USERS ||--o{ LISTENING_HISTORY : "écoute"
|
||||
|
||||
CONTENTS ||--o{ LISTENING_HISTORY : "écouté"
|
||||
CONTENTS }o--|| USERS : "créé par"
|
||||
|
||||
USERS {
|
||||
uuid id PK
|
||||
string email UK
|
||||
string pseudo UK
|
||||
date birthdate
|
||||
string role
|
||||
timestamp created_at
|
||||
boolean email_verified
|
||||
}
|
||||
|
||||
CONTENTS {
|
||||
uuid id PK
|
||||
uuid creator_id FK
|
||||
string title
|
||||
string audio_url
|
||||
string status
|
||||
string age_rating
|
||||
string geo_type
|
||||
point geo_location
|
||||
string[] tags
|
||||
int duration_seconds
|
||||
timestamp published_at
|
||||
boolean is_moderated
|
||||
}
|
||||
|
||||
SUBSCRIPTIONS {
|
||||
uuid id PK
|
||||
uuid subscriber_id FK
|
||||
uuid creator_id FK
|
||||
timestamp subscribed_at
|
||||
}
|
||||
|
||||
LISTENING_HISTORY {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid content_id FK
|
||||
uuid creator_id FK
|
||||
boolean is_subscribed
|
||||
decimal completion_rate
|
||||
int last_position_seconds
|
||||
string source
|
||||
boolean auto_like
|
||||
timestamp listened_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités de base** :
|
||||
|
||||
- **USERS** : Utilisateurs plateforme - Rôles : `listener`, `creator`, `moderator`, `admin`
|
||||
- **CONTENTS** : Contenus audio - Status : `draft`, `pending_review`, `published`, `moderated`, `deleted` - Geo-type : `geo_ancre` (70% geo), `geo_contextuel` (50% geo), `geo_neutre` (20% geo) - Age rating : `all`, `13+`, `16+`, `18+`
|
||||
- **SUBSCRIPTIONS** : Abonnements créateurs - Utilisé pour filtrer recommandations et calculer engagement
|
||||
- **LISTENING_HISTORY** : Historique écoutes - Source : `recommendation`, `search`, `direct_link`, `profile`, `history`, `live_notification`, `audio_guide` - Utilisé pour scoring recommandation et statistiques créateur
|
||||
@@ -0,0 +1,200 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @2fa @security @mvp
|
||||
Fonctionnalité: Appareils de confiance et authentification à deux facteurs
|
||||
|
||||
En tant qu'utilisateur soucieux de la sécurité
|
||||
Je veux gérer mes appareils de confiance et activer l'authentification à deux facteurs
|
||||
Afin de protéger mon compte contre les accès non autorisés
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système supporte les méthodes 2FA suivantes:
|
||||
| Méthode | Disponibilité | Recommandée |
|
||||
| Application TOTP | Oui | Oui |
|
||||
| SMS | Oui | Non |
|
||||
| Email | Oui | Non |
|
||||
| Clés de sécurité USB | Phase 2 | Oui |
|
||||
|
||||
Scénario: Activation de l'authentification à deux facteurs par TOTP
|
||||
Étant donné un utilisateur "alice@roadwave.fr" sans 2FA activé
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Authentification à deux facteurs"
|
||||
Et clique sur "Activer l'authentification à deux facteurs"
|
||||
Alors le système génère un QR code avec secret TOTP
|
||||
Et affiche le secret en texte clair pour saisie manuelle
|
||||
Et affiche les instructions: "Scannez ce QR code avec Google Authenticator, Authy ou Microsoft Authenticator"
|
||||
Et l'utilisateur scanne le QR code avec son application TOTP
|
||||
Et saisit le code à 6 chiffres généré par l'application
|
||||
Alors le 2FA est activé
|
||||
Et 10 codes de récupération à usage unique sont générés
|
||||
Et les codes de récupération sont affichés avec avertissement: "Conservez ces codes en lieu sûr"
|
||||
Et un événement "2FA_ENABLED" est enregistré
|
||||
Et un email de confirmation est envoyé
|
||||
Et la métrique "auth.2fa.enabled" est incrémentée
|
||||
|
||||
Scénario: Connexion avec 2FA depuis un nouvel appareil
|
||||
Étant donné un utilisateur "bob@roadwave.fr" avec 2FA activé
|
||||
Et aucun appareil de confiance enregistré
|
||||
Quand l'utilisateur se connecte depuis un iPhone avec email/mot de passe corrects
|
||||
Alors une page de vérification 2FA s'affiche
|
||||
Et l'utilisateur saisit le code à 6 chiffres de son application TOTP
|
||||
Et coche l'option "Faire confiance à cet appareil pour 30 jours"
|
||||
Alors la connexion est réussie
|
||||
Et l'iPhone est enregistré comme appareil de confiance
|
||||
Et un token d'appareil de confiance est stocké localement (durée: 30 jours)
|
||||
Et un événement "2FA_SUCCESS_NEW_TRUSTED_DEVICE" est enregistré
|
||||
Et un email est envoyé: "Nouvel appareil de confiance ajouté: iPhone"
|
||||
Et la métrique "auth.2fa.trusted_device.added" est incrémentée
|
||||
|
||||
Scénario: Connexion depuis un appareil de confiance existant
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec 2FA activé
|
||||
Et un iPhone enregistré comme appareil de confiance il y a 10 jours
|
||||
Quand l'utilisateur se connecte depuis cet iPhone avec email/mot de passe corrects
|
||||
Alors la connexion est réussie immédiatement sans demander le code 2FA
|
||||
Et un événement "LOGIN_TRUSTED_DEVICE" est enregistré
|
||||
Et la date de dernière utilisation de l'appareil de confiance est mise à jour
|
||||
Et la métrique "auth.2fa.trusted_device.used" est incrémentée
|
||||
|
||||
Scénario: Expiration automatique d'un appareil de confiance après 30 jours
|
||||
Étant donné un utilisateur "david@roadwave.fr" avec 2FA activé
|
||||
Et un iPad enregistré comme appareil de confiance il y a 31 jours
|
||||
Quand l'utilisateur se connecte depuis cet iPad avec email/mot de passe corrects
|
||||
Alors une page de vérification 2FA s'affiche
|
||||
Et l'utilisateur doit saisir le code TOTP
|
||||
Et un message s'affiche: "Votre appareil de confiance a expiré après 30 jours. Veuillez vous authentifier à nouveau."
|
||||
Et l'ancien token d'appareil de confiance est révoqué
|
||||
Et un événement "TRUSTED_DEVICE_EXPIRED" est enregistré
|
||||
Et la métrique "auth.2fa.trusted_device.expired" est incrémentée
|
||||
|
||||
Scénario: Gestion de la liste des appareils de confiance
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec 2FA activé
|
||||
Et 3 appareils de confiance enregistrés
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Appareils de confiance"
|
||||
Alors l'utilisateur voit la liste suivante:
|
||||
| Appareil | Ajouté le | Dernière utilisation | Expire le | Actions |
|
||||
| iPhone 14 Pro | 2026-01-15 | Il y a 2 heures | 2026-02-14 | [Révoquer] |
|
||||
| iPad Air | 2026-01-10 | Il y a 5 jours | 2026-02-09 | [Révoquer] |
|
||||
| MacBook Pro | 2026-01-05 | Il y a 10 jours | 2026-02-04 | [Révoquer] |
|
||||
Et un bouton "Révoquer tous les appareils de confiance" est disponible
|
||||
Et un compteur affiche "3 appareils de confiance actifs"
|
||||
|
||||
Scénario: Révocation manuelle d'un appareil de confiance
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 2FA activé
|
||||
Et un MacBook Pro enregistré comme appareil de confiance
|
||||
Quand l'utilisateur clique sur "Révoquer" pour le MacBook Pro
|
||||
Alors l'appareil de confiance est immédiatement révoqué
|
||||
Et le token d'appareil de confiance est invalidé
|
||||
Et un événement "TRUSTED_DEVICE_REVOKED_MANUAL" est enregistré
|
||||
Et un email est envoyé: "Vous avez révoqué l'appareil de confiance: MacBook Pro"
|
||||
Et lors de la prochaine connexion, le code 2FA sera demandé
|
||||
Et la métrique "auth.2fa.trusted_device.revoked" est incrémentée
|
||||
|
||||
Scénario: Utilisation d'un code de récupération en cas de perte de l'application TOTP
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec 2FA activé
|
||||
Et l'utilisateur a perdu l'accès à son application TOTP
|
||||
Et il possède ses codes de récupération
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects
|
||||
Et clique sur "Utiliser un code de récupération"
|
||||
Et saisit l'un des 10 codes de récupération
|
||||
Alors la connexion est réussie
|
||||
Et le code de récupération utilisé est marqué comme consommé
|
||||
Et il reste 9 codes de récupération disponibles
|
||||
Et un événement "2FA_RECOVERY_CODE_USED" est enregistré
|
||||
Et un email d'alerte est envoyé: "Un code de récupération a été utilisé. Il vous reste 9 codes."
|
||||
Et l'utilisateur est invité à reconfigurer son 2FA
|
||||
Et la métrique "auth.2fa.recovery_code.used" est incrémentée
|
||||
|
||||
Scénario: Régénération des codes de récupération
|
||||
Étant donné un utilisateur "henry@roadwave.fr" avec 2FA activé
|
||||
Et 3 codes de récupération ont été utilisés
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Codes de récupération"
|
||||
Et clique sur "Régénérer les codes de récupération"
|
||||
Alors un message d'avertissement s'affiche: "Les anciens codes seront invalidés. Êtes-vous sûr ?"
|
||||
Et après confirmation, 10 nouveaux codes de récupération sont générés
|
||||
Et les anciens codes sont invalidés immédiatement
|
||||
Et les nouveaux codes sont affichés une seule fois
|
||||
Et un événement "2FA_RECOVERY_CODES_REGENERATED" est enregistré
|
||||
Et un email est envoyé avec les nouveaux codes (chiffrés)
|
||||
Et la métrique "auth.2fa.recovery_codes.regenerated" est incrémentée
|
||||
|
||||
Scénario: Désactivation du 2FA avec vérification renforcée
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec 2FA activé
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Authentification à deux facteurs"
|
||||
Et clique sur "Désactiver l'authentification à deux facteurs"
|
||||
Alors un message d'avertissement s'affiche: "Cela réduira la sécurité de votre compte"
|
||||
Et l'utilisateur doit saisir son mot de passe actuel
|
||||
Et l'utilisateur doit saisir un code 2FA valide
|
||||
Et l'utilisateur doit confirmer par email via un lien sécurisé
|
||||
Alors le 2FA est désactivé
|
||||
Et tous les appareils de confiance sont révoqués
|
||||
Et tous les codes de récupération sont invalidés
|
||||
Et un événement "2FA_DISABLED" est enregistré
|
||||
Et un email de confirmation est envoyé
|
||||
Et la métrique "auth.2fa.disabled" est incrémentée
|
||||
|
||||
Scénario: Authentification 2FA par SMS en méthode de secours
|
||||
Étant donné un utilisateur "jack@roadwave.fr" avec 2FA par TOTP activé
|
||||
Et l'utilisateur a également configuré un numéro de téléphone de secours
|
||||
Quand l'utilisateur se connecte et clique sur "Recevoir un code par SMS"
|
||||
Alors un code à 6 chiffres est envoyé au numéro +33612345678
|
||||
Et l'utilisateur saisit le code reçu par SMS
|
||||
Alors la connexion est réussie
|
||||
Et un événement "2FA_SMS_FALLBACK_USED" est enregistré
|
||||
Et un email d'alerte est envoyé: "Vous avez utilisé la méthode SMS de secours"
|
||||
Et la métrique "auth.2fa.sms.used" est incrémentée
|
||||
|
||||
Scénario: Limitation des tentatives de codes 2FA
|
||||
Étant donné un utilisateur "kate@roadwave.fr" avec 2FA activé
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects
|
||||
Et saisit 5 codes 2FA incorrects consécutivement
|
||||
Alors le compte est temporairement bloqué pour 15 minutes
|
||||
Et un message s'affiche: "Trop de tentatives échouées. Veuillez réessayer dans 15 minutes."
|
||||
Et un email d'alerte est envoyé: "Multiples tentatives échouées de codes 2FA détectées"
|
||||
Et un événement "2FA_TOO_MANY_ATTEMPTS" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "auth.2fa.blocked.too_many_attempts" est incrémentée
|
||||
|
||||
Scénario: Détection de connexion suspecte malgré 2FA valide
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec 2FA activé
|
||||
Et toutes ses connexions habituelles sont depuis la France
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects depuis la Russie
|
||||
Et saisit un code 2FA valide
|
||||
Alors la connexion est réussie mais marquée comme suspecte
|
||||
Et l'utilisateur reçoit immédiatement un email: "Connexion inhabituelle depuis Russie"
|
||||
Et une notification push est envoyée sur tous les appareils de confiance
|
||||
Et l'accès aux fonctionnalités sensibles (paiement, changement de mot de passe) est temporairement bloqué
|
||||
Et l'utilisateur doit confirmer son identité par email avant accès complet
|
||||
Et un événement "2FA_SUSPICIOUS_LOCATION" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "auth.2fa.suspicious_login" est incrémentée
|
||||
|
||||
Scénario: Révocation de tous les appareils de confiance en cas de compromission
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec 2FA activé
|
||||
Et 5 appareils de confiance enregistrés
|
||||
Et l'utilisateur suspecte une compromission de son compte
|
||||
Quand l'utilisateur clique sur "Révoquer tous les appareils de confiance"
|
||||
Alors tous les appareils de confiance sont immédiatement révoqués
|
||||
Et tous les tokens d'appareils de confiance sont invalidés
|
||||
Et toutes les sessions actives sont fermées (sauf la session actuelle)
|
||||
Et un événement "ALL_TRUSTED_DEVICES_REVOKED" est enregistré avec niveau "HIGH"
|
||||
Et un email de confirmation est envoyé
|
||||
Et l'utilisateur devra saisir un code 2FA à chaque nouvelle connexion
|
||||
Et la métrique "auth.2fa.trusted_device.bulk_revoked" est incrémentée
|
||||
|
||||
Scénario: Métriques de sécurité pour le 2FA
|
||||
Étant donné que le système gère 50 000 utilisateurs avec 2FA activé
|
||||
Quand les métriques de sécurité sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Pourcentage d'utilisateurs avec 2FA | > 60% |
|
||||
| Taux de succès de validation 2FA | > 98% |
|
||||
| Temps moyen de saisie du code 2FA | < 15s |
|
||||
| Nombre d'appareils de confiance par user | Moyenne: 2.5 |
|
||||
| Taux d'utilisation des codes de récup. | < 0.5% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si le taux de succès < 95%
|
||||
|
||||
Scénario: Badge de sécurité pour utilisateurs avec 2FA activé
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec 2FA activé depuis 30 jours
|
||||
Quand l'utilisateur consulte son profil public
|
||||
Alors un badge "Compte sécurisé" s'affiche sur son profil
|
||||
Et le badge indique: "Cet utilisateur a activé l'authentification à deux facteurs"
|
||||
Et le badge améliore la visibilité et la crédibilité du créateur de contenu
|
||||
Et la métrique "profile.badge.2fa_secured" est visible
|
||||
@@ -0,0 +1,120 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Classification des contenus par âge
|
||||
En tant que plateforme responsable
|
||||
Je veux classifier les contenus par tranche d'âge
|
||||
Afin de protéger les mineurs et respecter les obligations légales
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Créateur doit classifier son contenu à la publication
|
||||
Étant donné que je suis un créateur connecté
|
||||
Quand je crée un nouveau contenu audio
|
||||
Alors je dois obligatoirement choisir une classification d'âge parmi:
|
||||
| classification | description |
|
||||
| Tout public | Contenu adapté à tous les âges |
|
||||
| 13+ | Contenu mature léger |
|
||||
| 16+ | Contenu mature |
|
||||
| 18+ | Contenu adulte |
|
||||
|
||||
Scénario: Publication impossible sans classification
|
||||
Étant donné que je crée un contenu audio
|
||||
Quand j'essaie de publier sans sélectionner de classification
|
||||
Alors la publication échoue
|
||||
Et je vois le message "Vous devez sélectionner une classification d'âge"
|
||||
|
||||
Scénario: Utilisateur 13-15 ans voit "Tout public" et "13+"
|
||||
Étant donné que je suis un utilisateur de 14 ans
|
||||
Et qu'il existe des contenus avec les classifications suivantes:
|
||||
| classification | nombre |
|
||||
| Tout public | 20 |
|
||||
| 13+ | 15 |
|
||||
| 16+ | 10 |
|
||||
| 18+ | 5 |
|
||||
Quand je demande des recommandations
|
||||
Alors je vois 35 contenus (Tout public + 13+)
|
||||
Et les contenus 16+ et 18+ ne sont jamais proposés
|
||||
|
||||
Scénario: Utilisateur 16-17 ans voit "Tout public", "13+" et "16+"
|
||||
Étant donné que je suis un utilisateur de 17 ans
|
||||
Et qu'il existe des contenus avec les classifications suivantes:
|
||||
| classification | nombre |
|
||||
| Tout public | 20 |
|
||||
| 13+ | 15 |
|
||||
| 16+ | 10 |
|
||||
| 18+ | 5 |
|
||||
Quand je demande des recommandations
|
||||
Alors je vois 45 contenus (Tout public + 13+ + 16+)
|
||||
Et les contenus 18+ ne sont pas proposés
|
||||
|
||||
Scénario: Utilisateur 18+ voit tous les contenus
|
||||
Étant donné que je suis un utilisateur de 25 ans
|
||||
Et qu'il existe des contenus avec toutes les classifications
|
||||
Quand je demande des recommandations
|
||||
Alors je vois tous les contenus sans restriction
|
||||
Et aucun filtre d'âge n'est appliqué
|
||||
|
||||
Scénario: Inscription réussie à 13 ans pile - accès limité à "Tout public" et "13+"
|
||||
Étant donné que je m'inscris avec une date de naissance "2013-01-21"
|
||||
Alors mon compte est créé avec succès
|
||||
Et je peux voir les contenus "Tout public" et "13+"
|
||||
Et les contenus 16+ et 18+ ne sont pas accessibles
|
||||
|
||||
Scénario: Modérateur reclassifie un contenu mal catégorisé
|
||||
Étant donné qu'un contenu est publié avec la classification "Tout public"
|
||||
Et que ce contenu contient du langage inapproprié détecté en modération
|
||||
Quand le modérateur reclassifie ce contenu en "16+"
|
||||
Alors la nouvelle classification est appliquée immédiatement
|
||||
Et le contenu n'est plus visible pour les utilisateurs de moins de 16 ans
|
||||
Et le créateur reçoit une notification de reclassification
|
||||
|
||||
Scénario: Strike si classification volontairement incorrecte
|
||||
Étant donné qu'un créateur a publié un contenu "18+" classifié comme "Tout public"
|
||||
Et que ce contenu a été signalé
|
||||
Quand le modérateur confirme la mauvaise classification volontaire
|
||||
Alors le créateur reçoit 1 strike
|
||||
Et le contenu est reclassifié en "18+"
|
||||
Et le créateur reçoit une notification explicative
|
||||
|
||||
Scénario: Créateur peut voir la distribution d'âge de son audience
|
||||
Étant donné que je suis un créateur
|
||||
Et que j'ai publié des contenus avec différentes classifications
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois la répartition des âges de mes auditeurs:
|
||||
| tranche_age | pourcentage |
|
||||
| 13-15 ans | 15% |
|
||||
| 16-17 ans | 20% |
|
||||
| 18+ ans | 65% |
|
||||
|
||||
Scénario: Recherche filtrée par classification d'âge
|
||||
Étant donné que je suis un utilisateur de 16 ans
|
||||
Quand je recherche des contenus
|
||||
Alors les résultats incluent uniquement:
|
||||
| classification |
|
||||
| Tout public |
|
||||
| 13+ |
|
||||
| 16+ |
|
||||
Et je ne vois pas les contenus 18+ dans les résultats
|
||||
|
||||
Scénario: Notification si tentative d'accès à contenu non autorisé
|
||||
Étant donné que je suis un utilisateur de 15 ans
|
||||
Et qu'un contenu "16+" est partagé avec moi via un lien direct
|
||||
Quand j'essaie d'accéder au contenu
|
||||
Alors l'accès est refusé
|
||||
Et je vois le message "Ce contenu est réservé aux utilisateurs de 16 ans et plus"
|
||||
|
||||
Scénario: Validation obligatoire des 3 premiers contenus inclut la classification
|
||||
Étant donné que je suis un nouveau créateur
|
||||
Et que je publie mon premier contenu classifié "18+"
|
||||
Quand le modérateur valide mon contenu
|
||||
Alors il vérifie que la classification "18+" est appropriée
|
||||
Et peut la modifier si nécessaire avant validation
|
||||
|
||||
Scénario: Statistiques de classification dans l'interface créateur
|
||||
Étant donné que je suis un créateur
|
||||
Quand je consulte mes contenus publiés
|
||||
Alors je vois pour chaque contenu:
|
||||
| information | exemple |
|
||||
| Classification actuelle | 13+ |
|
||||
| Nombre de signalements | 2 |
|
||||
| Reclassifications | Aucune / 1× par modérateur |
|
||||
@@ -0,0 +1,84 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Connexion utilisateur
|
||||
En tant qu'utilisateur existant
|
||||
Je veux me connecter à mon compte
|
||||
Afin d'accéder à mes contenus et paramètres
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur existe avec:
|
||||
| email | mot_de_passe |
|
||||
| user@test.fr | Password123 |
|
||||
|
||||
Scénario: Connexion réussie avec identifiants valides
|
||||
Quand je me connecte avec:
|
||||
| email | mot_de_passe |
|
||||
| user@test.fr | Password123 |
|
||||
Alors je suis connecté avec succès
|
||||
Et je reçois un access token valide pour 15 minutes
|
||||
Et je reçois un refresh token valide pour 30 jours
|
||||
|
||||
Scénario: Connexion échouée avec email inexistant
|
||||
Quand je me connecte avec l'email "inexistant@test.fr"
|
||||
Alors la connexion échoue
|
||||
Et je vois le message "Email ou mot de passe incorrect"
|
||||
|
||||
Scénario: Connexion échouée avec mot de passe incorrect
|
||||
Quand je me connecte avec:
|
||||
| email | mot_de_passe |
|
||||
| user@test.fr | MauvaisPass1 |
|
||||
Alors la connexion échoue
|
||||
Et je vois le message "Email ou mot de passe incorrect"
|
||||
|
||||
Scénario: Blocage après 5 tentatives échouées
|
||||
Étant donné que j'ai échoué 4 tentatives de connexion
|
||||
Quand j'échoue une 5ème tentative de connexion
|
||||
Alors mon compte est temporairement bloqué
|
||||
Et je vois le message "Compte bloqué pour 15 minutes après 5 tentatives échouées"
|
||||
Et je reçois un email de notification de blocage
|
||||
|
||||
Scénario: Tentative de connexion pendant le blocage
|
||||
Étant donné que mon compte est bloqué suite à 5 tentatives échouées
|
||||
Et que seulement 5 minutes se sont écoulées
|
||||
Quand j'essaie de me connecter avec les bons identifiants
|
||||
Alors la connexion échoue
|
||||
Et je vois le message "Compte bloqué. Réessayez dans 10 minutes"
|
||||
|
||||
Scénario: Déblocage automatique après 15 minutes
|
||||
Étant donné que mon compte est bloqué suite à 5 tentatives échouées
|
||||
Et que 15 minutes se sont écoulées
|
||||
Quand je me connecte avec les bons identifiants
|
||||
Alors je suis connecté avec succès
|
||||
Et le compteur de tentatives est réinitialisé
|
||||
|
||||
Scénario: Reset du compteur après connexion réussie
|
||||
Étant donné que j'ai échoué 3 tentatives de connexion
|
||||
Quand je me connecte avec les bons identifiants
|
||||
Alors je suis connecté avec succès
|
||||
Et le compteur de tentatives est remis à 0
|
||||
|
||||
Scénario: Reset automatique du compteur après 15 minutes sans blocage
|
||||
Étant donné que j'ai échoué 3 tentatives de connexion
|
||||
Et que 15 minutes se sont écoulées sans nouvelle tentative
|
||||
Quand je consulte mon compteur de tentatives
|
||||
Alors le compteur est réinitialisé à 0
|
||||
|
||||
Scénario: Déblocage via lien "Mot de passe oublié"
|
||||
Étant donné que mon compte est bloqué suite à 5 tentatives échouées
|
||||
Quand j'utilise la fonction "Mot de passe oublié"
|
||||
Et que je réinitialise mon mot de passe
|
||||
Alors le blocage est levé immédiatement
|
||||
Et je peux me connecter avec le nouveau mot de passe
|
||||
|
||||
Scénario: Email de notification lors d'un blocage
|
||||
Étant donné que j'ai échoué 5 tentatives de connexion
|
||||
Alors je reçois un email avec:
|
||||
| sujet | Tentatives de connexion suspectes détectées |
|
||||
| contenu_contient | Votre compte a été temporairement bloqué |
|
||||
| lien_mot_de_passe | présent |
|
||||
|
||||
Scénario: Connexion multi-device simultanée autorisée
|
||||
Étant donné que je suis connecté sur un appareil iOS
|
||||
Quand je me connecte également sur un appareil Android
|
||||
Alors les deux sessions sont actives simultanément
|
||||
Et je peux utiliser l'application sur les deux appareils
|
||||
@@ -0,0 +1,199 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion de compte utilisateur
|
||||
En tant qu'utilisateur connecté
|
||||
Je veux gérer les paramètres de mon compte
|
||||
Afin de maintenir la sécurité et l'exactitude de mes informations
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté avec:
|
||||
| email | user@test.fr |
|
||||
| mot_de_passe | Password123 |
|
||||
|
||||
# ==========================================
|
||||
# Déconnexion
|
||||
# ==========================================
|
||||
|
||||
Scénario: Déconnexion volontaire de l'appareil actuel
|
||||
Quand je clique sur "Se déconnecter"
|
||||
Alors ma session est invalidée immédiatement
|
||||
Et mon refresh token est révoqué
|
||||
Et je suis redirigé vers l'écran de connexion
|
||||
Et je dois me reconnecter pour accéder à l'application
|
||||
|
||||
Scénario: Déconnexion ne révoque pas les autres appareils
|
||||
Étant donné que je suis connecté sur mon iPhone et mon iPad
|
||||
Quand je me déconnecte depuis mon iPhone
|
||||
Alors la session iPhone est invalidée
|
||||
Et ma session iPad reste active
|
||||
Et je peux continuer à utiliser l'application sur iPad
|
||||
|
||||
# ==========================================
|
||||
# Changement de mot de passe
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement de mot de passe avec ancien mot de passe correct
|
||||
Quand je change mon mot de passe depuis les paramètres avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | NewPass456 |
|
||||
| confirmation | NewPass456 |
|
||||
Alors mon mot de passe est modifié avec succès
|
||||
Et je reste connecté sur cet appareil
|
||||
Et tous les autres appareils sont déconnectés
|
||||
Et je reçois un email de confirmation de changement
|
||||
Et je vois le message "Mot de passe modifié avec succès"
|
||||
|
||||
Scénario: Changement de mot de passe avec ancien mot de passe incorrect
|
||||
Quand je change mon mot de passe avec un ancien mot de passe incorrect "WrongPass123"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Ancien mot de passe incorrect"
|
||||
Et mon mot de passe actuel reste inchangé
|
||||
|
||||
Scénario: Changement de mot de passe avec nouveau mot de passe invalide
|
||||
Quand je change mon mot de passe avec un nouveau mot de passe "faible"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins 8 caractères, 1 majuscule et 1 chiffre"
|
||||
|
||||
Scénario: Changement de mot de passe avec confirmation non correspondante
|
||||
Quand je change mon mot de passe avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | NewPass456 |
|
||||
| confirmation | DiffPass789 |
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Les mots de passe ne correspondent pas"
|
||||
|
||||
Scénario: Nouveau mot de passe identique à l'ancien
|
||||
Quand je change mon mot de passe avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | Password123 |
|
||||
| confirmation | Password123 |
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le nouveau mot de passe doit être différent de l'ancien"
|
||||
|
||||
Scénario: Notification sur tous les appareils après changement de mot de passe
|
||||
Étant donné que je suis connecté sur 3 appareils différents
|
||||
Quand je change mon mot de passe depuis mon iPhone
|
||||
Alors je reçois une notification push sur mes 2 autres appareils
|
||||
Et je reçois un email de confirmation avec:
|
||||
| sujet | Votre mot de passe a été modifié |
|
||||
| appareil | iPhone 13 - Safari |
|
||||
| localisation | Paris, France |
|
||||
| action_urgence | Lien pour sécuriser le compte |
|
||||
|
||||
# ==========================================
|
||||
# Changement d'email
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement d'email avec vérification
|
||||
Quand je change mon email pour "nouveau@test.fr"
|
||||
Alors un email de vérification est envoyé à "nouveau@test.fr"
|
||||
Et mon ancien email "user@test.fr" reste actif pour la connexion
|
||||
Et je vois le message "Email de vérification envoyé à nouveau@test.fr"
|
||||
Et le lien de vérification expire dans 7 jours
|
||||
|
||||
Scénario: Validation du changement d'email
|
||||
Étant donné que j'ai demandé un changement d'email pour "nouveau@test.fr"
|
||||
Et que j'ai reçu le lien de vérification
|
||||
Quand je clique sur le lien de vérification dans l'email
|
||||
Alors mon email est changé pour "nouveau@test.fr"
|
||||
Et je reçois une notification sur l'ancien email "user@test.fr"
|
||||
Et je vois le message "Email modifié avec succès"
|
||||
Et je dois utiliser "nouveau@test.fr" pour me connecter désormais
|
||||
|
||||
Scénario: Changement d'email vers un email déjà utilisé
|
||||
Étant donné qu'un utilisateur existe avec l'email "existant@test.fr"
|
||||
Quand j'essaie de changer mon email pour "existant@test.fr"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Cet email est déjà utilisé par un autre compte"
|
||||
|
||||
Scénario: Changement d'email avec format invalide
|
||||
Quand j'essaie de changer mon email pour "email.invalide"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Format d'email invalide"
|
||||
|
||||
Scénario: Expiration du lien de vérification de changement d'email
|
||||
Étant donné que j'ai demandé un changement d'email il y a 8 jours
|
||||
Quand j'essaie d'utiliser le lien de vérification
|
||||
Alors la vérification échoue
|
||||
Et je vois le message "Ce lien a expiré"
|
||||
Et mon email reste inchangé à "user@test.fr"
|
||||
Et je peux demander un nouveau changement d'email
|
||||
|
||||
Scénario: Annulation du changement d'email avant vérification
|
||||
Étant donné que j'ai demandé un changement d'email pour "nouveau@test.fr"
|
||||
Et que je n'ai pas encore vérifié le nouveau email
|
||||
Quand je demande à annuler le changement d'email
|
||||
Alors la demande de changement est annulée
|
||||
Et le lien de vérification est invalidé
|
||||
Et mon email reste "user@test.fr"
|
||||
|
||||
Scénario: Limite de changements d'email
|
||||
Étant donné que j'ai déjà changé mon email 2 fois dans les 30 derniers jours
|
||||
Quand j'essaie de changer mon email une 3ème fois
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Maximum 2 changements d'email par mois"
|
||||
|
||||
Scénario: Notification de sécurité sur l'ancien email
|
||||
Étant donné que j'ai changé mon email de "ancien@test.fr" à "nouveau@test.fr"
|
||||
Alors je reçois un email sur "ancien@test.fr" avec:
|
||||
| sujet | Votre adresse email a été modifiée |
|
||||
| contenu | Votre email de connexion est maintenant nouveau@test.fr |
|
||||
| date_heure | présente |
|
||||
| appareil | présent |
|
||||
| action_urgence | Lien pour annuler si ce n'était pas vous |
|
||||
|
||||
# ==========================================
|
||||
# Changement de pseudo
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement de pseudo valide
|
||||
Quand je change mon pseudo pour "nouveau_pseudo"
|
||||
Alors mon pseudo est modifié avec succès
|
||||
Et je vois le message "Pseudo modifié avec succès"
|
||||
Et le nouveau pseudo apparaît sur mon profil
|
||||
|
||||
Scénario: Changement de pseudo invalide - trop court
|
||||
Quand j'essaie de changer mon pseudo pour "ab"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le pseudo doit contenir entre 3 et 30 caractères"
|
||||
|
||||
Scénario: Changement de pseudo invalide - caractères spéciaux
|
||||
Quand j'essaie de changer mon pseudo pour "user@123"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le pseudo ne peut contenir que des lettres, chiffres et underscores"
|
||||
|
||||
Scénario: Changement de pseudo déjà utilisé
|
||||
Étant donné qu'un utilisateur existe avec le pseudo "pseudo_existant"
|
||||
Quand j'essaie de changer mon pseudo pour "pseudo_existant"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Ce pseudo est déjà utilisé"
|
||||
|
||||
Scénario: Limite de changements de pseudo
|
||||
Étant donné que j'ai changé mon pseudo il y a 15 jours
|
||||
Quand j'essaie de changer mon pseudo à nouveau
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Vous ne pouvez changer votre pseudo qu'une fois par mois"
|
||||
|
||||
# ==========================================
|
||||
# Consultation des informations de compte
|
||||
# ==========================================
|
||||
|
||||
Scénario: Consulter les informations de mon compte
|
||||
Quand je consulte les paramètres de mon compte
|
||||
Alors je vois les informations suivantes:
|
||||
| champ | valeur |
|
||||
| Email | user@test.fr |
|
||||
| Pseudo | user_test |
|
||||
| Date création | 15/01/2026 |
|
||||
| Email vérifié | Oui |
|
||||
| 2FA activée | Non |
|
||||
| Abonnement | Gratuit |
|
||||
|
||||
Scénario: Historique des changements de sécurité
|
||||
Quand je consulte l'historique de sécurité de mon compte
|
||||
Alors je vois la liste des événements suivants:
|
||||
| événement | date | appareil |
|
||||
| Changement mot de passe | 01/02/2026 | iPhone 13 |
|
||||
| Activation 2FA | 25/01/2026 | iPad Pro |
|
||||
| Changement email | 20/01/2026 | PC Windows |
|
||||
| Création compte | 15/01/2026 | iPhone 13 |
|
||||
112
docs/domains/_shared/features/authentication/inscription.feature
Normal file
112
docs/domains/_shared/features/authentication/inscription.feature
Normal file
@@ -0,0 +1,112 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Inscription utilisateur
|
||||
En tant que nouvel utilisateur
|
||||
Je veux créer un compte avec email et mot de passe
|
||||
Afin d'accéder à l'application RoadWave
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que Zitadel est configuré
|
||||
|
||||
Scénario: Inscription réussie avec données valides
|
||||
Étant donné que l'email "nouveau@example.com" n'existe pas
|
||||
Quand je m'inscris avec les données suivantes:
|
||||
| champ | valeur |
|
||||
| email | nouveau@example.com |
|
||||
| mot_de_passe | Password123 |
|
||||
| pseudo | nouveau_user |
|
||||
| date_naissance | 1995-06-15 |
|
||||
Alors mon compte est créé avec succès
|
||||
Et je reçois un email de vérification
|
||||
Et le lien de vérification expire dans 7 jours
|
||||
Et je suis redirigé vers l'application
|
||||
|
||||
Scénario: Inscription avec email déjà existant
|
||||
Étant donné qu'un utilisateur existe avec l'email "existant@example.com"
|
||||
Quand je m'inscris avec l'email "existant@example.com"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Cet email est déjà utilisé"
|
||||
|
||||
Scénario: Inscription avec mot de passe invalide - trop court
|
||||
Quand je m'inscris avec un mot de passe de moins de 8 caractères "Pass1"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins 8 caractères"
|
||||
|
||||
Scénario: Inscription avec mot de passe invalide - sans majuscule
|
||||
Quand je m'inscris avec un mot de passe sans majuscule "password123"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins une majuscule"
|
||||
|
||||
Scénario: Inscription avec mot de passe invalide - sans chiffre
|
||||
Quand je m'inscris avec un mot de passe sans chiffre "Password"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins un chiffre"
|
||||
|
||||
Scénario: Inscription avec pseudo invalide - trop court
|
||||
Quand je m'inscris avec un pseudo de 2 caractères "ab"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Le pseudo doit contenir entre 3 et 30 caractères"
|
||||
|
||||
Scénario: Inscription avec pseudo invalide - caractères spéciaux
|
||||
Quand je m'inscris avec un pseudo contenant des caractères spéciaux "user@123"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Le pseudo ne peut contenir que des lettres, chiffres et underscores"
|
||||
|
||||
Scénario: Inscription avec email invalide
|
||||
Quand je m'inscris avec un email invalide "email.invalide"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Format d'email invalide"
|
||||
|
||||
Plan du Scénario: Inscription avec âge minimum non respecté
|
||||
Étant donné la date du jour est "2026-01-21"
|
||||
Quand je m'inscris avec une date de naissance "<date_naissance>"
|
||||
Alors l'inscription échoue
|
||||
Et je vois le message "Vous devez avoir au moins 13 ans pour créer un compte"
|
||||
|
||||
Exemples:
|
||||
| date_naissance | age |
|
||||
| 2013-01-22 | 12 |
|
||||
| 2015-06-15 | 10 |
|
||||
| 2020-01-01 | 6 |
|
||||
|
||||
Scénario: Inscription avec âge limite acceptable (13 ans)
|
||||
Étant donné la date du jour est "2026-01-21"
|
||||
Quand je m'inscris avec une date de naissance "2013-01-21"
|
||||
Alors mon compte est créé avec succès
|
||||
Et je peux accéder aux contenus "Tout public" et "13+"
|
||||
|
||||
Scénario: Inscription avec âge supérieur à 18 ans
|
||||
Étant donné la date du jour est "2026-01-21"
|
||||
Quand je m'inscris avec une date de naissance "1990-06-15"
|
||||
Alors mon compte est créé avec succès
|
||||
Et j'ai accès à tous les contenus sans restriction d'âge
|
||||
|
||||
Scénario: Données minimales requises à l'inscription
|
||||
Quand je m'inscris sans fournir de nom complet
|
||||
Et sans fournir de photo de profil
|
||||
Et sans fournir de bio
|
||||
Alors mon compte est créé avec succès
|
||||
Et un avatar par défaut est généré
|
||||
Et les champs optionnels sont vides
|
||||
|
||||
Scénario: Renvoyer l'email de vérification
|
||||
Étant donné que je me suis inscrit avec l'email "nouveau@example.com"
|
||||
Et que je n'ai pas vérifié mon email
|
||||
Quand je demande à renvoyer l'email de vérification
|
||||
Alors un nouvel email de vérification est envoyé
|
||||
Et le précédent lien est invalidé
|
||||
|
||||
Scénario: Limite de renvoi d'email de vérification
|
||||
Étant donné que je me suis inscrit avec l'email "nouveau@example.com"
|
||||
Et que j'ai déjà renvoyé l'email de vérification 3 fois aujourd'hui
|
||||
Quand je demande à renvoyer l'email de vérification une 4ème fois
|
||||
Alors la demande échoue
|
||||
Et je vois le message "Vous avez atteint la limite de 3 renvois par jour"
|
||||
|
||||
Scénario: Expiration du lien de vérification
|
||||
Étant donné que je me suis inscrit il y a 8 jours
|
||||
Et que je n'ai pas vérifié mon email
|
||||
Quand j'essaie d'utiliser le lien de vérification
|
||||
Alors la vérification échoue
|
||||
Et je vois le message "Ce lien a expiré"
|
||||
Et je peux demander un nouveau lien
|
||||
@@ -0,0 +1,171 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Limitation des tentatives de connexion
|
||||
|
||||
En tant que système de sécurité
|
||||
Je veux limiter les tentatives de connexion échouées
|
||||
Afin de protéger les comptes utilisateurs contre les attaques par force brute
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système est configuré avec les limites suivantes:
|
||||
| Paramètre | Valeur |
|
||||
| Tentatives max avant blocage | 5 |
|
||||
| Durée de blocage temporaire | 15 min |
|
||||
| Tentatives max avant blocage 24h | 10 |
|
||||
| Durée de blocage prolongé | 24h |
|
||||
| Fenêtre de temps pour reset | 30 min |
|
||||
|
||||
Scénario: Connexion réussie réinitialise le compteur de tentatives
|
||||
Étant donné un utilisateur "alice@roadwave.fr" avec 3 tentatives échouées
|
||||
Quand l'utilisateur se connecte avec les bons identifiants
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées est réinitialisé à 0
|
||||
Et un événement de sécurité "LOGIN_SUCCESS_AFTER_FAILURES" est enregistré
|
||||
|
||||
Scénario: Blocage temporaire après 5 tentatives échouées
|
||||
Étant donné un utilisateur "bob@roadwave.fr" avec 4 tentatives échouées
|
||||
Quand l'utilisateur tente de se connecter avec un mauvais mot de passe
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_TEMPORARILY_LOCKED"
|
||||
Et le message est "Votre compte est temporairement verrouillé pour 15 minutes suite à de multiples tentatives échouées"
|
||||
Et un email de notification de sécurité est envoyé à "bob@roadwave.fr"
|
||||
Et un événement de sécurité "ACCOUNT_LOCKED_TEMP" est enregistré
|
||||
Et la métrique "security.account_locks.temporary" est incrémentée
|
||||
|
||||
Scénario: Tentative de connexion pendant le blocage temporaire
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" bloqué temporairement
|
||||
Et il reste 10 minutes avant la fin du blocage
|
||||
Quand l'utilisateur tente de se connecter avec les bons identifiants
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_TEMPORARILY_LOCKED"
|
||||
Et le message contient "Votre compte reste verrouillé pour 10 minutes"
|
||||
Et le temps de blocage restant est indiqué en minutes
|
||||
Et la tentative ne rallonge pas la durée du blocage
|
||||
|
||||
Scénario: Connexion autorisée après expiration du blocage temporaire
|
||||
Étant donné un utilisateur "david@roadwave.fr" bloqué temporairement il y a 16 minutes
|
||||
Quand l'utilisateur tente de se connecter avec les bons identifiants
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées est réinitialisé à 0
|
||||
Et le statut de blocage est levé
|
||||
Et un événement de sécurité "ACCOUNT_UNLOCKED_AUTO" est enregistré
|
||||
|
||||
Scénario: Blocage prolongé après 10 tentatives échouées sur 24h
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec historique:
|
||||
| Tentatives échouées | Quand |
|
||||
| 5 | Il y a 2 heures |
|
||||
| Blocage 15min levé | Il y a 1h30 |
|
||||
| 4 | Il y a 30 minutes |
|
||||
Quand l'utilisateur tente une nouvelle connexion échouée
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_LOCKED_24H"
|
||||
Et le message est "Votre compte est verrouillé pour 24 heures suite à de multiples tentatives suspectes"
|
||||
Et un email urgent de sécurité est envoyé avec un lien de déblocage sécurisé
|
||||
Et une notification SMS est envoyée (si configuré)
|
||||
Et un événement de sécurité "ACCOUNT_LOCKED_24H" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "security.account_locks.prolonged" est incrémentée
|
||||
|
||||
Scénario: Blocage différencié par adresse IP
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 3 tentatives échouées depuis IP "1.2.3.4"
|
||||
Quand l'utilisateur se connecte avec succès depuis IP "5.6.7.8"
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées pour IP "1.2.3.4" reste à 3
|
||||
Et le compteur de tentatives échouées pour IP "5.6.7.8" est à 0
|
||||
Et un événement de sécurité "LOGIN_FROM_NEW_IP" est enregistré
|
||||
|
||||
Scénario: Alerte de sécurité sur pattern suspect multi-IP
|
||||
Étant donné un utilisateur "grace@roadwave.fr"
|
||||
Quand 5 tentatives échouées sont détectées depuis 5 IP différentes en 10 minutes:
|
||||
| IP | Tentatives | Timestamp |
|
||||
| 1.2.3.4 | 2 | Il y a 10 min |
|
||||
| 5.6.7.8 | 1 | Il y a 8 min |
|
||||
| 9.10.11.12 | 1 | Il y a 5 min |
|
||||
| 13.14.15.16| 1 | Il y a 2 min |
|
||||
Alors le compte est immédiatement bloqué pour 24h
|
||||
Et un email d'alerte critique "POSSIBLE_CREDENTIAL_STUFFING_ATTACK" est envoyé
|
||||
Et l'équipe de sécurité est notifiée via webhook
|
||||
Et toutes les sessions actives sont révoquées
|
||||
Et la métrique "security.attacks.credential_stuffing.detected" est incrémentée
|
||||
|
||||
Scénario: Déblocage manuel par l'utilisateur via email sécurisé
|
||||
Étant donné un utilisateur "henry@roadwave.fr" bloqué pour 24h
|
||||
Et il a reçu un email avec un lien de déblocage sécurisé à usage unique
|
||||
Quand l'utilisateur clique sur le lien dans les 2 heures suivant l'email
|
||||
Et confirme son identité via un code envoyé par SMS
|
||||
Alors le compte est débloqué immédiatement
|
||||
Et l'utilisateur est invité à changer son mot de passe
|
||||
Et un événement de sécurité "ACCOUNT_UNLOCKED_MANUAL" est enregistré
|
||||
Et la métrique "security.account_unlocks.user_initiated" est incrémentée
|
||||
|
||||
Scénario: Réinitialisation automatique du compteur après période d'inactivité
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec 3 tentatives échouées
|
||||
Et aucune nouvelle tentative depuis 35 minutes
|
||||
Quand l'utilisateur tente de se connecter avec un mauvais mot de passe
|
||||
Alors le compteur de tentatives est réinitialisé à 1
|
||||
Et le message d'erreur est standard sans mention de blocage imminent
|
||||
Et un événement de sécurité "ATTEMPT_COUNTER_RESET" est enregistré
|
||||
|
||||
Scénario: Protection contre les attaques par timing
|
||||
Étant donné un utilisateur "jack@roadwave.fr"
|
||||
Quand l'utilisateur effectue 10 tentatives de connexion échouées
|
||||
Alors chaque réponse HTTP prend entre 800ms et 1200ms (temps constant)
|
||||
Et les messages d'erreur ne révèlent pas si l'email existe
|
||||
Et la métrique "security.timing_protection.applied" est incrémentée
|
||||
Et les logs n'exposent pas de patterns de timing exploitables
|
||||
|
||||
Scénario: Escalade des notifications avec tentatives répétées
|
||||
Étant donné un utilisateur "kate@roadwave.fr" Premium
|
||||
Quand les événements suivants se produisent:
|
||||
| Événement | Notification |
|
||||
| 3 tentatives échouées | Aucune notification |
|
||||
| 5 tentatives (blocage) | Email standard |
|
||||
| 10 tentatives (24h) | Email + SMS + notification app|
|
||||
| Tentative pendant 24h | Email urgent + alerte support |
|
||||
Alors chaque niveau de notification est proportionnel à la gravité
|
||||
Et l'utilisateur peut configurer ses préférences de notification
|
||||
Et la métrique "security.notifications.escalated" est incrémentée
|
||||
|
||||
Scénario: Whitelist d'IP pour utilisateurs de confiance
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec IP de confiance "1.2.3.4"
|
||||
Et la whitelist est configurée pour autoriser 10 tentatives au lieu de 5
|
||||
Quand l'utilisateur effectue 7 tentatives échouées depuis "1.2.3.4"
|
||||
Alors le compte n'est pas bloqué
|
||||
Et un avertissement est affiché "3 tentatives restantes avant blocage"
|
||||
Et un événement de sécurité "TRUSTED_IP_EXTENDED_ATTEMPTS" est enregistré
|
||||
|
||||
Scénario: Logs de sécurité détaillés pour audit
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec tentatives échouées
|
||||
Quand un audit de sécurité est effectué
|
||||
Alors les logs contiennent pour chaque tentative:
|
||||
| Champ | Exemple |
|
||||
| Timestamp | 2026-02-03T14:32:18.123Z |
|
||||
| User ID | uuid-123-456 |
|
||||
| Email | mary@roadwave.fr |
|
||||
| IP Address | 1.2.3.4 |
|
||||
| User Agent | Mozilla/5.0 (iPhone...) |
|
||||
| Failure Reason | INVALID_PASSWORD |
|
||||
| Attempts Count | 3 |
|
||||
| Geolocation | Paris, France |
|
||||
| Device Fingerprint| hash-abc-def |
|
||||
Et les logs sont conservés pendant 90 jours minimum
|
||||
Et les logs sont conformes RGPD (pas de mots de passe en clair)
|
||||
|
||||
Scénario: Métriques de performance du système de limitation
|
||||
Étant donné que le système traite 1000 tentatives de connexion par minute
|
||||
Quand les métriques de performance sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps de vérification du compteur | < 50ms |
|
||||
| Latence ajoutée par le rate limiting | < 100ms |
|
||||
| Pourcentage de tentatives bloquées | < 2% |
|
||||
| Faux positifs (utilisateurs légitimes) | < 0.1% |
|
||||
| Temps de déblocage automatique | < 1s |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si les seuils sont dépassés
|
||||
|
||||
Scénario: Compatibilité avec authentification multi-facteurs
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec 2FA activé
|
||||
Et il a 4 tentatives échouées (mot de passe correct mais code 2FA incorrect)
|
||||
Quand l'utilisateur tente une 5ème connexion avec mot de passe correct et mauvais code 2FA
|
||||
Alors le compte est bloqué temporairement
|
||||
Et le message précise "Blocage suite à de multiples erreurs de code 2FA"
|
||||
Et le compteur 2FA est distinct du compteur de mot de passe
|
||||
Et un événement de sécurité "2FA_LOCK_TRIGGERED" est enregistré
|
||||
@@ -0,0 +1,191 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @sessions @mvp
|
||||
Fonctionnalité: Gestion des sessions multi-appareils
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux gérer mes sessions actives sur plusieurs appareils
|
||||
Afin de contrôler l'accès à mon compte et améliorer la sécurité
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système supporte les sessions suivantes:
|
||||
| Paramètre | Valeur |
|
||||
| Nombre max de sessions simultanées | 5 |
|
||||
| Durée de vie d'une session | 30 jours |
|
||||
| Durée d'inactivité avant expiration | 7 jours |
|
||||
| Durée du token de refresh | 90 jours |
|
||||
| Taille max du stockage de session | 10 KB |
|
||||
|
||||
Scénario: Création d'une nouvelle session avec empreinte d'appareil
|
||||
Étant donné un utilisateur "alice@roadwave.fr" non connecté
|
||||
Quand l'utilisateur se connecte depuis un iPhone 14 Pro avec iOS 17.2
|
||||
Alors une nouvelle session est créée avec les métadonnées:
|
||||
| Champ | Valeur |
|
||||
| Device Type | mobile |
|
||||
| OS | iOS 17.2 |
|
||||
| App Version | 1.2.3 |
|
||||
| Device Model | iPhone 14 Pro |
|
||||
| Browser | N/A |
|
||||
| IP Address | 1.2.3.4 |
|
||||
| Geolocation | Paris, France |
|
||||
| Created At | 2026-02-03T14:32:18Z |
|
||||
| Last Activity | 2026-02-03T14:32:18Z |
|
||||
Et un token JWT avec durée de vie de 30 jours est généré
|
||||
Et un refresh token avec durée de vie de 90 jours est généré
|
||||
Et un événement "SESSION_CREATED" est enregistré
|
||||
Et la métrique "sessions.created" est incrémentée
|
||||
|
||||
Scénario: Connexion simultanée sur plusieurs appareils
|
||||
Étant donné un utilisateur "bob@roadwave.fr" connecté sur:
|
||||
| Appareil | OS | Dernière activité |
|
||||
| iPhone 13 | iOS 16.5 | Il y a 5 min |
|
||||
| iPad Pro | iPadOS 17.1 | Il y a 2 heures |
|
||||
| MacBook Pro | macOS 14.2 | Il y a 1 jour |
|
||||
Quand l'utilisateur se connecte depuis un Samsung Galaxy S23
|
||||
Alors une nouvelle session est créée
|
||||
Et l'utilisateur a maintenant 4 sessions actives
|
||||
Et toutes les sessions précédentes restent valides
|
||||
Et un événement "NEW_DEVICE_LOGIN" est enregistré
|
||||
Et une notification push est envoyée sur tous les appareils: "Nouvelle connexion depuis Samsung Galaxy S23"
|
||||
|
||||
Scénario: Limitation du nombre de sessions simultanées
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec 5 sessions actives
|
||||
Quand l'utilisateur se connecte depuis un 6ème appareil
|
||||
Alors la session la plus ancienne est automatiquement révoquée
|
||||
Et une nouvelle session est créée pour le nouvel appareil
|
||||
Et l'utilisateur reçoit une notification: "Votre session sur [Ancien Appareil] a été fermée automatiquement"
|
||||
Et un événement "SESSION_EVICTED_MAX_LIMIT" est enregistré
|
||||
Et la métrique "sessions.evicted.max_limit" est incrémentée
|
||||
|
||||
Scénario: Liste des sessions actives dans les paramètres du compte
|
||||
Étant donné un utilisateur "david@roadwave.fr" avec 3 sessions actives
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Appareils connectés"
|
||||
Alors l'utilisateur voit la liste suivante:
|
||||
| Appareil | Localisation | Dernière activité | IP | Actions |
|
||||
| iPhone 14 Pro | Paris, France | Actif maintenant | 1.2.3.4 | [Cet appareil]|
|
||||
| iPad Air | Lyon, France | Il y a 2 heures | 5.6.7.8 | [Déconnecter] |
|
||||
| MacBook Pro | Marseille, FR | Il y a 3 jours | 9.10.11.12| [Déconnecter] |
|
||||
Et la session actuelle est clairement identifiée
|
||||
Et un bouton "Déconnecter tous les autres appareils" est disponible
|
||||
|
||||
Scénario: Révocation manuelle d'une session spécifique
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec 4 sessions actives
|
||||
Et il consulte la liste de ses appareils depuis son iPhone
|
||||
Quand l'utilisateur clique sur "Déconnecter" pour la session "MacBook Pro"
|
||||
Alors la session "MacBook Pro" est immédiatement révoquée
|
||||
Et le token JWT associé est invalidé dans Redis
|
||||
Et le refresh token est révoqué
|
||||
Et l'utilisateur sur le MacBook Pro est déconnecté lors de sa prochaine requête
|
||||
Et un événement "SESSION_REVOKED_MANUAL" est enregistré
|
||||
Et une notification est envoyée: "Vous avez été déconnecté de votre MacBook Pro"
|
||||
|
||||
Scénario: Déconnexion de tous les autres appareils
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 5 sessions actives
|
||||
Et il suspecte un accès non autorisé
|
||||
Quand l'utilisateur clique sur "Déconnecter tous les autres appareils" depuis son iPhone
|
||||
Alors toutes les sessions sauf la session actuelle (iPhone) sont révoquées
|
||||
Et 4 tokens JWT sont invalidés
|
||||
Et 4 refresh tokens sont révoqués
|
||||
Et un événement "SESSIONS_REVOKED_ALL_OTHER" est enregistré
|
||||
Et une notification est envoyée sur tous les appareils déconnectés
|
||||
Et un email de confirmation est envoyé: "Vous avez déconnecté tous vos autres appareils"
|
||||
Et la métrique "sessions.revoked.bulk" est incrémentée
|
||||
|
||||
Scénario: Détection de connexion suspecte depuis un nouveau pays
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec sessions habituelles en France
|
||||
Quand l'utilisateur se connecte depuis une IP en Russie
|
||||
Alors une alerte de sécurité est déclenchée
|
||||
Et un email est envoyé: "Connexion détectée depuis Russie - Est-ce bien vous ?"
|
||||
Et une notification push est envoyée sur tous les appareils de confiance
|
||||
Et la session est créée mais marquée comme "suspecte"
|
||||
Et un événement "SUSPICIOUS_LOCATION_LOGIN" est enregistré avec niveau "HIGH"
|
||||
Et l'utilisateur doit confirmer son identité par code SMS avant d'accéder aux fonctionnalités sensibles
|
||||
|
||||
Scénario: Expiration automatique d'une session inactive
|
||||
Étant donné un utilisateur "henry@roadwave.fr" avec une session sur iPad
|
||||
Et la session n'a pas été utilisée depuis 8 jours
|
||||
Quand le job de nettoyage des sessions s'exécute
|
||||
Alors la session iPad est automatiquement révoquée
|
||||
Et le token JWT est invalidé
|
||||
Et le refresh token est révoqué
|
||||
Et un événement "SESSION_EXPIRED_INACTIVITY" est enregistré
|
||||
Et un email est envoyé: "Votre session sur iPad a expiré suite à 8 jours d'inactivité"
|
||||
Et la métrique "sessions.expired.inactivity" est incrémentée
|
||||
|
||||
Scénario: Rafraîchissement automatique du token avant expiration
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec une session active
|
||||
Et le token JWT expire dans 2 minutes
|
||||
Quand l'application mobile effectue une requête API
|
||||
Alors l'API détecte que le token expire bientôt
|
||||
Et un nouveau token JWT est généré automatiquement
|
||||
Et le nouveau token est retourné dans le header "X-Refreshed-Token"
|
||||
Et l'application mobile stocke le nouveau token
|
||||
Et un événement "TOKEN_REFRESHED" est enregistré
|
||||
Et la métrique "tokens.refreshed" est incrémentée
|
||||
|
||||
Scénario: Révocation de toutes les sessions lors d'un changement de mot de passe
|
||||
Étant donné un utilisateur "jack@roadwave.fr" avec 4 sessions actives
|
||||
Quand l'utilisateur change son mot de passe depuis son iPhone
|
||||
Alors toutes les sessions sauf la session actuelle (iPhone) sont révoquées
|
||||
Et tous les tokens JWT sont invalidés
|
||||
Et tous les refresh tokens sont révoqués
|
||||
Et un événement "SESSIONS_REVOKED_PASSWORD_CHANGE" est enregistré
|
||||
Et un email est envoyé: "Votre mot de passe a été modifié. Toutes vos autres sessions ont été déconnectées."
|
||||
Et des notifications push sont envoyées sur tous les appareils déconnectés
|
||||
|
||||
Scénario: Persistance de la session avec "Se souvenir de moi"
|
||||
Étant donné un utilisateur "kate@roadwave.fr" qui se connecte
|
||||
Quand l'utilisateur coche l'option "Se souvenir de moi"
|
||||
Alors la durée de vie du token JWT est étendue à 90 jours
|
||||
Et la durée de vie du refresh token est étendue à 180 jours
|
||||
Et la session persiste même après fermeture de l'application
|
||||
Et un cookie sécurisé "remember_token" est stocké (pour web)
|
||||
Et un événement "LONG_SESSION_CREATED" est enregistré
|
||||
Et la métrique "sessions.remember_me.enabled" est incrémentée
|
||||
|
||||
Scénario: Détection de vol de token et révocation automatique
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec une session active
|
||||
Et le token JWT a été volé et utilisé depuis une IP différente
|
||||
Quand le système détecte une utilisation simultanée du même token depuis 2 IP différentes
|
||||
Alors toutes les sessions de l'utilisateur sont immédiatement révoquées
|
||||
Et tous les tokens sont invalidés
|
||||
Et un email d'alerte critique est envoyé: "Activité suspecte détectée - Toutes vos sessions ont été fermées"
|
||||
Et une notification push urgente est envoyée sur tous les appareils
|
||||
Et l'utilisateur doit réinitialiser son mot de passe avant de se reconnecter
|
||||
Et un événement "TOKEN_THEFT_DETECTED" est enregistré avec niveau "CRITICAL"
|
||||
Et l'équipe de sécurité est alertée via webhook
|
||||
|
||||
Scénario: Synchronisation des informations de session en temps réel
|
||||
Étant donné un utilisateur "mary@roadwave.fr" connecté sur 3 appareils
|
||||
Quand l'utilisateur révoque une session depuis son iPhone
|
||||
Alors la liste des sessions est mise à jour en temps réel sur tous les appareils via WebSocket
|
||||
Et l'appareil déconnecté reçoit immédiatement une notification de déconnexion
|
||||
Et l'UI est rafraîchie automatiquement sur tous les appareils connectés
|
||||
Et la métrique "sessions.realtime_sync" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance de gestion des sessions
|
||||
Étant donné que le système gère 100 000 sessions actives
|
||||
Quand les métriques de performance sont collectées
|
||||
Alors les indicateurs suivants sont respectés:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps de création de session | < 50ms |
|
||||
| Temps de validation de token | < 20ms |
|
||||
| Temps de révocation de session | < 100ms |
|
||||
| Latence de synchronisation temps réel | < 500ms |
|
||||
| Taux de succès du refresh automatique | > 99.9% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si les seuils sont dépassés
|
||||
|
||||
Scénario: Stockage sécurisé des sessions dans Redis
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec une session active
|
||||
Quand la session est stockée dans Redis
|
||||
Alors les données suivantes sont chiffrées:
|
||||
| Champ | Chiffrement |
|
||||
| User ID | Hash |
|
||||
| Refresh Token | AES-256 |
|
||||
| Device Info | Non |
|
||||
| IP Address | Hash |
|
||||
Et la clé Redis a un TTL correspondant à la durée de vie de la session
|
||||
Et les données sensibles ne sont jamais loggées en clair
|
||||
Et les accès à Redis sont audités
|
||||
Et la métrique "sessions.storage.encrypted" est incrémentée
|
||||
@@ -0,0 +1,107 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Récupération de compte
|
||||
En tant qu'utilisateur ayant oublié son mot de passe
|
||||
Je veux pouvoir réinitialiser mon mot de passe via email
|
||||
Afin de récupérer l'accès à mon compte
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur existe avec l'email "user@test.fr"
|
||||
|
||||
Scénario: Demander la réinitialisation du mot de passe
|
||||
Quand je clique sur "Mot de passe oublié"
|
||||
Et que je saisis mon email "user@test.fr"
|
||||
Alors je reçois un email avec un lien de réinitialisation
|
||||
Et le lien expire dans 1 heure
|
||||
Et je vois le message "Email de réinitialisation envoyé"
|
||||
|
||||
Scénario: Email inexistant lors de la demande de réinitialisation
|
||||
Quand je demande une réinitialisation pour l'email "inexistant@test.fr"
|
||||
Alors je vois le même message "Email de réinitialisation envoyé"
|
||||
Mais aucun email n'est envoyé (sécurité - pas d'énumération d'emails)
|
||||
|
||||
Scénario: Réinitialiser le mot de passe avec un lien valide
|
||||
Étant donné que j'ai demandé une réinitialisation de mot de passe
|
||||
Et que j'ai reçu le lien de réinitialisation
|
||||
Quand je clique sur le lien
|
||||
Et que je saisis un nouveau mot de passe "NouveauPass123"
|
||||
Et que je confirme le nouveau mot de passe "NouveauPass123"
|
||||
Alors mon mot de passe est modifié avec succès
|
||||
Et je suis déconnecté de tous mes appareils sauf celui en cours
|
||||
Et je reçois un email de confirmation de changement
|
||||
|
||||
Scénario: Lien de réinitialisation expiré
|
||||
Étant donné que j'ai demandé une réinitialisation il y a 2 heures
|
||||
Quand j'essaie d'utiliser le lien
|
||||
Alors je vois le message "Ce lien a expiré"
|
||||
Et je peux demander un nouveau lien
|
||||
|
||||
Scénario: Nouveau mot de passe ne respecte pas les règles
|
||||
Étant donné que j'ai un lien de réinitialisation valide
|
||||
Quand je saisis un nouveau mot de passe "faible"
|
||||
Alors la réinitialisation échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins 8 caractères, 1 majuscule et 1 chiffre"
|
||||
|
||||
Scénario: Confirmation du mot de passe ne correspond pas
|
||||
Étant donné que j'ai un lien de réinitialisation valide
|
||||
Quand je saisis un nouveau mot de passe "NouveauPass123"
|
||||
Et que je confirme avec un mot de passe différent "AutrePass123"
|
||||
Alors la réinitialisation échoue
|
||||
Et je vois le message "Les mots de passe ne correspondent pas"
|
||||
|
||||
Scénario: Limite de demandes de réinitialisation
|
||||
Étant donné que j'ai déjà demandé 3 réinitialisations dans la dernière heure
|
||||
Quand je demande une 4ème réinitialisation
|
||||
Alors la demande échoue
|
||||
Et je vois le message "Maximum 3 demandes par heure. Réessayez plus tard."
|
||||
|
||||
Scénario: Compteur de demandes se réinitialise après 1 heure
|
||||
Étant donné que j'ai demandé 3 réinitialisations
|
||||
Et que 1 heure s'est écoulée
|
||||
Quand je demande une nouvelle réinitialisation
|
||||
Alors la demande réussit
|
||||
Et je reçois un email avec un nouveau lien
|
||||
|
||||
Scénario: Email de notification de changement de mot de passe
|
||||
Étant donné que je viens de réinitialiser mon mot de passe
|
||||
Alors je reçois un email de confirmation avec:
|
||||
| sujet | Votre mot de passe a été modifié |
|
||||
| contenu_contient | Votre mot de passe a été modifié |
|
||||
| date_heure | présente |
|
||||
| appareil | présent |
|
||||
| localisation | présente |
|
||||
| action_urgence | Lien si ce n'était pas vous |
|
||||
|
||||
Scénario: Notification push si changement depuis appareil non reconnu
|
||||
Étant donné que je me suis toujours connecté depuis mon iPhone
|
||||
Et que je réinitialise mon mot de passe depuis un PC Windows
|
||||
Alors je reçois une notification push sur mon iPhone avec:
|
||||
| titre | Mot de passe modifié |
|
||||
| message | Depuis Windows - Paris, France |
|
||||
| action | Sécuriser le compte si ce n'est pas vous |
|
||||
|
||||
Scénario: Déconnexion de tous les appareils après réinitialisation
|
||||
Étant donné que je suis connecté sur 4 appareils différents
|
||||
Et que je réinitialise mon mot de passe depuis un navigateur web
|
||||
Alors les 3 autres appareils sont déconnectés immédiatement
|
||||
Et seule la session du navigateur web reste active
|
||||
Et je vois le message "Vous avez été déconnecté des autres appareils par sécurité"
|
||||
|
||||
Scénario: Lien de réinitialisation invalide si déjà utilisé
|
||||
Étant donné que j'ai réinitialisé mon mot de passe avec un lien
|
||||
Quand j'essaie de réutiliser le même lien
|
||||
Alors je vois le message "Ce lien a déjà été utilisé"
|
||||
Et je peux demander un nouveau lien si nécessaire
|
||||
|
||||
Scénario: Nouveau lien invalide l'ancien
|
||||
Étant donné que j'ai demandé une réinitialisation et reçu un lien
|
||||
Quand je demande une nouvelle réinitialisation
|
||||
Alors l'ancien lien est invalidé
|
||||
Et seul le nouveau lien fonctionne
|
||||
|
||||
Scénario: Réinitialisation débloque un compte bloqué
|
||||
Étant donné que mon compte est bloqué après 5 tentatives de connexion
|
||||
Quand je réinitialise mon mot de passe via email
|
||||
Alors le blocage est levé immédiatement
|
||||
Et je peux me connecter avec le nouveau mot de passe
|
||||
Et le compteur de tentatives est remis à 0
|
||||
@@ -0,0 +1,187 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Récupération et réinitialisation avancée du mot de passe
|
||||
|
||||
En tant qu'utilisateur ayant oublié son mot de passe
|
||||
Je veux pouvoir récupérer l'accès à mon compte de manière sécurisée
|
||||
Afin de reprendre l'utilisation de l'application
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de récupération est configuré avec:
|
||||
| Paramètre | Valeur |
|
||||
| Durée de validité du lien de reset | 1 heure |
|
||||
| Nombre max de demandes par heure | 3 |
|
||||
| Nombre max de demandes par jour | 10 |
|
||||
| Longueur du token de reset | 64 chars |
|
||||
| Délai de cooldown entre demandes | 5 minutes |
|
||||
|
||||
Scénario: Demande de réinitialisation de mot de passe
|
||||
Étant donné un utilisateur "alice@roadwave.fr" qui a oublié son mot de passe
|
||||
Quand l'utilisateur clique sur "Mot de passe oublié ?" sur l'écran de connexion
|
||||
Et saisit son adresse email "alice@roadwave.fr"
|
||||
Alors un email de réinitialisation est envoyé avec:
|
||||
| Élément | Contenu |
|
||||
| Sujet | Réinitialisation de votre mot de passe RoadWave |
|
||||
| Lien sécurisé | https://roadwave.fr/reset?token=abc123... |
|
||||
| Durée de validité | Ce lien expire dans 1 heure |
|
||||
| Warning sécurité | Si vous n'êtes pas à l'origine de cette demande... |
|
||||
Et un événement "PASSWORD_RESET_REQUESTED" est enregistré
|
||||
Et la métrique "auth.password_reset.requested" est incrémentée
|
||||
Et un message s'affiche: "Si cette adresse est enregistrée, vous recevrez un email de réinitialisation"
|
||||
|
||||
Scénario: Protection contre l'énumération d'adresses email
|
||||
Étant donné une adresse email "inexistant@roadwave.fr" non enregistrée
|
||||
Quand un utilisateur demande la réinitialisation pour cette adresse
|
||||
Alors le même message de confirmation s'affiche: "Si cette adresse est enregistrée, vous recevrez un email"
|
||||
Et aucun email n'est envoyé
|
||||
Et le temps de réponse est identique à une demande valide (800-1200ms)
|
||||
Et un événement "PASSWORD_RESET_UNKNOWN_EMAIL" est enregistré
|
||||
Et la métrique "auth.password_reset.unknown_email" est incrémentée
|
||||
Et les logs n'exposent pas l'information de l'existence ou non de l'email
|
||||
|
||||
Scénario: Limitation du nombre de demandes de réinitialisation
|
||||
Étant donné un utilisateur "bob@roadwave.fr"
|
||||
Et il a déjà effectué 3 demandes de réinitialisation dans la dernière heure
|
||||
Quand l'utilisateur effectue une 4ème demande
|
||||
Alors la demande est refusée avec le message: "Trop de demandes de réinitialisation. Veuillez attendre 1 heure."
|
||||
Et aucun email n'est envoyé
|
||||
Et un événement "PASSWORD_RESET_RATE_LIMITED" est enregistré
|
||||
Et la métrique "auth.password_reset.rate_limited" est incrémentée
|
||||
|
||||
Scénario: Utilisation du lien de réinitialisation valide
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" ayant demandé la réinitialisation
|
||||
Et il a reçu un email avec un token valide il y a 30 minutes
|
||||
Quand l'utilisateur clique sur le lien dans l'email
|
||||
Alors il est redirigé vers la page de réinitialisation
|
||||
Et le formulaire de nouveau mot de passe s'affiche
|
||||
Et le token est validé côté serveur
|
||||
Et un événement "PASSWORD_RESET_TOKEN_ACCESSED" est enregistré
|
||||
Et la session est sécurisée avec CSRF protection
|
||||
|
||||
Scénario: Définition du nouveau mot de passe avec validation
|
||||
Étant donné un utilisateur "david@roadwave.fr" sur la page de réinitialisation
|
||||
Et il a un token valide
|
||||
Quand l'utilisateur saisit un nouveau mot de passe "SecurePass2026!"
|
||||
Et confirme le mot de passe
|
||||
Alors le mot de passe est validé selon les règles de sécurité
|
||||
Et le mot de passe est hashé avec bcrypt (cost: 12)
|
||||
Et le mot de passe est enregistré dans la base de données
|
||||
Et toutes les sessions actives sont révoquées
|
||||
Et tous les tokens d'accès sont invalidés
|
||||
Et un événement "PASSWORD_RESET_COMPLETED" est enregistré
|
||||
Et un email de confirmation est envoyé: "Votre mot de passe a été modifié avec succès"
|
||||
Et la métrique "auth.password_reset.completed" est incrémentée
|
||||
Et l'utilisateur est redirigé vers la page de connexion
|
||||
|
||||
Scénario: Tentative d'utilisation d'un token expiré
|
||||
Étant donné un utilisateur "eve@roadwave.fr" ayant demandé la réinitialisation
|
||||
Et il a reçu un email avec un token valide il y a 2 heures
|
||||
Quand l'utilisateur clique sur le lien expiré
|
||||
Alors un message d'erreur s'affiche: "Ce lien de réinitialisation a expiré. Veuillez faire une nouvelle demande."
|
||||
Et un bouton "Demander un nouveau lien" est affiché
|
||||
Et un événement "PASSWORD_RESET_TOKEN_EXPIRED" est enregistré
|
||||
Et la métrique "auth.password_reset.token_expired" est incrémentée
|
||||
|
||||
Scénario: Tentative d'utilisation d'un token déjà utilisé
|
||||
Étant donné un utilisateur "frank@roadwave.fr" ayant réinitialisé son mot de passe
|
||||
Et le token a déjà été utilisé il y a 10 minutes
|
||||
Quand l'utilisateur tente de réutiliser le même lien
|
||||
Alors un message d'erreur s'affiche: "Ce lien a déjà été utilisé. Si vous avez besoin de réinitialiser à nouveau, faites une nouvelle demande."
|
||||
Et un événement "PASSWORD_RESET_TOKEN_REUSED" est enregistré avec niveau "MEDIUM"
|
||||
Et un email d'alerte est envoyé: "Tentative de réutilisation d'un ancien lien de réinitialisation"
|
||||
Et la métrique "auth.password_reset.token_reused" est incrémentée
|
||||
|
||||
Scénario: Détection de tentative d'attaque par force brute sur les tokens
|
||||
Étant donné un attaquant qui tente de deviner des tokens de réinitialisation
|
||||
Quand 10 tokens invalides sont testés depuis la même IP en 5 minutes
|
||||
Alors l'IP est bloquée temporairement pour 1 heure
|
||||
Et tous les tokens valides pour cette IP sont invalidés
|
||||
Et un événement "PASSWORD_RESET_BRUTE_FORCE_DETECTED" est enregistré avec niveau "CRITICAL"
|
||||
Et l'équipe de sécurité est alertée via webhook
|
||||
Et la métrique "security.password_reset.brute_force" est incrémentée
|
||||
|
||||
Scénario: Réinitialisation avec validation 2FA pour comptes sensibles
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec 2FA activé
|
||||
Et il a demandé la réinitialisation de son mot de passe
|
||||
Quand l'utilisateur clique sur le lien de réinitialisation
|
||||
Alors une étape supplémentaire de vérification 2FA s'affiche
|
||||
Et l'utilisateur doit saisir un code TOTP ou un code de récupération
|
||||
Et après validation 2FA, le formulaire de nouveau mot de passe s'affiche
|
||||
Et un événement "PASSWORD_RESET_2FA_VALIDATED" est enregistré
|
||||
Et la métrique "auth.password_reset.with_2fa" est incrémentée
|
||||
|
||||
Scénario: Notification de sécurité sur tous les appareils
|
||||
Étant donné un utilisateur "henry@roadwave.fr" connecté sur 3 appareils
|
||||
Quand l'utilisateur réinitialise son mot de passe
|
||||
Alors une notification push est envoyée sur tous les appareils:
|
||||
| Message |
|
||||
| Votre mot de passe a été modifié |
|
||||
| Si ce n'est pas vous, contactez immédiatement le support |
|
||||
Et un email est envoyé avec détails:
|
||||
| Détail | Valeur |
|
||||
| Date et heure | 2026-02-03 14:32:18 |
|
||||
| Adresse IP | 1.2.3.4 |
|
||||
| Localisation | Paris, France |
|
||||
| Appareil | iPhone 14 Pro |
|
||||
| Navigateur | Safari 17.2 |
|
||||
Et un lien "Ce n'était pas moi" permet de bloquer le compte immédiatement
|
||||
|
||||
Scénario: Historique des modifications de mot de passe
|
||||
Étant donné un utilisateur "iris@roadwave.fr"
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Historique"
|
||||
Alors l'utilisateur voit l'historique des modifications:
|
||||
| Date | Action | IP | Appareil | Localisation |
|
||||
| 2026-02-03 14:32 | Réinitialisation mot de passe | 1.2.3.4 | iPhone 14 | Paris, FR |
|
||||
| 2026-01-15 10:20 | Changement mot de passe | 5.6.7.8 | MacBook Pro | Lyon, FR |
|
||||
| 2025-12-01 08:15 | Création du compte | 9.10.11.12| iPad Air | Marseille, FR |
|
||||
Et les événements sont conservés pendant 90 jours minimum
|
||||
Et les logs sont conformes RGPD
|
||||
|
||||
Scénario: Réinitialisation impossible pour compte bloqué ou suspendu
|
||||
Étant donné un utilisateur "jack@roadwave.fr" dont le compte est suspendu
|
||||
Quand l'utilisateur demande la réinitialisation de son mot de passe
|
||||
Alors un message s'affiche: "Votre compte est actuellement suspendu. Veuillez contacter le support."
|
||||
Et aucun email de réinitialisation n'est envoyé
|
||||
Et un événement "PASSWORD_RESET_ACCOUNT_SUSPENDED" est enregistré
|
||||
Et un lien vers le support est fourni
|
||||
Et la métrique "auth.password_reset.blocked_account" est incrémentée
|
||||
|
||||
Scénario: Vérification de l'unicité du nouveau mot de passe
|
||||
Étant donné un utilisateur "kate@roadwave.fr" sur la page de réinitialisation
|
||||
Quand l'utilisateur tente de définir le même mot de passe que l'ancien
|
||||
Alors une erreur s'affiche: "Veuillez choisir un mot de passe différent de l'ancien"
|
||||
Et le mot de passe n'est pas enregistré
|
||||
Et un événement "PASSWORD_RESET_SAME_PASSWORD" est enregistré
|
||||
Et la métrique "auth.password_reset.same_password" est incrémentée
|
||||
|
||||
Scénario: Vérification contre les mots de passe compromis
|
||||
Étant donné un utilisateur "luke@roadwave.fr" sur la page de réinitialisation
|
||||
Quand l'utilisateur tente de définir un mot de passe "Password123!"
|
||||
Et ce mot de passe figure dans la base de données Have I Been Pwned
|
||||
Alors une erreur s'affiche: "Ce mot de passe est connu et a été compromis. Veuillez en choisir un autre."
|
||||
Et le mot de passe n'est pas enregistré
|
||||
Et un événement "PASSWORD_RESET_COMPROMISED_PASSWORD" est enregistré
|
||||
Et la métrique "auth.password_reset.compromised_blocked" est incrémentée
|
||||
|
||||
Scénario: Cooldown entre demandes successives de réinitialisation
|
||||
Étant donné un utilisateur "mary@roadwave.fr"
|
||||
Et il a fait une demande de réinitialisation il y a 2 minutes
|
||||
Quand l'utilisateur fait une nouvelle demande de réinitialisation
|
||||
Alors la demande est refusée avec le message: "Veuillez attendre 5 minutes entre chaque demande"
|
||||
Et un compteur affiche "Vous pourrez faire une nouvelle demande dans 3 minutes"
|
||||
Et un événement "PASSWORD_RESET_COOLDOWN" est enregistré
|
||||
Et la métrique "auth.password_reset.cooldown_hit" est incrémentée
|
||||
|
||||
Scénario: Métriques de sécurité pour la réinitialisation de mot de passe
|
||||
Étant donné que le système traite 1000 demandes de réinitialisation par jour
|
||||
Quand les métriques de sécurité sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Taux de complétion des réinitialisations | > 75% |
|
||||
| Taux de tokens expirés avant utilisation | < 20% |
|
||||
| Temps moyen de complétion | < 5 min |
|
||||
| Taux de détection de mots de passe compromis | > 5% |
|
||||
| Nombre de tentatives de brute force bloquées | Visible |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si anomalies détectées
|
||||
@@ -0,0 +1,114 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion des sessions et tokens
|
||||
En tant qu'utilisateur connecté
|
||||
Je veux que mes sessions soient sécurisées et gérées automatiquement
|
||||
Afin de maintenir l'accès à l'application sans friction
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté avec succès
|
||||
|
||||
Scénario: Access token expire après 15 minutes
|
||||
Étant donné que j'ai reçu un access token
|
||||
Et que 15 minutes se sont écoulées
|
||||
Quand je fais une requête API avec cet access token
|
||||
Alors la requête échoue avec le code 401
|
||||
Et je vois le message "Token expiré"
|
||||
|
||||
Scénario: Refresh automatique du token avec refresh token
|
||||
Étant donné que mon access token a expiré
|
||||
Et que mon refresh token est valide
|
||||
Quand l'application demande un nouveau access token
|
||||
Alors je reçois un nouvel access token valide pour 15 minutes
|
||||
Et je reçois un nouveau refresh token (rotation)
|
||||
Et l'ancien refresh token est invalidé
|
||||
|
||||
Scénario: Refresh token expire après 30 jours d'inactivité
|
||||
Étant donné que je me suis connecté il y a 30 jours
|
||||
Et que je n'ai pas utilisé l'application depuis
|
||||
Quand j'essaie d'utiliser mon refresh token
|
||||
Alors la requête échoue
|
||||
Et je dois me reconnecter avec email/password
|
||||
|
||||
Scénario: Prolongation automatique de la session si l'app est utilisée
|
||||
Étant donné que je me suis connecté il y a 25 jours
|
||||
Et que j'utilise l'application régulièrement
|
||||
Quand je fais une requête API
|
||||
Alors ma session est automatiquement prolongée
|
||||
Et mon refresh token reste valide
|
||||
|
||||
Scénario: Détection de token replay attack
|
||||
Étant donné que j'ai rafraîchi mon token
|
||||
Et que j'ai reçu un nouveau refresh token
|
||||
Quand j'essaie de réutiliser l'ancien refresh token
|
||||
Alors la requête échoue
|
||||
Et je vois le message "Token invalide ou révoqué"
|
||||
Et toutes mes sessions sont révoquées par sécurité
|
||||
|
||||
Scénario: Voir la liste des appareils connectés
|
||||
Étant donné que je suis connecté sur 3 appareils différents
|
||||
Quand je consulte la liste de mes appareils connectés
|
||||
Alors je vois 3 appareils avec les informations suivantes:
|
||||
| information | exemple |
|
||||
| OS | iOS 17.1 |
|
||||
| Navigateur | Safari |
|
||||
| Dernière connexion | Il y a 2 heures |
|
||||
| Localisation | Paris, France (IP visible) |
|
||||
|
||||
Scénario: Révoquer un appareil spécifique
|
||||
Étant donné que je suis connecté sur mon iPhone et mon iPad
|
||||
Quand je révoque la session de mon iPad depuis les paramètres
|
||||
Alors la session iPad est immédiatement déconnectée
|
||||
Et ma session iPhone reste active
|
||||
|
||||
Scénario: Déconnecter tous les appareils sauf celui en cours
|
||||
Étant donné que je suis connecté sur 4 appareils
|
||||
Quand je clique sur "Déconnecter tous les appareils"
|
||||
Alors les 3 autres appareils sont déconnectés
|
||||
Et seul l'appareil actuel reste connecté
|
||||
|
||||
Scénario: Alerte de connexion depuis nouveau device
|
||||
Étant donné que je me suis toujours connecté depuis Paris
|
||||
Quand je me connecte depuis un nouvel appareil à Lyon
|
||||
Alors je reçois une notification push sur mes autres appareils
|
||||
Et je reçois un email avec:
|
||||
| sujet | Nouvelle connexion détectée |
|
||||
| localisation | Lyon, France |
|
||||
| appareil | Android 14 - Chrome |
|
||||
| action | Lien pour révoquer la session |
|
||||
|
||||
Scénario: Alerte de connexion suspecte depuis pays différent
|
||||
Étant donné que je me suis toujours connecté depuis la France
|
||||
Quand je me connecte depuis un appareil aux États-Unis
|
||||
Alors je reçois une notification push immédiate
|
||||
Et je reçois un email d'alerte de sécurité
|
||||
Et la nouvelle session nécessite une validation 2FA même si désactivée
|
||||
|
||||
Scénario: Déconnexion après 30 jours d'inactivité totale
|
||||
Étant donné que je ne me suis pas connecté depuis 30 jours
|
||||
Quand j'ouvre l'application
|
||||
Alors je suis automatiquement déconnecté
|
||||
Et je dois me reconnecter avec email/password
|
||||
Et je vois le message "Session expirée après 30 jours d'inactivité"
|
||||
|
||||
Scénario: Sessions multiples simultanées autorisées
|
||||
Étant donné que je suis connecté sur:
|
||||
| appareil |
|
||||
| iPhone |
|
||||
| iPad |
|
||||
| PC Windows (Web) |
|
||||
Quand je fais des actions sur les 3 appareils simultanément
|
||||
Alors toutes les sessions fonctionnent sans conflit
|
||||
Et chaque appareil maintient sa propre session
|
||||
|
||||
Scénario: Validation de JWT via Zitadel
|
||||
Étant donné que j'ai reçu un access token JWT
|
||||
Quand l'API RoadWave valide le token
|
||||
Alors la validation est faite localement avec la clé publique Zitadel
|
||||
Et aucune requête externe n'est effectuée (performance)
|
||||
Et le token contient les claims suivants:
|
||||
| claim | valeur_exemple |
|
||||
| sub | user-id-123 |
|
||||
| email | user@test.fr |
|
||||
| exp | timestamp + 15 minutes |
|
||||
| iss | zitadel.roadwave.com |
|
||||
@@ -0,0 +1,132 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Authentification à deux facteurs (2FA)
|
||||
En tant qu'utilisateur soucieux de sécurité
|
||||
Je veux activer la 2FA sur mon compte
|
||||
Afin de protéger mon accès même si mon mot de passe est compromis
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté à mon compte
|
||||
|
||||
Scénario: Activer la 2FA TOTP (Time-based One-Time Password)
|
||||
Étant donné que la 2FA n'est pas activée sur mon compte
|
||||
Quand je choisis d'activer la 2FA TOTP
|
||||
Alors je vois un QR code à scanner
|
||||
Et je vois le secret partagé en texte clair (backup)
|
||||
Et je dois entrer un code de vérification depuis mon app authenticator
|
||||
Quand je saisis un code TOTP valide
|
||||
Alors la 2FA TOTP est activée avec succès
|
||||
Et je reçois des codes de backup (10 codes)
|
||||
|
||||
Scénario: Connexion avec 2FA TOTP activée
|
||||
Étant donné que la 2FA TOTP est activée sur mon compte
|
||||
Quand je me connecte avec email/password
|
||||
Alors je suis redirigé vers la page de saisie du code 2FA
|
||||
Quand je saisis un code TOTP valide de mon authenticator
|
||||
Alors je suis connecté avec succès
|
||||
|
||||
Scénario: Connexion échouée avec code TOTP invalide
|
||||
Étant donné que la 2FA TOTP est activée
|
||||
Quand je me connecte avec email/password
|
||||
Et que je saisis un code TOTP invalide "000000"
|
||||
Alors la connexion échoue
|
||||
Et je vois le message "Code d'authentification invalide"
|
||||
Et je peux réessayer
|
||||
|
||||
Scénario: Utiliser un code de backup pour 2FA
|
||||
Étant donné que la 2FA TOTP est activée
|
||||
Et que j'ai perdu l'accès à mon authenticator
|
||||
Quand je me connecte avec email/password
|
||||
Et que je clique sur "Utiliser un code de backup"
|
||||
Et que je saisis un code de backup valide
|
||||
Alors je suis connecté avec succès
|
||||
Et le code de backup utilisé est invalidé
|
||||
Et il me reste 9 codes de backup
|
||||
|
||||
Scénario: Activer la 2FA par email
|
||||
Étant donné que la 2FA n'est pas activée
|
||||
Quand je choisis d'activer la 2FA par email
|
||||
Alors la 2FA email est activée immédiatement
|
||||
Et je vois le message "2FA email activée. Vous recevrez un code à chaque connexion"
|
||||
|
||||
Scénario: Connexion avec 2FA email
|
||||
Étant donné que la 2FA email est activée
|
||||
Quand je me connecte avec email/password
|
||||
Alors je reçois un email avec un code à 6 chiffres
|
||||
Et le code expire dans 10 minutes
|
||||
Et je dois saisir ce code pour terminer la connexion
|
||||
|
||||
Scénario: Code 2FA email expiré
|
||||
Étant donné que la 2FA email est activée
|
||||
Et que je me suis connecté avec email/password
|
||||
Et que j'ai reçu un code 2FA par email il y a 11 minutes
|
||||
Quand je saisis ce code
|
||||
Alors la connexion échoue
|
||||
Et je vois le message "Code expiré. Demandez un nouveau code."
|
||||
|
||||
Scénario: Renvoyer le code 2FA email
|
||||
Étant donné que la 2FA email est activée
|
||||
Et que je suis sur la page de saisie du code 2FA
|
||||
Quand je clique sur "Renvoyer le code"
|
||||
Alors je reçois un nouveau code par email
|
||||
Et l'ancien code est invalidé
|
||||
|
||||
Scénario: Ajouter un appareil de confiance (skip 2FA pendant 30 jours)
|
||||
Étant donné que la 2FA TOTP est activée
|
||||
Quand je me connecte avec email/password et code TOTP
|
||||
Et que je coche "Ne plus demander sur cet appareil"
|
||||
Alors je suis connecté avec succès
|
||||
Et cet appareil est enregistré comme "appareil de confiance"
|
||||
Quand je me reconnecte dans les 30 jours suivants sur ce même appareil
|
||||
Alors je ne dois pas saisir de code 2FA
|
||||
|
||||
Scénario: Appareil de confiance expire après 30 jours
|
||||
Étant donné que j'ai enregistré un appareil de confiance il y a 31 jours
|
||||
Quand je me connecte depuis cet appareil
|
||||
Alors je dois saisir un code 2FA
|
||||
Et je vois le message "Appareil de confiance expiré. Veuillez vous authentifier"
|
||||
|
||||
Scénario: Voir la liste des appareils de confiance
|
||||
Étant donné que j'ai enregistré 3 appareils de confiance
|
||||
Quand je consulte mes paramètres de sécurité
|
||||
Alors je vois la liste de mes 3 appareils de confiance avec:
|
||||
| information | exemple |
|
||||
| Nom | iPhone 13 - Safari |
|
||||
| Date ajout | 15 janvier 2026 |
|
||||
| Dernière vue | Il y a 2 heures |
|
||||
| Expire le | 14 février 2026 |
|
||||
|
||||
Scénario: Révoquer un appareil de confiance
|
||||
Étant donné que j'ai un iPhone enregistré comme appareil de confiance
|
||||
Quand je révoque cet appareil depuis les paramètres
|
||||
Alors l'appareil est supprimé de la liste
|
||||
Quand je me reconnecte depuis cet iPhone
|
||||
Alors je dois saisir un code 2FA
|
||||
|
||||
Scénario: Révoquer tous les appareils de confiance
|
||||
Étant donné que j'ai 5 appareils de confiance enregistrés
|
||||
Quand je clique sur "Révoquer tous les appareils de confiance"
|
||||
Alors tous les appareils sont révoqués
|
||||
Et je vois le message "Tous les appareils de confiance ont été révoqués"
|
||||
|
||||
Scénario: 2FA forcée pour connexion suspecte malgré appareil de confiance
|
||||
Étant donné que j'ai un appareil de confiance enregistré en France
|
||||
Et que je me connecte depuis ce même appareil mais avec une IP américaine
|
||||
Quand je tente de me connecter
|
||||
Alors la 2FA est requise malgré l'appareil de confiance
|
||||
Et je vois le message "Connexion suspecte détectée. Authentification requise."
|
||||
|
||||
Scénario: Désactiver la 2FA
|
||||
Étant donné que la 2FA TOTP est activée
|
||||
Quand je désactive la 2FA depuis mes paramètres
|
||||
Et que je confirme avec mon mot de passe
|
||||
Alors la 2FA est désactivée
|
||||
Et tous les codes de backup sont invalidés
|
||||
Et tous les appareils de confiance sont révoqués
|
||||
|
||||
Scénario: Régénérer les codes de backup
|
||||
Étant donné que la 2FA est activée
|
||||
Et que j'ai utilisé 8 codes de backup sur 10
|
||||
Quand je demande à régénérer les codes de backup
|
||||
Alors je reçois 10 nouveaux codes
|
||||
Et tous les anciens codes (utilisés ou non) sont invalidés
|
||||
@@ -0,0 +1,250 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Validation des règles de mot de passe
|
||||
|
||||
En tant que système d'authentification
|
||||
Je veux valider la complexité des mots de passe
|
||||
Afin de garantir la sécurité des comptes utilisateurs
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur souhaite créer un compte ou modifier son mot de passe
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION LONGUEUR MINIMALE (8 CARACTÈRES)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec 8 caractères minimum
|
||||
Étant donné l'utilisateur saisit le mot de passe "Azerty123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et aucune erreur ne doit être affichée
|
||||
|
||||
Scénario: Mot de passe trop court (7 caractères)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Azert12"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 8 caractères"
|
||||
Et le champ doit être marqué en rouge
|
||||
|
||||
Scénario: Mot de passe très court (3 caractères)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Ab1"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 8 caractères"
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION MAJUSCULE REQUISE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec au moins 1 majuscule
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le critère "majuscule" doit être validé avec une coche verte
|
||||
|
||||
Scénario: Mot de passe sans majuscule
|
||||
Étant donné l'utilisateur saisit le mot de passe "monpass123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 1 majuscule"
|
||||
|
||||
Scénario: Mot de passe avec plusieurs majuscules
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonPASSword123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car au moins 1 majuscule est présente
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION CHIFFRE REQUIS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec au moins 1 chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass1"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le critère "chiffre" doit être validé avec une coche verte
|
||||
|
||||
Scénario: Mot de passe sans chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpassword"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 1 chiffre"
|
||||
|
||||
Scénario: Mot de passe avec plusieurs chiffres
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass123456"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car au moins 1 chiffre est présent
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION COMBINÉE DES 3 CRITÈRES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide respectant tous les critères
|
||||
Étant donné l'utilisateur saisit le mot de passe "SecurePass2024!"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et tous les critères doivent être validés :
|
||||
| critère | statut |
|
||||
| longueur | ✓ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✓ |
|
||||
|
||||
Scénario: Mot de passe échouant sur plusieurs critères
|
||||
Étant donné l'utilisateur saisit le mot de passe "pass"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et les messages d'erreur suivants doivent être affichés :
|
||||
| Le mot de passe doit contenir au moins 8 caractères |
|
||||
| Le mot de passe doit contenir au moins 1 majuscule |
|
||||
| Le mot de passe doit contenir au moins 1 chiffre |
|
||||
|
||||
Scénario: Mot de passe long mais sans majuscule ni chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "monmotdepasse"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et les messages d'erreur suivants doivent être affichés :
|
||||
| Le mot de passe doit contenir au moins 1 majuscule |
|
||||
| Le mot de passe doit contenir au moins 1 chiffre |
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION TEMPS RÉEL (FRONTEND)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Affichage progressif des critères pendant la saisie
|
||||
Étant donné l'utilisateur commence à saisir son mot de passe
|
||||
Quand l'utilisateur tape "m"
|
||||
Alors les critères suivants doivent être affichés :
|
||||
| critère | statut |
|
||||
| longueur | ✗ |
|
||||
| majuscule | ✗ |
|
||||
| chiffre | ✗ |
|
||||
Quand l'utilisateur tape "Mon"
|
||||
Alors les critères doivent être mis à jour :
|
||||
| critère | statut |
|
||||
| longueur | ✗ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✗ |
|
||||
Quand l'utilisateur tape "Monpass1"
|
||||
Alors les critères doivent être mis à jour :
|
||||
| critère | statut |
|
||||
| longueur | ✓ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✓ |
|
||||
|
||||
Scénario: Feedback visuel temps réel
|
||||
Étant donné l'utilisateur saisit progressivement son mot de passe
|
||||
Quand un critère est validé
|
||||
Alors une coche verte ✓ doit apparaître à côté du critère
|
||||
Et le texte du critère doit passer en vert
|
||||
Quand un critère n'est pas validé
|
||||
Alors une croix rouge ✗ doit apparaître
|
||||
Et le texte du critère doit rester en gris ou rouge
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION BACKEND (SÉCURITÉ)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Validation backend en plus du frontend
|
||||
Étant donné l'utilisateur contourne la validation frontend
|
||||
Et envoie directement le mot de passe "weak" via API
|
||||
Quand le backend reçoit la requête
|
||||
Alors la validation backend doit rejeter le mot de passe
|
||||
Et retourner une erreur HTTP 400 Bad Request
|
||||
Et le message doit être :
|
||||
"""
|
||||
{
|
||||
"error": "invalid_password",
|
||||
"details": [
|
||||
"Le mot de passe doit contenir au moins 8 caractères",
|
||||
"Le mot de passe doit contenir au moins 1 majuscule",
|
||||
"Le mot de passe doit contenir au moins 1 chiffre"
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Validation backend avec mot de passe valide
|
||||
Étant donné l'utilisateur envoie le mot de passe "SecurePass123"
|
||||
Quand le backend valide le mot de passe
|
||||
Alors la validation backend doit réussir
|
||||
Et le mot de passe doit être hashé avec bcrypt (coût 12)
|
||||
Et le hash doit être stocké dans la base de données
|
||||
|
||||
# ============================================================================
|
||||
# CAS LIMITES ET CARACTÈRES SPÉCIAUX
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe avec caractères spéciaux (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonP@ss123!"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les caractères spéciaux sont autorisés (mais non obligatoires)
|
||||
|
||||
Scénario: Mot de passe avec espaces (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Mon Pass 123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les espaces sont autorisés
|
||||
|
||||
Scénario: Mot de passe avec accents (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MônPàss123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les caractères accentués comptent comme des lettres
|
||||
|
||||
Scénario: Mot de passe avec émojis (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonPass123🔒"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les émojis sont autorisés
|
||||
|
||||
Scénario: Mot de passe vide
|
||||
Étant donné l'utilisateur laisse le champ mot de passe vide
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe est requis"
|
||||
|
||||
# ============================================================================
|
||||
# MODIFICATION MOT DE PASSE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Changement de mot de passe avec validation
|
||||
Étant donné un utilisateur authentifié veut changer son mot de passe
|
||||
Et l'utilisateur saisit son ancien mot de passe "OldPass123"
|
||||
Et l'utilisateur saisit le nouveau mot de passe "NewSecure456"
|
||||
Quand le système valide le nouveau mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le nouveau mot de passe doit respecter les mêmes règles
|
||||
Et l'ancien mot de passe doit être vérifié avant le changement
|
||||
|
||||
Scénario: Nouveau mot de passe identique à l'ancien (autorisé)
|
||||
Étant donné un utilisateur veut changer son mot de passe
|
||||
Et l'utilisateur saisit le nouveau mot de passe identique à l'ancien
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car il n'y a pas de règle interdisant la réutilisation
|
||||
|
||||
# ============================================================================
|
||||
# MESSAGES D'AIDE ET UX
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Affichage des règles avant saisie
|
||||
Étant donné l'utilisateur accède au formulaire d'inscription
|
||||
Quand le champ mot de passe reçoit le focus
|
||||
Alors une info-bulle doit s'afficher avec les règles :
|
||||
"""
|
||||
Votre mot de passe doit contenir :
|
||||
• Au moins 8 caractères
|
||||
• Au moins 1 majuscule
|
||||
• Au moins 1 chiffre
|
||||
"""
|
||||
|
||||
Scénario: Indicateur de force du mot de passe
|
||||
Étant donné l'utilisateur saisit progressivement son mot de passe
|
||||
Quand l'utilisateur tape "Weak1"
|
||||
Alors l'indicateur de force doit afficher "Faible" en orange
|
||||
Quand l'utilisateur tape "Medium12"
|
||||
Alors l'indicateur de force doit afficher "Moyen" en jaune
|
||||
Quand l'utilisateur tape "VeryStrong123!"
|
||||
Alors l'indicateur de force doit afficher "Fort" en vert
|
||||
@@ -0,0 +1,79 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Vérification d'email
|
||||
En tant qu'utilisateur inscrit
|
||||
Je veux vérifier mon adresse email
|
||||
Afin d'accéder à toutes les fonctionnalités selon mon rôle
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Auditeur avec email non vérifié - lecture illimitée
|
||||
Étant donné que je suis un auditeur avec email non vérifié
|
||||
Quand j'essaie d'écouter du contenu
|
||||
Alors je peux écouter tous les contenus sans limite
|
||||
|
||||
Scénario: Auditeur avec email non vérifié - création limitée à 5 contenus
|
||||
Étant donné que je suis un auditeur avec email non vérifié
|
||||
Et que j'ai créé 4 contenus
|
||||
Quand je crée un 5ème contenu
|
||||
Alors le contenu est créé avec succès
|
||||
Mais quand j'essaie de créer un 6ème contenu
|
||||
Alors la création échoue
|
||||
Et je vois le message "Vérifiez votre email pour créer plus de contenus"
|
||||
|
||||
Scénario: Rappel de vérification après le 3ème contenu créé
|
||||
Étant donné que je suis un auditeur avec email non vérifié
|
||||
Et que j'ai créé 2 contenus
|
||||
Quand je crée mon 3ème contenu
|
||||
Alors le contenu est créé avec succès
|
||||
Et je vois une notification in-app "Vérifiez votre email pour débloquer la création illimitée"
|
||||
|
||||
Scénario: Auditeur vérifie son email
|
||||
Étant donné que je suis un auditeur avec email non vérifié
|
||||
Et que j'ai reçu un lien de vérification
|
||||
Quand je clique sur le lien de vérification dans l'email
|
||||
Alors mon email est marqué comme vérifié
|
||||
Et je vois le message "Email vérifié avec succès"
|
||||
Et toutes les fonctionnalités sont débloquées
|
||||
|
||||
Scénario: Créateur doit vérifier son email sous 7 jours pour monétisation
|
||||
Étant donné que je suis inscrit comme créateur
|
||||
Et que mon email n'est pas vérifié
|
||||
Et que je remplis les conditions de monétisation
|
||||
Quand j'essaie d'accéder au programme de monétisation
|
||||
Alors l'accès est refusé
|
||||
Et je vois le message "Vérifiez votre email pour accéder à la monétisation"
|
||||
|
||||
Scénario: Créateur ne peut pas publier de contenus illimités sans vérification
|
||||
Étant donné que je suis un créateur avec email non vérifié
|
||||
Et que j'ai créé 5 contenus
|
||||
Quand j'essaie de créer un 6ème contenu
|
||||
Alors la création échoue
|
||||
Et je vois le message "Vérifiez votre email pour publier des contenus illimités"
|
||||
|
||||
Scénario: Créateur vérifie son email et déboque tout
|
||||
Étant donné que je suis un créateur avec email non vérifié
|
||||
Et que j'ai reçu un lien de vérification
|
||||
Quand je clique sur le lien de vérification
|
||||
Alors mon email est marqué comme vérifié
|
||||
Et je peux publier des contenus illimités
|
||||
Et je peux accéder au programme de monétisation si j'en remplis les conditions
|
||||
|
||||
Scénario: KYC impossible sans email vérifié
|
||||
Étant donné que je suis un créateur avec email non vérifié
|
||||
Quand j'essaie de compléter le KYC via Mangopay
|
||||
Alors l'accès au KYC est refusé
|
||||
Et je vois le message "Vérifiez votre email avant de procéder au KYC"
|
||||
|
||||
Scénario: Tentative de vérification avec un lien déjà utilisé
|
||||
Étant donné que j'ai déjà vérifié mon email avec un lien
|
||||
Quand j'essaie de réutiliser le même lien de vérification
|
||||
Alors la vérification échoue
|
||||
Et je vois le message "Ce lien a déjà été utilisé"
|
||||
|
||||
Scénario: Auditeur vérifié peut créer plus de 5 contenus
|
||||
Étant donné que je suis un auditeur avec email vérifié
|
||||
Et que j'ai créé 10 contenus
|
||||
Quand je crée un 11ème contenu
|
||||
Alors le contenu est créé avec succès
|
||||
Et il n'y a pas de limite de création
|
||||
@@ -0,0 +1,115 @@
|
||||
# language: fr
|
||||
|
||||
@error-handling @no-content
|
||||
Fonctionnalité: Élargissement automatique de zone quand aucun contenu n'est disponible
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que la géolocalisation est activée
|
||||
Et que je suis en mode écoute
|
||||
|
||||
# 12.1 - Élargissement progressif
|
||||
|
||||
Scénario: Aucun contenu dans rayon 50km - élargissement à 100km
|
||||
Étant donné que je suis situé à la position GPS 48.8566, 2.3522
|
||||
Et qu'aucun contenu n'existe dans un rayon de 50 km autour de ma position
|
||||
Mais qu'au moins 1 contenu existe dans un rayon de 100 km
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors le système élargit automatiquement la zone de recherche à 100 km
|
||||
Et je reçois un message "Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km)"
|
||||
Et un contenu dans le rayon de 100 km m'est proposé
|
||||
|
||||
Scénario: Aucun contenu dans rayon 100km - élargissement au département
|
||||
Étant donné que je suis situé dans le département "75" (Paris)
|
||||
Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position
|
||||
Mais qu'au moins 1 contenu existe avec la zone "département" pour "75"
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors le système élargit automatiquement la zone de recherche au département
|
||||
Et je reçois un message "Aucun contenu local disponible. Voici du contenu dans votre département"
|
||||
Et un contenu départemental m'est proposé
|
||||
|
||||
Scénario: Aucun contenu départemental - élargissement à la région
|
||||
Étant donné que je suis situé dans la région "Île-de-France"
|
||||
Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position
|
||||
Et qu'aucun contenu départemental n'existe pour mon département
|
||||
Mais qu'au moins 1 contenu existe avec la zone "région" pour "Île-de-France"
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors le système élargit automatiquement la zone de recherche à la région
|
||||
Et je reçois un message "Aucun contenu local disponible. Voici du contenu dans votre région"
|
||||
Et un contenu régional m'est proposé
|
||||
|
||||
Scénario: Aucun contenu régional - basculement sur contenu national
|
||||
Étant donné que je suis situé en France
|
||||
Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position
|
||||
Et qu'aucun contenu départemental n'existe pour mon département
|
||||
Et qu'aucun contenu régional n'existe pour ma région
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors le système bascule automatiquement sur du contenu national
|
||||
Et je reçois un message "Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser"
|
||||
Et un contenu national m'est proposé
|
||||
Et je ne reste jamais sans contenu disponible
|
||||
|
||||
Scénario: Élargissement progressif avec plusieurs étapes
|
||||
Étant donné que je suis situé dans une zone rurale isolée
|
||||
Et qu'aucun contenu n'existe dans un rayon de 50 km
|
||||
Et qu'aucun contenu n'existe dans un rayon de 100 km
|
||||
Et qu'aucun contenu départemental n'existe
|
||||
Et qu'aucun contenu régional n'existe
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors le système essaie d'abord 50 km
|
||||
Puis essaie 100 km
|
||||
Puis essaie le département
|
||||
Puis essaie la région
|
||||
Puis bascule sur le contenu national
|
||||
Et tout ce processus se fait de manière transparente et automatique
|
||||
Et je reçois le message correspondant au dernier niveau trouvé
|
||||
|
||||
# Messages adaptatifs selon le niveau d'élargissement
|
||||
|
||||
Scénario: Message personnalisé selon la distance trouvée
|
||||
Étant donné que je suis situé à la position GPS 43.6047, 1.4442
|
||||
Et que <niveau_geo> contenu(s) est/sont trouvé(s)
|
||||
Quand le système me propose du contenu
|
||||
Alors je reçois le message "<message_attendu>"
|
||||
|
||||
Exemples:
|
||||
| niveau_geo | message_attendu |
|
||||
| 100 km | Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km) |
|
||||
| département | Aucun contenu local disponible. Voici du contenu dans votre département |
|
||||
| région | Aucun contenu local disponible. Voici du contenu dans votre région |
|
||||
| national | Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser|
|
||||
|
||||
# Filet de sécurité - toujours du contenu disponible
|
||||
|
||||
Scénario: Le contenu national sert de filet de sécurité
|
||||
Étant donné que le système a épuisé toutes les zones géographiques locales
|
||||
Quand le système bascule sur du contenu national
|
||||
Alors je dois toujours avoir au moins 1 contenu disponible
|
||||
Et ce contenu peut être:
|
||||
| type_contenu |
|
||||
| Actualités Le Monde |
|
||||
| Podcasts génériques |
|
||||
| Contenu éducatif national |
|
||||
| Contenu culturel national |
|
||||
|
||||
# Pas de message d'erreur bloquant
|
||||
|
||||
Scénario: Pas d'écran d'erreur "Aucun contenu"
|
||||
Étant donné que je lance l'application
|
||||
Et qu'aucun contenu local n'est disponible dans ma zone
|
||||
Quand le système recherche du contenu
|
||||
Alors je ne dois jamais voir un message d'erreur "Aucun contenu disponible"
|
||||
Et je ne dois jamais voir un écran vide
|
||||
Et un contenu doit toujours m'être proposé, même si c'est du contenu national
|
||||
|
||||
# Comportement avec historique d'écoute
|
||||
|
||||
Scénario: Élargissement avec prise en compte des centres d'intérêt
|
||||
Étant donné que je suis situé dans une zone rurale
|
||||
Et qu'aucun contenu n'existe dans un rayon de 50 km
|
||||
Et que mes centres d'intérêt incluent "Automobile" à 80% et "Voyage" à 70%
|
||||
Et qu'un contenu national existe avec le tag "Automobile"
|
||||
Et qu'un contenu national existe avec le tag "Politique"
|
||||
Quand le système bascule sur du contenu national
|
||||
Alors le contenu national proposé prend en compte mes centres d'intérêt
|
||||
Et le contenu "Automobile" a un score supérieur au contenu "Politique"
|
||||
@@ -0,0 +1,116 @@
|
||||
# language: fr
|
||||
|
||||
@error-handling @content-removal
|
||||
Fonctionnalité: Gestion d'un contenu supprimé pendant l'écoute
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que je suis en mode écoute
|
||||
Et qu'un contenu "C123" est en cours de lecture
|
||||
|
||||
# 12.2 - Pas d'interruption brutale
|
||||
|
||||
Scénario: Contenu supprimé pendant lecture - fin de lecture sans interruption
|
||||
Étant donné que j'écoute le contenu "C123" depuis 30 secondes
|
||||
Et que la durée totale du contenu est de 120 secondes
|
||||
Quand le contenu est supprimé par la modération côté backend
|
||||
Alors la lecture du contenu continue sans interruption
|
||||
Et je peux écouter le contenu jusqu'à la fin
|
||||
Et aucune interruption brutale ne se produit
|
||||
|
||||
Scénario: Passage automatique après fin du contenu supprimé
|
||||
Étant donné que le contenu "C123" a été supprimé pendant ma lecture
|
||||
Et que j'ai écouté le contenu jusqu'à la fin
|
||||
Quand le contenu se termine
|
||||
Alors le système attend 2 secondes
|
||||
Et passe automatiquement au contenu suivant
|
||||
Et je reçois une notification toast discrète "Contenu précédent retiré (violation règles)"
|
||||
|
||||
Scénario: Bouton Précédent désactivé après suppression
|
||||
Étant donné que le contenu "C123" a été supprimé pendant ma lecture
|
||||
Et que je suis passé au contenu suivant "C456"
|
||||
Quand j'essaie d'appuyer sur le bouton "Précédent"
|
||||
Alors le bouton "Précédent" ne me ramène pas au contenu supprimé
|
||||
Et je reçois un message "Ce contenu n'est plus disponible"
|
||||
Et la lecture du contenu actuel "C456" continue
|
||||
|
||||
Scénario: Tentative de retour manuel au contenu supprimé
|
||||
Étant donné que je suis sur le contenu "C456"
|
||||
Et que le contenu précédent "C123" a été supprimé
|
||||
Quand j'appuie sur le bouton "Précédent" pour revenir au contenu supprimé
|
||||
Alors je reçois un message "Ce contenu n'est plus disponible"
|
||||
Et la lecture reste sur le contenu actuel "C456"
|
||||
Et aucune action n'est effectuée
|
||||
|
||||
# Sécurité routière - pas d'alerte intrusive
|
||||
|
||||
Scénario: Notification discrète pendant la conduite
|
||||
Étant donné que je conduis à une vitesse de 60 km/h
|
||||
Et que le contenu "C123" est supprimé pendant ma lecture
|
||||
Quand le contenu se termine
|
||||
Alors la notification "Contenu précédent retiré (violation règles)" s'affiche en toast discret
|
||||
Et la notification disparaît automatiquement après 5 secondes
|
||||
Et aucune popup modale n'interrompt ma conduite
|
||||
Et le contenu suivant démarre automatiquement après 2 secondes
|
||||
|
||||
Scénario: Message informatif mais non alarmiste
|
||||
Étant donné que le contenu "C123" a été supprimé
|
||||
Et que je passe au contenu suivant
|
||||
Quand la notification s'affiche
|
||||
Alors le message doit être informatif: "Contenu précédent retiré (violation règles)"
|
||||
Et le ton ne doit pas être alarmiste
|
||||
Et le message doit être bref et compréhensible
|
||||
Et aucun détail technique n'est affiché pendant la conduite
|
||||
|
||||
# Empêche la réécoute du contenu modéré
|
||||
|
||||
Scénario: Contenu supprimé retiré de l'historique
|
||||
Étant donné que le contenu "C123" a été supprimé
|
||||
Quand je consulte mon historique d'écoute
|
||||
Alors le contenu "C123" n'apparaît plus dans mon historique
|
||||
Et je ne peux pas relancer la lecture de ce contenu
|
||||
Et l'historique affiche "[Contenu retiré]" à la place du titre
|
||||
|
||||
Scénario: Contenu supprimé non accessible via lien direct
|
||||
Étant donné que le contenu "C123" a été supprimé
|
||||
Et que j'ai un lien de partage "roadwave.fr/share/c/C123"
|
||||
Quand je clique sur le lien de partage
|
||||
Alors je reçois un message "Ce contenu a été retiré pour violation des règles de la communauté"
|
||||
Et je suis redirigé vers l'accueil de l'application
|
||||
Et aucune lecture n'est possible
|
||||
|
||||
# Gestion de plusieurs contenus supprimés consécutivement
|
||||
|
||||
Scénario: Plusieurs contenus supprimés dans l'historique récent
|
||||
Étant donné que j'ai écouté les contenus suivants:
|
||||
| id | statut |
|
||||
| C123 | supprimé |
|
||||
| C456 | actif |
|
||||
| C789 | supprimé |
|
||||
Et que je suis actuellement sur le contenu "C456"
|
||||
Quand j'appuie plusieurs fois sur "Précédent"
|
||||
Alors je ne peux pas revenir aux contenus "C123" ou "C789"
|
||||
Et le système saute automatiquement les contenus supprimés
|
||||
Et je reviens au dernier contenu actif disponible avant "C456"
|
||||
|
||||
# Comportement à l'arrêt du véhicule
|
||||
|
||||
Scénario: Consultation détaillée du contenu supprimé à l'arrêt
|
||||
Étant donné que je suis à l'arrêt
|
||||
Et que le contenu "C123" a été supprimé pendant ma session
|
||||
Quand j'ouvre les détails de la notification de suppression
|
||||
Alors je peux voir les informations suivantes:
|
||||
| information |
|
||||
| Titre du contenu |
|
||||
| Créateur |
|
||||
| Raison de suppression |
|
||||
| Date de suppression |
|
||||
Et je peux signaler une erreur de modération si je pense qu'elle est injustifiée
|
||||
|
||||
Scénario: Pas d'impact sur les jauges d'intérêt lors de la suppression
|
||||
Étant donné que j'ai écouté le contenu "C123" pendant 80 secondes (66%)
|
||||
Et que mes jauges d'intérêt ont été mises à jour pendant l'écoute
|
||||
Quand le contenu est supprimé après mon écoute
|
||||
Alors les modifications de mes jauges d'intérêt sont conservées
|
||||
Et l'écoute déjà effectuée reste comptabilisée
|
||||
Et seules les futures écoutes de ce contenu sont bloquées
|
||||
@@ -0,0 +1,210 @@
|
||||
# language: fr
|
||||
|
||||
@error-handling @gps-disabled
|
||||
Fonctionnalité: Mode dégradé sans géolocalisation
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que j'ai refusé ou désactivé l'accès à la géolocalisation
|
||||
|
||||
# 12.4 - Mode dégradé automatique
|
||||
|
||||
Scénario: Types de contenu disponibles sans géolocalisation
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand j'ouvre l'application
|
||||
Alors les types de contenu suivants sont disponibles:
|
||||
| type_contenu | disponible |
|
||||
| Contenu national | oui |
|
||||
| Contenu téléchargé (offline) | oui |
|
||||
| Contenus "Neutre" géographiquement | oui |
|
||||
| Contenu géolocalisé Ancré | non |
|
||||
| Contenu géolocalisé Contextuel | non |
|
||||
| Audio-guides | non |
|
||||
| Notifications push géo-déclenchées | non |
|
||||
|
||||
# Popup au premier lancement
|
||||
|
||||
Scénario: Popup d'information au premier lancement sans GPS
|
||||
Étant donné que c'est mon premier lancement de l'application
|
||||
Et que j'ai refusé l'accès à la géolocalisation
|
||||
Quand l'application détecte que le GPS est désactivé
|
||||
Alors une popup s'affiche avec le message:
|
||||
"""
|
||||
RoadWave fonctionne mieux avec la géolocalisation activée. Sans elle, seul le contenu national sera disponible.
|
||||
"""
|
||||
Et la popup contient les boutons suivants:
|
||||
| bouton | action |
|
||||
| Activer | Redirection vers paramètres OS |
|
||||
| Continuer sans | Ferme popup et lance en mode dégradé |
|
||||
Et une checkbox "Ne plus me demander" est disponible
|
||||
|
||||
Scénario: Popup non affichée si case "Ne plus me demander" cochée
|
||||
Étant donné que j'ai déjà vu la popup de géolocalisation
|
||||
Et que j'ai coché "Ne plus me demander"
|
||||
Quand je lance l'application avec le GPS désactivé
|
||||
Alors la popup de géolocalisation ne s'affiche pas
|
||||
Et l'application démarre directement en mode dégradé
|
||||
Et le banner permanent de rappel s'affiche
|
||||
|
||||
Scénario: Redirection vers paramètres OS lors du clic sur "Activer"
|
||||
Étant donné que la popup de géolocalisation est affichée
|
||||
Quand je clique sur "Activer"
|
||||
Alors je suis redirigé vers les paramètres de géolocalisation de mon OS
|
||||
Et sur iOS, j'arrive dans "Réglages > RoadWave > Localisation"
|
||||
Et sur Android, j'arrive dans "Paramètres > Applications > RoadWave > Autorisations > Position"
|
||||
|
||||
# Banner permanent en mode dégradé
|
||||
|
||||
Scénario: Banner de rappel permanent sans GPS
|
||||
Étant donné que j'ai cliqué sur "Continuer sans" géolocalisation
|
||||
Quand l'application s'affiche
|
||||
Alors un bandeau s'affiche en haut de l'écran
|
||||
Et le bandeau contient le texte: "Mode limité : géolocalisation désactivée. [Activer]"
|
||||
Et le bandeau a un fond de couleur avertissement (jaune/orange)
|
||||
Et le bandeau n'est pas intrusif mais reste visible
|
||||
Et le bandeau reste affiché sur toutes les pages de l'application
|
||||
|
||||
Scénario: Clic sur le bouton "Activer" du banner
|
||||
Étant donné que le banner "Mode limité" est affiché
|
||||
Quand je clique sur le lien "[Activer]" dans le banner
|
||||
Alors je suis redirigé vers les paramètres de géolocalisation de mon OS
|
||||
|
||||
Scénario: Disparition du banner après activation GPS
|
||||
Étant donné que le banner "Mode limité" est affiché
|
||||
Et que je reviens dans l'application après avoir activé le GPS dans les paramètres
|
||||
Quand l'application détecte que la géolocalisation est maintenant active
|
||||
Alors le banner disparaît automatiquement
|
||||
Et l'application bascule en mode normal avec contenu géolocalisé
|
||||
Et un toast de confirmation s'affiche: "Géolocalisation activée"
|
||||
|
||||
# Contenu disponible en mode dégradé
|
||||
|
||||
Scénario: Lecture de contenu national sans GPS
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Et que du contenu national existe (actualités Le Monde, podcasts génériques)
|
||||
Quand je lance la lecture
|
||||
Alors je peux écouter le contenu national sans restriction
|
||||
Et l'algorithme de recommandation se base uniquement sur:
|
||||
| critère |
|
||||
| Mes centres d'intérêt |
|
||||
| Mon historique d'écoute |
|
||||
| Popularité générale |
|
||||
Et la proximité géographique n'est pas prise en compte
|
||||
|
||||
Scénario: Lecture de contenu téléchargé sans GPS
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Et que j'ai téléchargé 30 contenus quand j'avais le GPS activé
|
||||
Quand j'accède à mes contenus téléchargés
|
||||
Alors je peux lire tous mes contenus téléchargés normalement
|
||||
Et les contenus géolocalisés téléchargés restent accessibles
|
||||
Et le filtre géographique n'est pas appliqué pour les contenus offline
|
||||
|
||||
Scénario: Contenu "Neutre" géographiquement disponible
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Et qu'un créateur a publié du contenu avec la classification géographique "Neutre"
|
||||
Quand je recherche du contenu
|
||||
Alors les contenus "Neutre" sont inclus dans les résultats
|
||||
Et ils sont mélangés avec le contenu national
|
||||
Et l'algorithme les priorise selon mes centres d'intérêt
|
||||
|
||||
# Restrictions de contenu sans GPS
|
||||
|
||||
Scénario: Audio-guides inaccessibles sans GPS
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand je recherche un audio-guide spécifique
|
||||
Alors les audio-guides apparaissent dans les résultats de recherche
|
||||
Mais un badge "GPS requis" est affiché sur chaque audio-guide
|
||||
Et quand je clique sur un audio-guide, un message s'affiche:
|
||||
"""
|
||||
Les audio-guides nécessitent la géolocalisation pour fonctionner. Voulez-vous activer le GPS ?
|
||||
"""
|
||||
Et je peux choisir "Activer" ou "Annuler"
|
||||
|
||||
Scénario: Notifications push géo-déclenchées désactivées
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Et que je suis abonné à un créateur qui diffuse du contenu géolocalisé
|
||||
Quand le créateur publie un nouveau contenu géolocalisé
|
||||
Alors je ne reçois pas de notification push géo-déclenchée
|
||||
Mais je reçois une notification push standard (non géo-déclenchée) si le créateur publie du contenu national
|
||||
Et la notification précise: "Nouveau contenu national de [Créateur]"
|
||||
|
||||
Scénario: Contenu géolocalisé non proposé dans le feed
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand le système génère mon feed de contenu
|
||||
Alors aucun contenu "Ancré" ou "Contextuel" n'est inclus
|
||||
Et seuls les contenus "Neutre" et "National" sont proposés
|
||||
Et mon feed contient au minimum 20 contenus disponibles
|
||||
|
||||
# RGPD: Respect du consentement libre
|
||||
|
||||
Scénario: Application fonctionnelle sans GPS (pas de blocage)
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand j'utilise l'application
|
||||
Alors je ne suis jamais bloqué par un écran "GPS requis"
|
||||
Et toutes les fonctionnalités non-géolocalisées restent accessibles:
|
||||
| fonctionnalité |
|
||||
| Écoute contenu national |
|
||||
| Gestion profil |
|
||||
| Abonnements créateurs |
|
||||
| Recherche textuelle |
|
||||
| Historique d'écoute |
|
||||
| Paramètres |
|
||||
| Mode offline |
|
||||
Et je peux créer et publier du contenu national
|
||||
|
||||
Scénario: Respect du choix utilisateur de ne pas activer GPS
|
||||
Étant donné que j'ai coché "Ne plus me demander" pour la géolocalisation
|
||||
Quand j'utilise l'application pendant plusieurs semaines
|
||||
Alors la popup de demande GPS ne s'affiche plus jamais automatiquement
|
||||
Et seul le banner permanent reste affiché
|
||||
Et l'application ne force jamais l'activation du GPS
|
||||
|
||||
# Réactivation GPS et bascule en mode normal
|
||||
|
||||
Scénario: Bascule automatique en mode normal après activation GPS
|
||||
Étant donné que j'utilise l'application en mode dégradé depuis 1 semaine
|
||||
Et que je décide d'activer la géolocalisation
|
||||
Quand l'application détecte que le GPS est maintenant actif
|
||||
Alors le mode dégradé est désactivé automatiquement
|
||||
Et le banner "Mode limité" disparaît
|
||||
Et le contenu géolocalisé devient disponible immédiatement
|
||||
Et mon feed se rafraîchit avec du contenu local pertinent
|
||||
Et un toast de confirmation s'affiche: "Géolocalisation activée - Contenu local disponible"
|
||||
|
||||
Scénario: Demande de permission GPS lors de l'utilisation d'une fonctionnalité géo
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand j'essaie d'accéder à une fonctionnalité nécessitant le GPS (ex: audio-guide)
|
||||
Alors une popup contextuelle s'affiche:
|
||||
"""
|
||||
Cette fonctionnalité nécessite la géolocalisation. Voulez-vous l'activer ?
|
||||
"""
|
||||
Et je peux accepter ou refuser
|
||||
Et si j'accepte, je suis redirigé vers les paramètres OS
|
||||
Et si je refuse, je reste en mode dégradé sans message d'erreur répétitif
|
||||
|
||||
# Incitation à activer GPS sans être intrusif
|
||||
|
||||
Scénario: Statistiques de contenu local disponible non affiché
|
||||
Étant donné que la géolocalisation est désactivée
|
||||
Quand je navigue dans l'application
|
||||
Alors le banner peut afficher occasionnellement:
|
||||
"""
|
||||
Mode limité : géolocalisation désactivée. Plus de 500 contenus locaux disponibles près de vous. [Activer]
|
||||
"""
|
||||
Et ce message incitatif change tous les 3 jours
|
||||
Et il reste non intrusif (pas de popup, juste le banner)
|
||||
|
||||
Scénario: Onboarding différent pour utilisateurs sans GPS
|
||||
Étant donné que c'est ma première utilisation de RoadWave
|
||||
Et que j'ai refusé la géolocalisation
|
||||
Quand l'onboarding se termine
|
||||
Alors un écran explicatif s'affiche:
|
||||
"""
|
||||
Vous utilisez RoadWave sans géolocalisation. Voici ce qui est disponible :
|
||||
- Contenu national (actualités, podcasts)
|
||||
- Contenus téléchargés
|
||||
- Tous les créateurs et abonnements
|
||||
|
||||
Pour profiter pleinement de RoadWave (contenu local, audio-guides), activez la géolocalisation à tout moment.
|
||||
"""
|
||||
Et je peux continuer avec un bouton "Compris"
|
||||
@@ -0,0 +1,179 @@
|
||||
# language: fr
|
||||
|
||||
@error-handling @network-loss
|
||||
Fonctionnalité: Gestion de la perte de réseau et buffering adaptatif
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que je suis en mode écoute
|
||||
Et qu'un contenu est en cours de lecture
|
||||
|
||||
# 12.3 - Buffer adaptatif selon type de réseau (cf. TECHNICAL.md et ADR-002)
|
||||
|
||||
Plan du Scénario: Paramètres de buffer selon le type de réseau
|
||||
Étant donné que je suis connecté en "<type_reseau>"
|
||||
Quand le système initialise le buffer audio
|
||||
Alors le buffer minimum est de <buffer_min> secondes
|
||||
Et le buffer cible est de <buffer_cible> secondes
|
||||
Et le buffer maximum est de <buffer_max> secondes
|
||||
|
||||
Exemples:
|
||||
| type_reseau | buffer_min | buffer_cible | buffer_max |
|
||||
| WiFi | 5 | 30 | 120 |
|
||||
| 4G | 10 | 45 | 120 |
|
||||
| 5G | 10 | 45 | 120 |
|
||||
| 3G | 30 | 90 | 300 |
|
||||
|
||||
# Phase 1: Connexion instable
|
||||
|
||||
Scénario: Connexion instable avec latence élevée - aucun message immédiat
|
||||
Étant donné que je suis connecté en 4G
|
||||
Et que le buffer contient 45 secondes de contenu
|
||||
Quand la latence réseau dépasse 500ms
|
||||
Alors aucun message n'est affiché immédiatement
|
||||
Et la lecture continue normalement sur le buffer
|
||||
Et le système tente de continuer le téléchargement en arrière-plan
|
||||
|
||||
Scénario: Connexion instable pendant plus de 10 secondes - toast discret
|
||||
Étant donné que je suis connecté en 4G
|
||||
Et que la latence réseau dépasse 500ms depuis 10 secondes
|
||||
Quand le système détecte la latence prolongée
|
||||
Alors un toast discret s'affiche: "Connexion instable"
|
||||
Et le toast disparaît automatiquement après 3 secondes
|
||||
Et la lecture continue normalement
|
||||
|
||||
# Phase 2: Perte totale réseau
|
||||
|
||||
Scénario: Perte totale de réseau - lecture sur buffer
|
||||
Étant donné que je suis connecté en WiFi
|
||||
Et que le buffer contient 30 secondes de contenu
|
||||
Quand je perds totalement la connexion réseau
|
||||
Alors la lecture continue sur le buffer disponible
|
||||
Et un toast s'affiche: "Hors ligne, lecture sur buffer (30s restantes)"
|
||||
Et un compte à rebours du temps de buffer restant est visible
|
||||
|
||||
Scénario: Buffer qui s'épuise pendant la perte réseau
|
||||
Étant donné que je suis hors ligne
|
||||
Et que le buffer contient 30 secondes de contenu
|
||||
Quand le contenu continue de jouer
|
||||
Alors le compte à rebours diminue en temps réel
|
||||
Et le toast affiche "Hors ligne, lecture sur buffer (15s restantes)" après 15 secondes
|
||||
Et le toast affiche "Hors ligne, lecture sur buffer (5s restantes)" après 25 secondes
|
||||
|
||||
# Phase 3: Buffer épuisé sans reconnexion
|
||||
|
||||
Scénario: Pause automatique après épuisement du buffer
|
||||
Étant donné que je suis hors ligne depuis 30 secondes
|
||||
Et que le buffer est complètement épuisé
|
||||
Quand il n'y a plus de contenu audio à lire
|
||||
Alors la lecture se met en pause automatiquement
|
||||
Et un overlay s'affiche: "Connexion perdue. Reconnexion en cours..."
|
||||
Et le système tente de se reconnecter automatiquement
|
||||
|
||||
Scénario: Tentatives de reconnexion automatique
|
||||
Étant donné que la lecture est en pause suite à l'épuisement du buffer
|
||||
Quand le système tente de se reconnecter
|
||||
Alors une tentative de reconnexion est effectuée toutes les 5 secondes
|
||||
Et un maximum de 6 tentatives sont effectuées (30 secondes au total)
|
||||
Et l'overlay affiche "Tentative de reconnexion... (X/6)"
|
||||
|
||||
# Phase 4: Basculement mode offline après échec reconnexion
|
||||
|
||||
Scénario: Proposition du mode offline après 30 secondes d'échec
|
||||
Étant donné que 6 tentatives de reconnexion ont échoué
|
||||
Et que cela fait 30 secondes que je suis déconnecté
|
||||
Quand la 6ème tentative échoue
|
||||
Alors une popup s'affiche: "Voulez-vous continuer avec vos contenus téléchargés ?"
|
||||
Et la popup contient deux boutons:
|
||||
| bouton | action |
|
||||
| Réessayer | Nouvelle série de 6 tentatives |
|
||||
| Mode offline | Bascule sur contenus téléchargés |
|
||||
|
||||
Scénario: Basculement réussi vers le mode offline
|
||||
Étant donné que la popup de mode offline est affichée
|
||||
Et que j'ai téléchargé 20 contenus dans ma zone géographique
|
||||
Quand je clique sur "Mode offline"
|
||||
Alors le système bascule sur les contenus téléchargés
|
||||
Et un nouveau contenu téléchargé démarre automatiquement
|
||||
Et un bandeau permanent indique "Mode hors ligne - Contenus téléchargés"
|
||||
|
||||
Scénario: Aucun contenu téléchargé disponible
|
||||
Étant donné que la popup de mode offline est affichée
|
||||
Et que je n'ai aucun contenu téléchargé
|
||||
Quand je clique sur "Mode offline"
|
||||
Alors un message s'affiche: "Aucun contenu téléchargé disponible"
|
||||
Et je suis invité à me connecter en WiFi pour télécharger du contenu
|
||||
Et le bouton "Réessayer" reste la seule option
|
||||
|
||||
# Reconnexion réussie
|
||||
|
||||
Scénario: Reprise automatique après reconnexion
|
||||
Étant donné que la lecture est en pause depuis 15 secondes
|
||||
Et que j'étais à 02:35 du contenu en cours
|
||||
Quand la connexion réseau est rétablie
|
||||
Alors la lecture reprend automatiquement au point d'arrêt exact (02:35)
|
||||
Et un toast s'affiche: "Connexion rétablie"
|
||||
Et le toast disparaît après 3 secondes
|
||||
Et le buffer se remplit progressivement selon le type de réseau
|
||||
|
||||
Scénario: Reconnexion avec changement de type de réseau
|
||||
Étant donné que j'étais connecté en WiFi
|
||||
Et que j'ai perdu la connexion
|
||||
Quand je me reconnecte en 4G
|
||||
Alors le système ajuste automatiquement les paramètres de buffer
|
||||
Et le buffer minimum passe de 5s à 10s
|
||||
Et le buffer cible passe de 30s à 45s
|
||||
Et la lecture reprend normalement
|
||||
|
||||
# Cas spécifique: tunnel routier
|
||||
|
||||
Scénario: Passage dans un tunnel avec perte de signal
|
||||
Étant donné que je conduis à 90 km/h sur autoroute
|
||||
Et que je suis connecté en 4G avec un buffer de 45 secondes
|
||||
Quand j'entre dans un tunnel et perds le signal
|
||||
Alors la lecture continue sur le buffer pendant 45 secondes maximum
|
||||
Et aucune notification n'est affichée pendant les 10 premières secondes
|
||||
Et un toast discret s'affiche après 10 secondes: "Connexion instable"
|
||||
|
||||
Scénario: Sortie du tunnel avant épuisement du buffer
|
||||
Étant donné que je suis dans un tunnel depuis 30 secondes
|
||||
Et qu'il reste 15 secondes de buffer
|
||||
Quand je sors du tunnel et récupère le signal 4G
|
||||
Alors la lecture continue sans interruption
|
||||
Et le buffer se remplit à nouveau
|
||||
Et un toast s'affiche: "Connexion rétablie"
|
||||
|
||||
# Handoff réseau (changement de cellule mobile)
|
||||
|
||||
Scénario: Changement de cellule 4G pendant la lecture
|
||||
Étant donné que je conduis et change de cellule mobile toutes les 5-10 minutes
|
||||
Et que le buffer contient 45 secondes de contenu
|
||||
Quand un handoff de cellule se produit
|
||||
Alors la lecture continue sans interruption grâce au buffer
|
||||
Et la connexion à la nouvelle cellule se fait de manière transparente
|
||||
Et aucune notification n'est affichée si le handoff réussit en moins de 5 secondes
|
||||
|
||||
# Mode offline préventif avant perte réseau
|
||||
|
||||
Scénario: Téléchargement préventif en WiFi avant trajet
|
||||
Étant donné que je suis connecté en WiFi
|
||||
Et que j'ai activé le téléchargement automatique
|
||||
Quand le système détecte que je suis à l'arrêt en WiFi
|
||||
Alors le système me propose de télécharger du contenu pour mon trajet
|
||||
Et je peux sélectionner une zone géographique à télécharger
|
||||
Et le téléchargement se fait en arrière-plan
|
||||
|
||||
# Métriques et monitoring
|
||||
|
||||
Scénario: Tracking des événements de perte réseau pour amélioration
|
||||
Étant donné que je perds la connexion réseau
|
||||
Quand l'événement de perte est détecté
|
||||
Alors le système enregistre les métriques suivantes:
|
||||
| métrique |
|
||||
| Type de réseau avant perte |
|
||||
| Durée de la coupure |
|
||||
| Buffer disponible |
|
||||
| Position GPS approximative |
|
||||
| Heure de la journée |
|
||||
Et ces métriques sont anonymisées et envoyées en batch lors de la prochaine connexion WiFi
|
||||
Et les données servent à améliorer les paramètres de buffer
|
||||
@@ -0,0 +1,67 @@
|
||||
# language: fr
|
||||
|
||||
@ui @sharing @premium @viral @mvp
|
||||
Fonctionnalité: Partage de contenu Premium pour viralité
|
||||
|
||||
En tant qu'utilisateur Premium
|
||||
Je veux partager mes découvertes
|
||||
Afin de recommander la plateforme à mes amis
|
||||
|
||||
Scénario: Partage d'un audio-guide avec preview
|
||||
Étant donné un utilisateur "alice@roadwave.fr" Premium
|
||||
Quand elle partage l'audio-guide "Visite du Louvre"
|
||||
Alors un lien unique est généré: roadwave.fr/share/abc123
|
||||
Et le lien affiche une preview attractive:
|
||||
| Élément | Contenu |
|
||||
| Image cover | Photo du Louvre |
|
||||
| Titre | Visite du Louvre |
|
||||
| Description | Découvrez 3000 ans d'art... |
|
||||
| Durée | 2h 30min - 12 séquences |
|
||||
| Note | 4.8/5 (1,234 avis) |
|
||||
| Créateur | @MuseeDuLouvre |
|
||||
| CTA | [Écouter gratuitement] |
|
||||
Et un événement "CONTENT_SHARED" est enregistré
|
||||
|
||||
Scénario: Essai gratuit de 3 jours pour contenu partagé
|
||||
Étant donné un utilisateur Free qui clique sur un lien partagé
|
||||
Quand il consulte un contenu Premium
|
||||
Alors une offre s'affiche: "Essai gratuit 3 jours offerts par votre ami"
|
||||
Et il peut écouter le contenu sans payer
|
||||
Et un événement "FREE_TRIAL_FROM_SHARE" est enregistré
|
||||
|
||||
Scénario: Programme de parrainage avec récompenses
|
||||
Étant donné un utilisateur Premium qui partage
|
||||
Quand 3 amis s'abonnent via son lien
|
||||
Alors il reçoit 1 mois gratuit par ami converti
|
||||
Et un badge "Ambassadeur" s'affiche sur son profil
|
||||
Et un événement "REFERRAL_REWARDS_GRANTED" est enregistré
|
||||
|
||||
Scénario: Statistiques de partage
|
||||
Étant donné un utilisateur "bob@roadwave.fr"
|
||||
Quand il consulte ses statistiques de partage
|
||||
Alors il voit:
|
||||
| Métrique | Valeur |
|
||||
| Contenus partagés | 12 |
|
||||
| Clics sur liens | 45 |
|
||||
| Amis convertis | 3 |
|
||||
| Mois gratuits gagnés | 3 |
|
||||
Et un événement "SHARE_STATS_VIEWED" est enregistré
|
||||
|
||||
Scénario: Partage optimisé pour réseaux sociaux
|
||||
Étant donné un lien partagé sur Facebook
|
||||
Alors les Open Graph tags sont optimisés:
|
||||
| Tag | Valeur |
|
||||
| og:title | Visite du Louvre - RoadWave |
|
||||
| og:image | Image haute résolution |
|
||||
| og:description| Description accrocheuse |
|
||||
Et génère un maximum d'engagement
|
||||
Et un événement "SOCIAL_SHARE_OPTIMIZED" est enregistré
|
||||
|
||||
Scénario: Métriques de viralité
|
||||
Étant donné 1000 partages effectués
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de clic sur partage | 18% |
|
||||
| Taux de conversion | 12% |
|
||||
| K-factor (viralité) | 1.3 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
206
docs/domains/_shared/features/partage/partage-contenu.feature
Normal file
206
docs/domains/_shared/features/partage/partage-contenu.feature
Normal file
@@ -0,0 +1,206 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Partage de contenu
|
||||
En tant qu'utilisateur de RoadWave
|
||||
Je veux pouvoir partager du contenu audio
|
||||
Afin de faire découvrir l'application à d'autres personnes
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté
|
||||
|
||||
# 15.1.1 - Bouton "Partager"
|
||||
|
||||
Scénario: Bouton partager disponible dans le player en lecture
|
||||
Étant donné que le contenu "Balade à Paris" est en cours de lecture
|
||||
Quand l'utilisateur consulte les contrôles du player
|
||||
Alors le bouton "Partager" ⬆️ est visible
|
||||
|
||||
Scénario: Bouton partager disponible sur la page profil créateur
|
||||
Étant donné que l'utilisateur consulte le profil de "@paris_stories"
|
||||
Quand l'utilisateur consulte un contenu dans la liste
|
||||
Alors le bouton "Partager" est disponible pour chaque contenu
|
||||
|
||||
Scénario: Bouton partager dans la liste de recherche
|
||||
Étant donné que l'utilisateur effectue une recherche "voyage paris"
|
||||
Quand l'utilisateur ouvre le menu contextuel d'un résultat
|
||||
Alors l'option "Partager" est disponible
|
||||
|
||||
Scénario: Bouton partager dans l'historique personnel
|
||||
Étant donné que l'utilisateur consulte son historique d'écoute
|
||||
Quand l'utilisateur sélectionne un contenu de l'historique
|
||||
Alors le bouton "Partager" est accessible
|
||||
|
||||
Plan du Scénario: Menu de partage avec options multiples
|
||||
Étant donné que le contenu "<contenu>" est disponible
|
||||
Quand l'utilisateur clique sur le bouton "Partager"
|
||||
Alors le menu natif OS s'ouvre
|
||||
Et les options suivantes sont disponibles:
|
||||
| option |
|
||||
| Copier le lien |
|
||||
| WhatsApp |
|
||||
| Email |
|
||||
| SMS |
|
||||
| Plus... |
|
||||
|
||||
Exemples:
|
||||
| contenu |
|
||||
| Balade à Paris |
|
||||
| Secrets de Montmartre |
|
||||
|
||||
# 15.1.2 - Comportement du lien partagé
|
||||
|
||||
Scénario: Génération du lien de partage
|
||||
Étant donné un contenu avec l'ID "content_12345"
|
||||
Quand l'utilisateur copie le lien de partage
|
||||
Alors le lien généré est "https://roadwave.fr/share/c/content_12345"
|
||||
|
||||
Scénario: Ouverture du lien partagé avec l'application installée (Deep link)
|
||||
Étant donné que l'application RoadWave est installée sur l'appareil
|
||||
Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé
|
||||
Quand l'utilisateur clique sur le lien
|
||||
Alors l'application RoadWave s'ouvre automatiquement
|
||||
Et le contenu "content_12345" commence à jouer
|
||||
|
||||
Scénario: Ouverture du lien partagé sans l'application installée (Web player)
|
||||
Étant donné que l'application RoadWave n'est pas installée
|
||||
Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé
|
||||
Quand l'utilisateur clique sur le lien
|
||||
Alors une page web responsive s'affiche
|
||||
Et le web player HTML5 est visible
|
||||
Et les boutons de téléchargement App Store et Google Play sont affichés
|
||||
|
||||
Scénario: Contenu de la page web de partage
|
||||
Étant donné un contenu public avec les métadonnées suivantes:
|
||||
| champ | valeur |
|
||||
| titre | Balade à Paris |
|
||||
| créateur | @paris_stories |
|
||||
| durée | 12 min |
|
||||
| écoutes | 2300 |
|
||||
| localisation | Paris 5e |
|
||||
| type_geo | Ancré |
|
||||
| tags | Voyage, Histoire |
|
||||
Quand la page de partage est affichée
|
||||
Alors la page contient:
|
||||
| élément |
|
||||
| Cover image 16:9 |
|
||||
| Titre "Balade à Paris" |
|
||||
| "@paris_stories" |
|
||||
| "12 min · 🎧 2.3K" |
|
||||
| "📍 Paris 5e · Ancré" |
|
||||
| "🏷️ #Voyage #Histoire" |
|
||||
| Description |
|
||||
| Player HTML5 |
|
||||
| Bouton App Store |
|
||||
| Bouton Google Play |
|
||||
|
||||
Scénario: Métadonnées Open Graph pour partage social
|
||||
Étant donné un contenu "Balade à Paris" par "@paris_stories"
|
||||
Quand la page de partage est générée
|
||||
Alors les métadonnées Open Graph incluent:
|
||||
| propriété | valeur |
|
||||
| og:title | Balade à Paris - RoadWave |
|
||||
| og:description | Écoutez ce contenu par @paris_stories |
|
||||
| og:type | music.song |
|
||||
| og:site_name | RoadWave |
|
||||
| twitter:card | player |
|
||||
Et l'aperçu s'affiche correctement sur WhatsApp
|
||||
Et l'aperçu s'affiche correctement sur Facebook
|
||||
Et l'aperçu s'affiche correctement sur Twitter
|
||||
|
||||
Plan du Scénario: Deep linking par plateforme
|
||||
Étant donné que l'application RoadWave est installée sur <plateforme>
|
||||
Et qu'un lien de partage est ouvert
|
||||
Quand le système détecte l'application
|
||||
Alors l'application s'ouvre via <mécanisme>
|
||||
|
||||
Exemples:
|
||||
| plateforme | mécanisme |
|
||||
| iOS | Universal Links |
|
||||
| Android | App Links |
|
||||
|
||||
Scénario: Fallback URL scheme pour deep linking
|
||||
Étant donné que les App Links ne fonctionnent pas
|
||||
Quand le système tente d'ouvrir le contenu
|
||||
Alors l'URL scheme "roadwave://content/content_12345" est utilisé
|
||||
|
||||
# 15.1.3 - Contenus Premium partagés
|
||||
|
||||
Scénario: Badge Premium visible sur le lien partagé
|
||||
Étant donné un contenu Premium "Visite VIP Louvre"
|
||||
Quand l'utilisateur non-premium clique sur le lien partagé
|
||||
Alors la page web affiche le badge "👑 Contenu Premium"
|
||||
|
||||
Scénario: Preview 30 secondes d'un contenu Premium partagé
|
||||
Étant donné un contenu Premium "Visite VIP Louvre" de 15 minutes
|
||||
Et qu'un utilisateur non-premium ouvre le lien partagé
|
||||
Quand le player démarre automatiquement
|
||||
Alors l'audio joue pendant 30 secondes exactement
|
||||
Et un fade out de 2 secondes est appliqué
|
||||
Et un overlay "Contenu réservé Premium" s'affiche après 32 secondes
|
||||
|
||||
Scénario: Contenu de l'overlay paywall Premium
|
||||
Étant donné qu'un contenu Premium a atteint la limite de 30 secondes
|
||||
Quand l'overlay paywall s'affiche
|
||||
Alors le texte suivant est visible:
|
||||
"""
|
||||
👑 Contenu réservé Premium
|
||||
|
||||
Profitez de ce contenu complet
|
||||
et de milliers d'autres
|
||||
sans publicité
|
||||
|
||||
[Passer Premium - 4.99€/mois]
|
||||
[Télécharger l'app]
|
||||
"""
|
||||
|
||||
Scénario: Actions disponibles sur l'overlay Premium
|
||||
Étant donné que l'overlay paywall Premium est affiché
|
||||
Quand l'utilisateur consulte les options
|
||||
Alors les actions suivantes sont disponibles:
|
||||
| action | comportement |
|
||||
| Passer Premium | Redirection vers paiement Mangopay web |
|
||||
| Télécharger l'app | Redirection vers App Store/Google Play |
|
||||
| Rejouer les 30 premières sec | Relecture illimitée du preview |
|
||||
|
||||
Scénario: Relecture illimitée du preview Premium
|
||||
Étant donné un contenu Premium partagé
|
||||
Et que l'utilisateur a écouté les 30 premières secondes
|
||||
Quand l'utilisateur clique sur "Rejouer"
|
||||
Alors les 30 premières secondes sont rejouées
|
||||
Et cette action est possible de manière illimitée
|
||||
|
||||
Scénario: Tracking des partages Premium
|
||||
Étant donné un créateur "@guide_louvre" avec un contenu Premium
|
||||
Quand son contenu est partagé
|
||||
Alors les métriques suivantes sont enregistrées:
|
||||
| métrique | valeur |
|
||||
| Partages Premium | +1 |
|
||||
| Ouvertures lien | compteur |
|
||||
| Conversions Premium | si souscription |
|
||||
|
||||
Scénario: Rémunération créateur sur conversion Premium via partage
|
||||
Étant donné un contenu Premium partagé par "@guide_louvre"
|
||||
Quand un utilisateur s'abonne via le lien partagé
|
||||
Alors le créateur reçoit 70% des revenus de cet abonnement
|
||||
Et la conversion est trackée dans son dashboard
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Partage d'un contenu supprimé
|
||||
Étant donné qu'un lien de partage "https://roadwave.fr/share/c/deleted_content" est ouvert
|
||||
Et que le contenu n'existe plus
|
||||
Quand la page web se charge
|
||||
Alors un message "Ce contenu n'est plus disponible" s'affiche
|
||||
Et les boutons de téléchargement de l'app sont affichés
|
||||
|
||||
Scénario: Partage d'un contenu en attente de modération
|
||||
Étant donné un contenu en cours de validation modération
|
||||
Quand un lien de partage est ouvert
|
||||
Alors le message "Ce contenu est en cours de validation" s'affiche
|
||||
|
||||
Scénario: Génération du lien hors connexion
|
||||
Étant donné que l'utilisateur n'a pas de connexion réseau
|
||||
Quand l'utilisateur tente de partager un contenu
|
||||
Alors le lien est copié dans le presse-papiers
|
||||
Et un message "Lien copié (nécessite connexion pour ouvrir)" s'affiche
|
||||
90
docs/domains/_shared/features/profil/badge-verifie.feature
Normal file
90
docs/domains/_shared/features/profil/badge-verifie.feature
Normal file
@@ -0,0 +1,90 @@
|
||||
# language: fr
|
||||
|
||||
@api @profile @verification @mvp
|
||||
Fonctionnalité: Badge compte vérifié pour créateurs authentiques
|
||||
|
||||
En tant que créateur officiel
|
||||
Je veux obtenir un badge vérifié
|
||||
Afin de prouver mon authenticité et gagner la confiance
|
||||
|
||||
Scénario: Demande de vérification par un créateur
|
||||
Étant donné un créateur "MuseeDuLouvre" avec 1000+ abonnés
|
||||
Quand il demande la vérification via "Paramètres > Demander la vérification"
|
||||
Alors un formulaire de demande s'affiche:
|
||||
| Champ requis | Exemple |
|
||||
| Nom officiel | Musée du Louvre |
|
||||
| Type d'organisation | Institution culturelle |
|
||||
| Document officiel | KBIS / Statuts |
|
||||
| Preuve d'identité | Carte d'identité |
|
||||
| Site web officiel | louvre.fr |
|
||||
| Compte social officiel | @MuseeLouvre (Twitter) |
|
||||
Et la demande est soumise pour review
|
||||
Et un événement "VERIFICATION_REQUEST_SUBMITTED" est enregistré
|
||||
|
||||
Scénario: Vérification par l'équipe RoadWave
|
||||
Étant donné une demande de vérification reçue
|
||||
Quand un modérateur examine le dossier
|
||||
Alors il vérifie:
|
||||
| Critère | Validation |
|
||||
| Documents officiels | Authentiques |
|
||||
| Correspondance identité | Confirmée |
|
||||
| Site web officiel | Vérifié (DNS) |
|
||||
| Réseaux sociaux | Cross-vérifiés |
|
||||
| Activité sur RoadWave | Régulière (3+ mois) |
|
||||
Et prend une décision dans les 7 jours
|
||||
Et un événement "VERIFICATION_REVIEWED" est enregistré
|
||||
|
||||
Scénario: Attribution du badge vérifié
|
||||
Étant donné une demande acceptée
|
||||
Quand le badge est attribué
|
||||
Alors un badge bleu "✓ Vérifié" s'affiche:
|
||||
| Emplacement | Affichage |
|
||||
| À côté du nom de profil | ✓ Musée du Louvre |
|
||||
| Dans les résultats | Badge visible |
|
||||
| Dans les commentaires | Badge visible |
|
||||
Et une notification est envoyée: "Félicitations ! Votre compte est maintenant vérifié"
|
||||
Et un événement "VERIFICATION_BADGE_GRANTED" est enregistré
|
||||
|
||||
Scénario: Avantages du compte vérifié
|
||||
Étant donné un créateur vérifié
|
||||
Alors il bénéficie de:
|
||||
| Avantage | Détail |
|
||||
| Badge bleu visible | Crédibilité accrue |
|
||||
| Priorité dans les recherches | Meilleur ranking SEO |
|
||||
| Statistiques avancées | Analytics détaillées |
|
||||
| Support prioritaire | Réponse < 24h |
|
||||
| Contenu mis en avant | Page "Créateurs vérifiés" |
|
||||
Et un événement "VERIFIED_BENEFITS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Révocation du badge pour violation
|
||||
Étant donné un créateur vérifié "InstitutionX"
|
||||
Quand il viole les CGU (contenu inapproprié)
|
||||
Alors le badge est révoqué immédiatement
|
||||
Et un email explique la raison
|
||||
Et il peut faire appel de la décision
|
||||
Et un événement "VERIFICATION_BADGE_REVOKED" est enregistré
|
||||
|
||||
Scénario: Renouvellement annuel de la vérification
|
||||
Étant donné un créateur vérifié depuis 12 mois
|
||||
Quand l'anniversaire de la vérification arrive
|
||||
Alors une review automatique est lancée
|
||||
Et des documents à jour peuvent être demandés
|
||||
Et le badge reste actif pendant la review
|
||||
Et un événement "VERIFICATION_RENEWAL_STARTED" est enregistré
|
||||
|
||||
Scénario: Badge spécial pour partenaires officiels
|
||||
Étant donné un partenaire stratégique (Offices du Tourisme, Musées nationaux)
|
||||
Alors un badge or "✓ Partenaire Officiel" est attribué
|
||||
Et des privilèges supplémentaires sont accordés
|
||||
Et un événement "OFFICIAL_PARTNER_BADGE_GRANTED" est enregistré
|
||||
|
||||
Scénario: Statistiques des comptes vérifiés
|
||||
Étant donné que 150 comptes sont vérifiés
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Comptes vérifiés | 150 |
|
||||
| % de la base créateurs | 1.5% |
|
||||
| Demandes en attente | 45 |
|
||||
| Taux d'acceptation | 65% |
|
||||
| Temps moyen de vérification | 5 jours |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
293
docs/domains/_shared/features/profil/profil-createur.feature
Normal file
293
docs/domains/_shared/features/profil/profil-createur.feature
Normal file
@@ -0,0 +1,293 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Profil créateur
|
||||
En tant qu'utilisateur de RoadWave
|
||||
Je veux consulter les profils des créateurs
|
||||
Afin de découvrir leur contenu et décider de m'abonner
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
|
||||
# 15.2.1 - Structure de la page profil
|
||||
|
||||
Scénario: URL du profil créateur
|
||||
Étant donné un créateur avec le pseudo "paris_stories"
|
||||
Quand l'utilisateur accède au profil
|
||||
Alors l'URL est "https://roadwave.fr/@paris_stories"
|
||||
|
||||
Scénario: Informations principales du profil
|
||||
Étant donné un créateur "@paris_stories" avec les informations suivantes:
|
||||
| champ | valeur |
|
||||
| photo | avatar_120x120.jpg |
|
||||
| pseudo | paris_stories |
|
||||
| badge_vérifié | true |
|
||||
| bio | Histoires et anecdotes de Paris |
|
||||
| abonnés | 1200 |
|
||||
| contenus | 42 |
|
||||
| durée_totale | 18h |
|
||||
| écoutes_totales | 54000 |
|
||||
Quand le profil est affiché
|
||||
Alors les éléments suivants sont visibles:
|
||||
| élément | valeur affichée |
|
||||
| Photo profil | 120×120 px |
|
||||
| @pseudo | @paris_stories |
|
||||
| Badge vérifié | ✓ |
|
||||
| Bio | Histoires et... |
|
||||
| Nombre abonnés | 1.2K abonnés |
|
||||
| Nombre contenus | 42 contenus |
|
||||
| Durée totale | 18h de contenu créé |
|
||||
| Écoutes totales | 54K écoutes totales |
|
||||
|
||||
Plan du Scénario: Arrondi des statistiques publiques
|
||||
Étant donné un créateur avec <valeur_exacte> <métrique>
|
||||
Quand le profil est affiché
|
||||
Alors la valeur affichée est "<valeur_affichée>"
|
||||
|
||||
Exemples:
|
||||
| métrique | valeur_exacte | valeur_affichée |
|
||||
| abonnés | 342 | 342 |
|
||||
| abonnés | 1200 | 1.2K |
|
||||
| abonnés | 54000 | 54K |
|
||||
| abonnés | 1200000 | 1.2M |
|
||||
| écoutes | 842 | 842 |
|
||||
| écoutes | 5400 | 5.4K |
|
||||
| écoutes | 142000 | 142K |
|
||||
| écoutes | 2100000 | 2.1M |
|
||||
| durée (heures) | 18 | 18h |
|
||||
| durée (heures) | 142 | 142h |
|
||||
|
||||
Scénario: Bio avec markdown basique
|
||||
Étant donné un créateur avec la bio suivante en markdown:
|
||||
"""
|
||||
**Histoires de Paris** - Découvrez la capitale
|
||||
*Nouveau contenu chaque semaine*
|
||||
https://paris-stories.fr
|
||||
"""
|
||||
Quand le profil est affiché
|
||||
Alors le texte en gras "Histoires de Paris" est formaté
|
||||
Et le texte en italique "Nouveau contenu chaque semaine" est formaté
|
||||
Et le lien "https://paris-stories.fr" est cliquable
|
||||
|
||||
Scénario: Limitation de la bio à 300 caractères
|
||||
Étant donné un créateur qui entre une bio de 350 caractères
|
||||
Quand la bio est sauvegardée
|
||||
Alors seuls les 300 premiers caractères sont conservés
|
||||
Et un message "Maximum 300 caractères" s'affiche
|
||||
|
||||
Scénario: Boutons d'action principaux
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Quand la page est chargée
|
||||
Alors les boutons suivants sont visibles:
|
||||
| bouton | action |
|
||||
| S'abonner | Abonnement au créateur |
|
||||
| Partager profil | Menu de partage |
|
||||
| ••• | Menu contextuel |
|
||||
|
||||
Scénario: Menu contextuel du profil [•••]
|
||||
Étant donné que l'utilisateur clique sur le bouton [•••]
|
||||
Quand le menu s'ouvre
|
||||
Alors les options suivantes sont disponibles:
|
||||
| option | description |
|
||||
| Partager profil | Partager le lien du profil |
|
||||
| Signaler profil | Signaler spam ou usurpation d'identité |
|
||||
| Bloquer créateur | Masquer tous les contenus du créateur |
|
||||
|
||||
Scénario: Liste des contenus du créateur
|
||||
Étant donné un créateur avec 3 contenus publiés
|
||||
Quand le profil est affiché
|
||||
Alors chaque contenu affiche:
|
||||
| élément | exemple |
|
||||
| Cover image | Image 16:9 |
|
||||
| Titre | Balade à Paris |
|
||||
| Durée et écoutes | 12 min · 🎧 2.3K |
|
||||
| Localisation | 📍 Paris |
|
||||
| Bouton lecture | ▶️ |
|
||||
|
||||
Plan du Scénario: Options de tri des contenus
|
||||
Étant donné un créateur avec 10 contenus publiés
|
||||
Quand l'utilisateur sélectionne le tri "<option_tri>"
|
||||
Alors les contenus sont triés par <critère>
|
||||
|
||||
Exemples:
|
||||
| option_tri | critère |
|
||||
| Plus récents | Date publication DESC (défaut) |
|
||||
| Plus populaires | Écoutes × facteur temporel (90 jours) |
|
||||
| Plus anciens | Date publication ASC |
|
||||
|
||||
Scénario: Filtrage des contenus par tag
|
||||
Étant donné un créateur avec des contenus taggés "Voyage", "Histoire", "Gastronomie"
|
||||
Quand l'utilisateur filtre par tags "Voyage, Histoire"
|
||||
Alors seuls les contenus avec ces tags sont affichés
|
||||
Et le nombre de résultats est indiqué "12 contenus"
|
||||
|
||||
Scénario: Recherche locale dans le profil
|
||||
Étant donné que l'utilisateur consulte le profil de "@paris_stories"
|
||||
Et que le créateur a publié 50 contenus
|
||||
Quand l'utilisateur entre "Montmartre" dans la barre de recherche
|
||||
Alors la recherche s'effectue sur les titres et descriptions
|
||||
Et seuls les contenus correspondants sont affichés
|
||||
Et le placeholder indique "Rechercher dans les contenus de @paris_stories"
|
||||
|
||||
Scénario: Chargement paginé des contenus
|
||||
Étant donné un créateur avec 100 contenus publiés
|
||||
Quand le profil est affiché
|
||||
Alors 20 contenus sont chargés initialement
|
||||
Et un bouton "Charger plus" est visible en bas de page
|
||||
Quand l'utilisateur clique sur "Charger plus"
|
||||
Alors 20 contenus supplémentaires sont chargés
|
||||
|
||||
# 15.2.2 - Statistiques publiques
|
||||
|
||||
Scénario: Informations publiques visibles par tous
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Alors les informations suivantes sont publiques:
|
||||
| information | visible |
|
||||
| Photo et pseudo | ✅ |
|
||||
| Badge vérifié | ✅ |
|
||||
| Bio | ✅ |
|
||||
| Nombre abonnés | ✅ |
|
||||
| Nombre contenus | ✅ |
|
||||
| Durée totale créée | ✅ |
|
||||
| Écoutes totales | ✅ |
|
||||
|
||||
Scénario: Informations privées non visibles
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Alors les informations suivantes sont privées:
|
||||
| information | visible |
|
||||
| Liste des abonnés | ❌ |
|
||||
| Revenus | ❌ |
|
||||
| Localisation précise | ❌ |
|
||||
| Email | ❌ |
|
||||
|
||||
Scénario: Dashboard créateur avec métriques privées
|
||||
Étant donné que le créateur "@paris_stories" consulte son propre dashboard
|
||||
Quand la page statistiques est affichée
|
||||
Alors les métriques suivantes sont accessibles:
|
||||
| métrique | type |
|
||||
| Taux complétion moyen | 78% |
|
||||
| Évolution abonnés | Graphique |
|
||||
| Écoutes par contenu | Tableau |
|
||||
| Revenus | Dashboard |
|
||||
| Taux conversion Premium | Pourcentage |
|
||||
| Démographie (âge/zone) | Agrégée |
|
||||
|
||||
Scénario: Graphique d'évolution des abonnés
|
||||
Étant donné que le créateur consulte son dashboard
|
||||
Quand il sélectionne la période "30 jours"
|
||||
Alors un graphique d'évolution des abonnés est affiché
|
||||
Et les périodes disponibles sont:
|
||||
| période |
|
||||
| 30j |
|
||||
| 90j |
|
||||
| 1 an |
|
||||
|
||||
Scénario: Tableau détaillé des écoutes par contenu
|
||||
Étant donné un créateur avec 10 contenus publiés
|
||||
Quand il consulte le tableau des performances
|
||||
Alors chaque contenu affiche:
|
||||
| métrique | exemple |
|
||||
| Titre | Balade |
|
||||
| Écoutes totales | 2300 |
|
||||
| Écoutes complètes >80% | 1840 |
|
||||
| Taux complétion | 80% |
|
||||
| Likes | 420 |
|
||||
| Partages | 56 |
|
||||
|
||||
# 15.2.3 - Badge vérifié
|
||||
|
||||
Scénario: Affichage du badge vérifié
|
||||
Étant donné un créateur vérifié "@paris_stories"
|
||||
Quand son profil est affiché
|
||||
Alors le badge bleu "✓" est accolé au pseudo
|
||||
Et un tooltip "Compte vérifié" s'affiche au survol
|
||||
|
||||
Scénario: Badge vérifié visible partout
|
||||
Étant donné un créateur vérifié "@paris_stories"
|
||||
Alors le badge "✓" est affiché dans:
|
||||
| emplacement |
|
||||
| Page profil |
|
||||
| Player en lecture |
|
||||
| Résultats de recherche|
|
||||
| Notifications |
|
||||
|
||||
Plan du Scénario: Attribution automatique du badge selon critères
|
||||
Étant donné un créateur avec <critère>
|
||||
Quand les conditions sont validées
|
||||
Alors le badge vérifié est attribué <automatique>
|
||||
|
||||
Exemples:
|
||||
| critère | automatique |
|
||||
| KYC Mangopay validé | Oui |
|
||||
| ≥10K abonnés + compte >6 mois | Oui |
|
||||
| Célébrité / Média officiel | Manuel |
|
||||
|
||||
Scénario: Attribution automatique via KYC
|
||||
Étant donné un créateur qui complète son KYC Mangopay
|
||||
Quand les documents sont validés
|
||||
Alors le badge vérifié est attribué automatiquement
|
||||
Et une notification "Votre compte est maintenant vérifié ✓" est envoyée
|
||||
|
||||
Scénario: Attribution automatique à 10K abonnés
|
||||
Étant donné un créateur avec 9999 abonnés et un compte de 7 mois
|
||||
Quand il atteint 10000 abonnés
|
||||
Alors le badge vérifié est attribué automatiquement
|
||||
Et une notification de félicitations est envoyée
|
||||
|
||||
Scénario: Demande manuelle de vérification (célébrité)
|
||||
Étant donné un créateur reconnu publiquement
|
||||
Quand il soumet le formulaire de demande de vérification
|
||||
Alors une requête est créée pour l'équipe RoadWave
|
||||
Et l'équipe vérifie l'identité sous 48-72h
|
||||
Et le badge est attribué si validation réussie
|
||||
|
||||
Scénario: Retrait du badge en cas de suspension
|
||||
Étant donné un créateur vérifié avec le badge "✓"
|
||||
Quand sa monétisation est suspendue
|
||||
Alors le badge vérifié est retiré temporairement
|
||||
Et le badge est restauré après levée de la suspension
|
||||
|
||||
Scénario: Retrait définitif du badge pour strikes multiples
|
||||
Étant donné un créateur vérifié avec 3 strikes actifs
|
||||
Quand un 4ème strike est appliqué (ban)
|
||||
Alors le badge vérifié est retiré définitivement
|
||||
Et le compte est banni
|
||||
|
||||
Scénario: Retrait du badge pour usurpation d'identité
|
||||
Étant donné un créateur vérifié qui usurpe l'identité d'une célébrité
|
||||
Quand la fraude est détectée
|
||||
Alors le badge est retiré immédiatement
|
||||
Et le compte est banni
|
||||
Et une enquête est ouverte
|
||||
|
||||
# Cas d'erreur et limites
|
||||
|
||||
Scénario: Profil créateur supprimé
|
||||
Étant donné qu'un utilisateur tente d'accéder à "@deleted_user"
|
||||
Quand la page est chargée
|
||||
Alors un message "Ce profil n'existe pas ou a été supprimé" s'affiche
|
||||
|
||||
Scénario: Blocage d'un créateur
|
||||
Étant donné que l'utilisateur bloque le créateur "@spam_account"
|
||||
Quand l'utilisateur consulte son flux de recommandations
|
||||
Alors aucun contenu de "@spam_account" n'est affiché
|
||||
Et le créateur n'apparaît plus dans les recherches
|
||||
|
||||
Scénario: Déblocage d'un créateur
|
||||
Étant donné que l'utilisateur a bloqué "@paris_stories"
|
||||
Quand il accède à ses paramètres "Comptes bloqués"
|
||||
Et qu'il débloque "@paris_stories"
|
||||
Alors les contenus du créateur réapparaissent dans les recommandations
|
||||
|
||||
Scénario: Signalement d'un profil pour spam
|
||||
Étant donné que l'utilisateur signale le profil "@spam_account"
|
||||
Quand il sélectionne la raison "Spam"
|
||||
Alors le signalement est envoyé à la modération
|
||||
Et un message de confirmation s'affiche
|
||||
Et le profil reste visible jusqu'à décision de modération
|
||||
|
||||
Scénario: Signalement pour usurpation d'identité
|
||||
Étant donné que l'utilisateur signale le profil "@fake_celebrity"
|
||||
Quand il sélectionne "Usurpation d'identité"
|
||||
Et qu'il fournit une preuve
|
||||
Alors le signalement est priorisé (priorité HAUTE)
|
||||
Et l'équipe modération traite sous 24h
|
||||
@@ -0,0 +1,70 @@
|
||||
# language: fr
|
||||
|
||||
@ui @profile @privacy @mvp
|
||||
Fonctionnalité: Statistiques arrondies pour protection de la vie privée
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes statistiques publiques soient arrondies
|
||||
Afin de protéger ma vie privée et éviter le tracking précis
|
||||
|
||||
Scénario: Arrondi du nombre d'écoutes publiques
|
||||
Étant donné un créateur avec 1,234 écoutes exactes
|
||||
Quand son profil public est affiché
|
||||
Alors le nombre affiché est: "1.2k écoutes"
|
||||
Et non pas "1,234"
|
||||
Et un événement "STATS_ROUNDED_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Règles d'arrondi selon les volumes
|
||||
Étant donné différents volumes d'écoutes
|
||||
Alors l'arrondi appliqué est:
|
||||
| Écoutes exactes | Affiché publiquement |
|
||||
| 42 | 40 |
|
||||
| 157 | 150+ |
|
||||
| 1,234 | 1.2k |
|
||||
| 15,678 | 15k |
|
||||
| 123,456 | 120k |
|
||||
| 1,234,567 | 1.2M |
|
||||
Et un événement "ROUNDING_RULES_APPLIED" est enregistré
|
||||
|
||||
Scénario: Statistiques précises pour le créateur seulement
|
||||
Étant donné un créateur "alice@roadwave.fr"
|
||||
Quand elle consulte son propre dashboard
|
||||
Alors elle voit les chiffres exacts: 1,234
|
||||
Mais les visiteurs externes voient: 1.2k
|
||||
Et un événement "PRECISE_STATS_CREATOR_VIEW" est enregistré
|
||||
|
||||
Scénario: Arrondi des revenus publics
|
||||
Étant donné un créateur avec 1,567€ de revenus
|
||||
Quand ses stats publiques sont affichées
|
||||
Alors le montant est arrondi: "1.5k€"
|
||||
Et les décimales exactes sont masquées
|
||||
Et un événement "REVENUE_ROUNDED_PUBLIC" est enregistré
|
||||
|
||||
Scénario: Arrondi du nombre d'abonnés
|
||||
Étant donné un créateur avec 8,743 abonnés
|
||||
Alors le profil public affiche: "8.7k abonnés"
|
||||
Et évite le tracking précis de croissance
|
||||
Et un événement "FOLLOWERS_ROUNDED_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Protection contre le scraping de données
|
||||
Étant donné un bot qui scrape les profils
|
||||
Quand il collecte les statistiques arrondies
|
||||
Alors il ne peut pas obtenir de données précises
|
||||
Et le tracking temporel est rendu imprécis
|
||||
Et un événement "SCRAPING_PROTECTION_ACTIVE" est enregistré
|
||||
|
||||
Scénario: Option de désactivation de l'arrondi pour créateurs vérifiés
|
||||
Étant donné un créateur vérifié "MuseeDuLouvre"
|
||||
Quand il active "Afficher statistiques exactes"
|
||||
Alors les chiffres précis sont publics
|
||||
Et cela renforce la transparence
|
||||
Et un événement "PRECISE_STATS_PUBLIC_ENABLED" est enregistré
|
||||
|
||||
Scénario: Métriques d'impact de l'arrondi sur la vie privée
|
||||
Étant donné que 10 000 profils affichent des stats arrondies
|
||||
Alors l'impact est mesuré:
|
||||
| Métrique | Valeur |
|
||||
| Tentatives de tracking bloquées | 1,234 |
|
||||
| Précision moyenne du scraping | -70% |
|
||||
| Satisfaction utilisateurs | 4.5/5 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,176 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @anonymization @gps
|
||||
Fonctionnalité: Anonymisation des données GPS après 24h
|
||||
|
||||
# 13.2 - Geohash après 24h (conformité RGPD + CNIL)
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur avec le GPS activé
|
||||
Et que j'utilise l'application depuis plusieurs jours
|
||||
|
||||
Scénario: Conservation des données GPS précises pendant 24h
|
||||
Étant donné que j'écoute un contenu à la position GPS 48.8566, 2.3522 (Paris, Tour Eiffel)
|
||||
Et qu'il est 10:00 le 2025-01-20
|
||||
Quand l'événement d'écoute est enregistré en base de données
|
||||
Alors les coordonnées précises 48.8566, 2.3522 sont stockées
|
||||
Et le champ `anonymized` est à `false`
|
||||
Et le champ `created_at` contient "2025-01-20 10:00:00"
|
||||
Et ces données précises servent à la recommandation personnalisée
|
||||
|
||||
Scénario: Conversion en geohash après 24h
|
||||
Étant donné que j'ai écouté un contenu le 2025-01-20 à 10:00 à la position 48.8566, 2.3522
|
||||
Quand le job quotidien d'anonymisation s'exécute le 2025-01-21 à 02:00
|
||||
Alors les coordonnées précises sont converties en geohash précision 5
|
||||
Et le geohash correspond à une zone d'environ 5km²
|
||||
Et les coordonnées originales 48.8566, 2.3522 sont supprimées définitivement
|
||||
Et le champ `anonymized` passe à `true`
|
||||
Et il est impossible de retrouver la position précise d'origine
|
||||
|
||||
Scénario: Requête SQL d'anonymisation (PostGIS)
|
||||
Étant donné que le job quotidien d'anonymisation s'exécute
|
||||
Quand la requête SQL suivante est exécutée:
|
||||
"""sql
|
||||
UPDATE location_history
|
||||
SET location = ST_SetSRID(ST_GeomFromGeoHash(ST_GeoHash(location::geography, 5)), 4326)::geography,
|
||||
anonymized = true
|
||||
WHERE created_at < NOW() - INTERVAL '24 hours' AND anonymized = false;
|
||||
"""
|
||||
Alors toutes les positions vieilles de plus de 24h sont anonymisées
|
||||
Et le processus est automatique et irréversible
|
||||
Et les données sont conformes RGPD
|
||||
|
||||
Scénario: Précision du geohash niveau 5
|
||||
Étant donné qu'une position GPS est convertie en geohash précision 5
|
||||
Quand on analyse la zone couverte
|
||||
Alors la zone fait environ 5km² (4.9km × 4.9km)
|
||||
Et cette précision est suffisante pour des analytics agrégées
|
||||
Et cette précision ne permet pas d'identifier un individu (conformité CNIL)
|
||||
|
||||
Scénario: Exemple de conversion Paris
|
||||
Étant donné que ma position précise est 48.8566, 2.3522 (Tour Eiffel)
|
||||
Quand la conversion en geohash précision 5 est appliquée
|
||||
Alors le geohash généré est "u09wh"
|
||||
Et ce geohash couvre une zone de ~5km² autour de la Tour Eiffel
|
||||
Et toutes les positions dans cette zone partagent le même geohash
|
||||
Et il est impossible de distinguer deux utilisateurs dans cette zone
|
||||
|
||||
# Exception: Historique personnel
|
||||
|
||||
Scénario: Conservation de l'historique personnel utilisateur
|
||||
Étant donné que j'ai écouté des contenus aux positions suivantes:
|
||||
| date | heure | latitude | longitude | lieu |
|
||||
| 2025-01-15 | 08:30 | 48.8566 | 2.3522 | Paris |
|
||||
| 2025-01-16 | 14:00 | 43.6047 | 1.4442 | Toulouse |
|
||||
| 2025-01-17 | 19:00 | 45.7640 | 4.8357 | Lyon |
|
||||
Quand j'ouvre mon historique personnel dans "Profil > Mes trajets"
|
||||
Alors je vois mes trajets avec les positions précises intégrales
|
||||
Et ces données ne sont pas anonymisées tant que mon compte est actif
|
||||
Et seul moi peut accéder à ces données
|
||||
Et elles ne sont pas utilisées pour des analytics globales
|
||||
|
||||
Scénario: Anonymisation pour analytics globales uniquement
|
||||
Étant donné que RoadWave génère des analytics agrégées
|
||||
Quand l'équipe analyse les zones géographiques populaires
|
||||
Alors seules les données anonymisées (geohash) sont utilisées
|
||||
Et les positions précises de l'historique personnel ne sont jamais agrégées
|
||||
Et les heatmaps de trafic utilisent uniquement les geohash ~5km²
|
||||
|
||||
# Job quotidien automatique
|
||||
|
||||
Scénario: Planification du job d'anonymisation
|
||||
Étant donné que le système est en production
|
||||
Quand on consulte les jobs planifiés (cron)
|
||||
Alors un job "anonymize_gps_data" est configuré
|
||||
Et le job s'exécute tous les jours à 02:00 (heure creuse)
|
||||
Et le job traite toutes les positions vieilles de plus de 24h
|
||||
Et un log est généré pour traçabilité
|
||||
|
||||
Scénario: Exécution du job avec métriques
|
||||
Étant donné que le job d'anonymisation s'exécute le 2025-01-21 à 02:00
|
||||
Quand le job se termine
|
||||
Alors un rapport est généré avec:
|
||||
| métrique | valeur |
|
||||
| Nombre de positions traitées | 15420 |
|
||||
| Nombre de positions anonymisées| 15420 |
|
||||
| Durée d'exécution | 3.5s |
|
||||
| Erreurs | 0 |
|
||||
Et le rapport est loggé dans Sentry/Grafana
|
||||
Et une alerte est envoyée si le job échoue
|
||||
|
||||
Scénario: Performances du job d'anonymisation
|
||||
Étant donné que 100 000 positions doivent être anonymisées
|
||||
Quand le job s'exécute
|
||||
Alors le traitement se fait en moins de 30 secondes
|
||||
Et la requête PostGIS est optimisée avec index
|
||||
Et aucun impact sur les performances de l'application en production
|
||||
|
||||
# Vraie anonymisation RGPD
|
||||
|
||||
Scénario: Impossibilité de réidentification
|
||||
Étant donné qu'une position a été anonymisée en geohash "u09wh"
|
||||
Quand un attaquant tente de retrouver la position précise d'origine
|
||||
Alors il est impossible de déterminer la position exacte
|
||||
Et des milliers de positions précises correspondent au même geohash
|
||||
Et il n'y a aucune traçabilité vers la position originale
|
||||
Et cette anonymisation est irréversible
|
||||
|
||||
Scénario: Conformité CNIL - données véritablement anonymisées
|
||||
Étant donné que les positions sont converties en geohash précision 5
|
||||
Quand un auditeur CNIL vérifie la conformité
|
||||
Alors les données sont considérées comme véritablement anonymisées
|
||||
Et elles ne sont plus considérées comme des données personnelles
|
||||
Et aucun consentement n'est requis pour leur traitement analytique
|
||||
Et elles peuvent être conservées indéfiniment
|
||||
|
||||
# Analytics agrégées autorisées
|
||||
|
||||
Scénario: Heatmap de trafic avec données anonymisées
|
||||
Étant donné que RoadWave génère une heatmap des zones populaires
|
||||
Quand on analyse les données utilisées
|
||||
Alors seules les positions anonymisées (geohash) sont agrégées
|
||||
Et la heatmap montre des zones de ~5km²
|
||||
Et aucune position précise n'est révélée
|
||||
Et cette analyse ne nécessite pas de consentement utilisateur (données anonymes)
|
||||
|
||||
Scénario: Statistiques géographiques par département
|
||||
Étant donné que RoadWave analyse l'utilisation par département
|
||||
Quand les statistiques sont générées
|
||||
Alors les données anonymisées sont agrégées par département
|
||||
Et les résultats montrent: "Paris (75): 12 500 écoutes, Lyon (69): 8 300 écoutes"
|
||||
Et aucune donnée personnelle n'est révélée
|
||||
Et les statistiques sont RGPD-compliant
|
||||
|
||||
# PostGIS natif - 0€
|
||||
|
||||
Scénario: Coût de la solution d'anonymisation
|
||||
Étant donné que PostGIS est utilisé pour l'anonymisation GPS
|
||||
Quand on calcule le coût de la solution
|
||||
Alors le coût est de 0€ (PostGIS inclus dans PostgreSQL)
|
||||
Et aucune librairie tierce n'est nécessaire
|
||||
Et la solution est entièrement maîtrisée (self-hosted)
|
||||
|
||||
# Cas limites
|
||||
|
||||
Scénario: Anonymisation respecte les positions en cours de session
|
||||
Étant donné que je suis en train d'écouter du contenu actuellement
|
||||
Et que certaines de mes positions ont plus de 24h
|
||||
Quand le job d'anonymisation s'exécute
|
||||
Alors mes positions de plus de 24h sont anonymisées
|
||||
Mais ma position actuelle (session en cours) reste précise
|
||||
Et la recommandation continue de fonctionner normalement
|
||||
|
||||
Scénario: Suppression de compte et anonymisation GPS
|
||||
Étant donné que je demande la suppression de mon compte
|
||||
Quand le compte est supprimé (après grace period de 30j)
|
||||
Alors toutes mes positions GPS (précises et anonymisées) sont supprimées
|
||||
Et mon historique personnel de trajets est supprimé
|
||||
Et aucune donnée GPS ne subsiste, même anonymisée
|
||||
|
||||
Scénario: Export de données avant anonymisation
|
||||
Étant donné que je demande un export de mes données
|
||||
Et que certaines de mes positions ont été anonymisées
|
||||
Quand l'export est généré
|
||||
Alors les positions précises de mon historique personnel sont incluses
|
||||
Mais les positions déjà anonymisées (>24h, analytics) apparaissent en geohash
|
||||
Et l'export précise quelles données ont été anonymisées et pourquoi
|
||||
@@ -0,0 +1,256 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @administrative @dpo
|
||||
Fonctionnalité: Conformité administrative RGPD (Registre, Breach, DPO)
|
||||
|
||||
# 13.8 - Registre des traitements (Article 30 RGPD)
|
||||
|
||||
@registre-traitements
|
||||
Scénario: Registre des traitements en Markdown versionné Git
|
||||
Étant donné que RoadWave doit tenir un registre des traitements
|
||||
Quand on consulte la documentation
|
||||
Alors un fichier `docs/rgpd/registre-traitements.md` existe
|
||||
Et le fichier est versionné dans Git
|
||||
Et l'historique des modifications est traçable via Git
|
||||
Et chaque traitement est documenté dans une section dédiée
|
||||
|
||||
Scénario: Contenu obligatoire pour chaque traitement
|
||||
Étant donné que le registre des traitements contient le traitement "Géolocalisation utilisateurs"
|
||||
Quand on lit la section correspondante
|
||||
Alors les informations suivantes sont présentes:
|
||||
| information obligatoire | exemple |
|
||||
| Nom du traitement | Géolocalisation utilisateurs |
|
||||
| Finalité | Recommandation de contenu géolocalisé |
|
||||
| Catégories de données | Coordonnées GPS, historique de position |
|
||||
| Base légale | Consentement (Article 6.1.a RGPD) |
|
||||
| Durée de conservation | 24h (précis), puis geohash anonymisé |
|
||||
| Destinataires | Aucun tiers |
|
||||
| Transferts hors UE | Aucun |
|
||||
| Mesures de sécurité | TLS 1.3, anonymisation après 24h |
|
||||
|
||||
Plan du Scénario: Traitements documentés dans le registre
|
||||
Étant donné que le registre des traitements est complet
|
||||
Quand on liste tous les traitements
|
||||
Alors le traitement "<traitement>" est documenté avec la base légale "<base_legale>"
|
||||
|
||||
Exemples:
|
||||
| traitement | base_legale |
|
||||
| Géolocalisation utilisateurs | Consentement |
|
||||
| Historique d'écoute | Intérêt légitime |
|
||||
| Création de contenu | Exécution du contrat |
|
||||
| Analytics (Matomo) | Consentement |
|
||||
| Paiements (Mangopay) | Exécution du contrat |
|
||||
| Modération contenus | Intérêt légitime |
|
||||
| Notifications push | Consentement |
|
||||
|
||||
Scénario: Review trimestrielle du registre
|
||||
Étant donné que le registre des traitements existe
|
||||
Quand on consulte l'historique Git
|
||||
Alors une mise à jour est effectuée au moins tous les 3 mois
|
||||
Et chaque mise à jour a un commit avec message explicite
|
||||
Et un tag Git marque chaque review trimestrielle
|
||||
Et les modifications sont traçables (auteur, date, changements)
|
||||
|
||||
Scénario: Mise à jour immédiate si nouveau traitement
|
||||
Étant donné qu'une nouvelle fonctionnalité nécessite un traitement de données
|
||||
Quand la fonctionnalité est développée
|
||||
Alors le registre est mis à jour AVANT le déploiement en production
|
||||
Et le nouveau traitement est documenté complètement
|
||||
Et un commit Git enregistre l'ajout
|
||||
Et le DPO valide la conformité RGPD du nouveau traitement
|
||||
|
||||
Scénario: Migration future vers interface admin PostgreSQL
|
||||
Étant donné que RoadWave dépasse 100 000 utilisateurs
|
||||
Quand la complexité du registre augmente
|
||||
Alors une interface admin PostgreSQL est développée
|
||||
Et le registre Markdown est migré vers la base de données
|
||||
Et l'historique Git est conservé pour audit
|
||||
Et l'interface permet une gestion plus efficace des traitements
|
||||
|
||||
# 13.9 - Notification violations de données (Breach)
|
||||
|
||||
@breach-notification
|
||||
Scénario: Détection automatique d'événements critiques
|
||||
Étant donné que le système de monitoring est actif
|
||||
Quand un événement critique se produit
|
||||
Alors une alerte est envoyée selon le type d'événement:
|
||||
| événement | outil | alerte |
|
||||
| Erreur backend critique | Sentry | Discord/Slack immédiat |
|
||||
| Pic requêtes anormal | Grafana | Email équipe |
|
||||
| Accès non autorisé DB | PostgreSQL logs | SMS fondateur |
|
||||
| Authentification suspecte | Zitadel alerts | Email équipe |
|
||||
Et les alertes permettent une réaction rapide
|
||||
|
||||
Scénario: Runbook de procédure breach disponible
|
||||
Étant donné qu'une violation de données potentielle est détectée
|
||||
Quand l'équipe consulte la documentation
|
||||
Alors un runbook `docs/rgpd/procedure-breach.md` existe
|
||||
Et le runbook contient une checklist 72h CNIL
|
||||
Et chaque étape est clairement documentée
|
||||
Et les contacts d'urgence sont listés
|
||||
|
||||
Scénario: Checklist 72h en cas de breach
|
||||
Étant donné qu'une violation de données est confirmée
|
||||
Quand l'équipe suit la procédure breach
|
||||
Alors les étapes suivantes sont exécutées dans les délais:
|
||||
| délai | étape |
|
||||
| H+0 | Détection et confinement immédiat |
|
||||
| H+24 | Évaluation gravité (données concernées, users impactés) |
|
||||
| H+48 | Notification CNIL si risque pour utilisateurs |
|
||||
| H+72 | Notification utilisateurs si risque élevé |
|
||||
Et chaque étape est documentée pour audit
|
||||
|
||||
Scénario: Évaluation de la gravité du breach
|
||||
Étant donné qu'une violation de données est détectée
|
||||
Quand l'équipe évalue la gravité
|
||||
Alors les critères suivants sont analysés:
|
||||
| critère | exemple |
|
||||
| Type de données concernées | Emails, mots de passe, GPS, etc. |
|
||||
| Nombre d'utilisateurs impactés | 10, 100, 10000, etc. |
|
||||
| Mesures de sécurité existantes | Chiffrement, hachage, anonymisation |
|
||||
| Risque pour les droits et libertés | Faible, modéré, élevé |
|
||||
Et si le risque est élevé, la CNIL est notifiée sous 72h
|
||||
|
||||
Scénario: Notification CNIL dans les 72h
|
||||
Étant donné qu'un breach avec risque élevé est confirmé à 10:00 le 2025-01-20
|
||||
Quand la gravité est évaluée comme nécessitant une notification
|
||||
Alors la CNIL est notifiée avant 10:00 le 2025-01-23 (72h)
|
||||
Et la notification contient:
|
||||
| information |
|
||||
| Nature de la violation |
|
||||
| Données concernées |
|
||||
| Nombre d'utilisateurs impactés |
|
||||
| Conséquences probables |
|
||||
| Mesures prises |
|
||||
| Mesures de remédiation |
|
||||
Et un email pré-rédigé (template) est utilisé pour gagner du temps
|
||||
|
||||
Scénario: Notification des utilisateurs si risque élevé
|
||||
Étant donné qu'un breach impacte 5000 utilisateurs
|
||||
Et que le risque est élevé (mots de passe non chiffrés exposés)
|
||||
Quand la CNIL est notifiée
|
||||
Alors les utilisateurs impactés sont notifiés dans les 72h
|
||||
Et l'email contient:
|
||||
"""
|
||||
Objet: Alerte sécurité - Action requise
|
||||
|
||||
Bonjour,
|
||||
|
||||
Nous vous informons qu'une violation de données a affecté votre compte RoadWave.
|
||||
|
||||
Données concernées: [liste]
|
||||
Date de l'incident: [date]
|
||||
Actions prises: [mesures]
|
||||
|
||||
Action requise: Changez immédiatement votre mot de passe.
|
||||
|
||||
Pour toute question: security@roadwave.fr
|
||||
"""
|
||||
Et un lien de réinitialisation de mot de passe est inclus
|
||||
|
||||
Scénario: Aucune notification si risque faible
|
||||
Étant donné qu'un breach mineur est détecté (logs techniques exposés, aucune donnée personnelle)
|
||||
Quand l'équipe évalue la gravité
|
||||
Alors le risque est jugé faible
|
||||
Et aucune notification CNIL n'est requise (Article 33.1 RGPD)
|
||||
Et aucune notification utilisateur n'est envoyée
|
||||
Et un log interne est créé pour traçabilité
|
||||
|
||||
Scénario: Monitoring proactif pour éviter découverte tardive
|
||||
Étant donné que Sentry et Grafana sont configurés
|
||||
Quand un comportement anormal est détecté
|
||||
Alors une alerte est envoyée en temps réel
|
||||
Et l'équipe peut réagir avant qu'un breach majeur ne se produise
|
||||
Et les logs sont analysés quotidiennement pour détecter des anomalies
|
||||
Et cette approche proactive limite les risques de découverte tardive
|
||||
|
||||
# 13.10 - DPO (Délégué à la Protection des Données)
|
||||
|
||||
@dpo
|
||||
Scénario: Fondateur = DPO temporaire (MVP)
|
||||
Étant donné que RoadWave est en phase MVP
|
||||
Et que l'entreprise a moins de 250 employés
|
||||
Quand on vérifie l'obligation légale d'avoir un DPO
|
||||
Alors le DPO n'est pas obligatoire selon le RGPD Article 37
|
||||
Et le fondateur assume temporairement le rôle de DPO
|
||||
Et le fondateur suit la formation CNIL gratuite (4h)
|
||||
|
||||
Scénario: Formation CNIL du DPO temporaire
|
||||
Étant donné que le fondateur est DPO temporaire
|
||||
Quand on vérifie sa formation
|
||||
Alors le fondateur a suivi la formation CNIL en ligne (4h)
|
||||
Et le fondateur a obtenu la certification "Atelier RGPD" (gratuit)
|
||||
Et le certificat est conservé pour audit
|
||||
Et la formation couvre:
|
||||
| sujet |
|
||||
| Principes fondamentaux du RGPD |
|
||||
| Droits des personnes |
|
||||
| Sécurité des données |
|
||||
| Violations de données (breach) |
|
||||
| Registre des traitements |
|
||||
|
||||
Scénario: Contact DPO publié et accessible
|
||||
Étant donné que je consulte les mentions légales de RoadWave
|
||||
Quand je cherche le contact du DPO
|
||||
Alors l'email "dpo@roadwave.fr" est clairement affiché
|
||||
Et cet email est également dans les CGU
|
||||
Et le délai de réponse garanti est de 1 mois maximum (RGPD Article 12.3)
|
||||
Et une adresse postale est également fournie
|
||||
|
||||
Scénario: Demande d'exercice de droits RGPD au DPO
|
||||
Étant donné que je veux exercer mon droit d'accès à mes données
|
||||
Quand j'envoie un email à dpo@roadwave.fr
|
||||
Alors je reçois un accusé de réception dans les 48h
|
||||
Et ma demande est traitée dans un délai maximum de 1 mois
|
||||
Et si le délai dépasse 1 mois, je suis informé de la prolongation (max 2 mois supplémentaires)
|
||||
Et la réponse est complète et conforme au RGPD
|
||||
|
||||
Scénario: Types de demandes gérées par le DPO
|
||||
Étant donné que je contacte le DPO
|
||||
Quand j'envoie une demande
|
||||
Alors le DPO peut traiter les demandes suivantes:
|
||||
| type de demande |
|
||||
| Droit d'accès (Article 15) |
|
||||
| Droit de rectification (Article 16) |
|
||||
| Droit à l'effacement (Article 17) |
|
||||
| Droit à la portabilité (Article 20) |
|
||||
| Droit d'opposition (Article 21) |
|
||||
| Plainte RGPD |
|
||||
| Question sur le traitement des données |
|
||||
Et chaque demande reçoit une réponse personnalisée
|
||||
|
||||
Scénario: Migration vers DPO externe si croissance
|
||||
Étant donné que RoadWave dépasse 100 000 utilisateurs
|
||||
Quand la charge de travail DPO augmente
|
||||
Alors un DPO externe mutualisé est engagé
|
||||
Et le coût est d'environ 200€/mois
|
||||
Et le DPO externe a les certifications CNIL requises
|
||||
Et un contrat de sous-traitance RGPD est signé
|
||||
|
||||
Scénario: Recrutement DPO interne si >10 employés
|
||||
Étant donné que RoadWave a plus de 10 employés
|
||||
Quand l'entreprise se structure
|
||||
Alors un DPO interne peut être recruté
|
||||
Et le DPO interne a une certification CNIL (AFCDP ou équivalent)
|
||||
Et le DPO est indépendant et ne peut être licencié pour ses fonctions
|
||||
Et le DPO a un accès direct à la direction
|
||||
|
||||
# Coût total conformité RGPD
|
||||
|
||||
Scénario: Récapitulatif des coûts RGPD
|
||||
Étant donné que toutes les mesures RGPD sont en place
|
||||
Quand on calcule le coût total mensuel
|
||||
Alors le récapitulatif est le suivant:
|
||||
| mesure | implémentation | coût |
|
||||
| Consentement | Tarteaucitron.js + PostgreSQL | 0€ |
|
||||
| Anonymisation GPS | Geohash PostGIS (24h) | 0€ |
|
||||
| Export données | JSON+HTML+ZIP asynchrone | 0€ |
|
||||
| Suppression compte | Grace period 30j + anonymisation | 0€ |
|
||||
| Mode dégradé | GeoIP IP2Location + GPS optionnel | 0€ |
|
||||
| Conservation | Purge auto 5 ans inactivité | 0€ |
|
||||
| Analytics | Matomo self-hosted | ~5€/mois |
|
||||
| Registre traitements | Markdown Git | 0€ |
|
||||
| Breach detection | Sentry + Grafana + runbook | 0€ (< 5K events) |
|
||||
| DPO | Fondateur formé CNIL | 0€ |
|
||||
Et le coût total est d'environ 5€/mois
|
||||
Et cette conformité est 100% opensource et maîtrisée
|
||||
@@ -0,0 +1,186 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @consent
|
||||
Fonctionnalité: Gestion du consentement RGPD
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un nouvel utilisateur
|
||||
Et que j'accède à l'application pour la première fois
|
||||
|
||||
# 13.1 - Gestion du consentement avec Tarteaucitron.js + PostgreSQL
|
||||
|
||||
Scénario: Affichage du banner de consentement au premier lancement web
|
||||
Étant donné que j'accède à l'application web pour la première fois
|
||||
Quand la page se charge
|
||||
Alors un banner RGPD Tarteaucitron.js s'affiche
|
||||
Et le banner est en français
|
||||
Et le banner propose les options suivantes:
|
||||
| option | description |
|
||||
| Tout accepter | Active tous les consentements |
|
||||
| Tout refuser | Refuse tous les consentements optionnels |
|
||||
| Personnaliser | Ouvre le panneau de personnalisation |
|
||||
Et le banner est customisé aux couleurs de RoadWave
|
||||
|
||||
Scénario: Granularité des consentements
|
||||
Étant donné que le banner RGPD est affiché
|
||||
Quand je clique sur "Personnaliser"
|
||||
Alors je vois les catégories de consentements suivantes:
|
||||
| catégorie | type | requis |
|
||||
| Fonctionnel | Nécessaire | oui |
|
||||
| Analytique | Optionnel | non |
|
||||
| Marketing | Optionnel | non |
|
||||
Et chaque catégorie a une description claire de son usage
|
||||
Et je peux accepter ou refuser chaque catégorie individuellement
|
||||
|
||||
Scénario: Consentement géolocalisation précise - obligatoire
|
||||
Étant donné que je suis sur l'application mobile
|
||||
Et que l'onboarding est terminé
|
||||
Quand l'application a besoin d'accéder à ma position précise
|
||||
Alors un écran de demande de consentement s'affiche
|
||||
Et le message explique clairement l'usage:
|
||||
"""
|
||||
RoadWave utilise votre position GPS pour vous proposer du contenu audio géolocalisé pertinent.
|
||||
Vos données sont anonymisées après 24h.
|
||||
"""
|
||||
Et je peux accepter ou refuser
|
||||
Et si je refuse, l'application bascule en mode dégradé (GeoIP uniquement)
|
||||
|
||||
Scénario: Double consentement GPS - banner app + permission OS
|
||||
Étant donné que je veux activer la géolocalisation précise
|
||||
Quand j'accepte le consentement dans l'application
|
||||
Alors l'application demande également la permission au système d'exploitation
|
||||
Et sur iOS, la popup système s'affiche: "Autoriser RoadWave à accéder à votre position ?"
|
||||
Et sur Android, la popup système s'affiche avec les options "Toujours autoriser / Autoriser seulement pendant l'utilisation / Refuser"
|
||||
Et les deux consentements (app + OS) doivent être acceptés pour activer le GPS précis
|
||||
|
||||
# Historique des consentements - PostgreSQL backend
|
||||
|
||||
Scénario: Enregistrement du consentement en base de données
|
||||
Étant donné que j'ai accepté les consentements suivants:
|
||||
| type | accepté |
|
||||
| Fonctionnel | oui |
|
||||
| Analytique | oui |
|
||||
| Marketing | non |
|
||||
| GPS précis | oui |
|
||||
Quand je valide mes choix
|
||||
Alors un enregistrement est créé dans la table `user_consents`
|
||||
Et l'enregistrement contient les champs suivants:
|
||||
| champ | valeur |
|
||||
| user_id | [mon ID utilisateur] |
|
||||
| consent_type | fonctionnel / analytique / gps |
|
||||
| version | 1 |
|
||||
| accepted | true / false |
|
||||
| timestamp | [date et heure exacte] |
|
||||
Et chaque type de consentement a un enregistrement séparé
|
||||
|
||||
Scénario: Versioning des consentements
|
||||
Étant donné que j'ai accepté le consentement "Analytique" version 1 le 2025-01-01
|
||||
Et que les CGU sont mises à jour le 2025-06-01
|
||||
Quand je me connecte après la mise à jour
|
||||
Alors un nouveau consentement version 2 m'est demandé
|
||||
Et mon ancien consentement version 1 reste dans l'historique
|
||||
Et je dois accepter la nouvelle version pour continuer à utiliser les analytics
|
||||
|
||||
Scénario: Historique complet conservé pour preuve légale
|
||||
Étant donné que j'ai modifié mes consentements plusieurs fois:
|
||||
| date | consent_type | accepted | version |
|
||||
| 2025-01-01 | Analytique | oui | 1 |
|
||||
| 2025-03-15 | Analytique | non | 1 |
|
||||
| 2025-06-01 | Analytique | oui | 2 |
|
||||
Quand un auditeur CNIL consulte mon historique de consentements
|
||||
Alors tous les enregistrements sont conservés
|
||||
Et l'historique prouve que chaque consentement a été donné librement
|
||||
Et les timestamps permettent de prouver la conformité à tout moment
|
||||
|
||||
# Consentements requis vs optionnels
|
||||
|
||||
Scénario: Consentement analytique - optionnel
|
||||
Étant donné que je refuse le consentement "Analytique"
|
||||
Quand j'utilise l'application
|
||||
Alors aucun cookie Matomo `_pk_id` n'est déposé
|
||||
Et aucune donnée d'usage n'est envoyée à Matomo
|
||||
Et l'application fonctionne normalement sans analytics
|
||||
|
||||
Scénario: Consentement notifications push - optionnel
|
||||
Étant donné que je refuse le consentement "Notifications push"
|
||||
Quand un créateur que je suis publie un nouveau contenu
|
||||
Alors je ne reçois pas de notification push
|
||||
Mais je peux voir le nouveau contenu dans l'application
|
||||
Et l'application fonctionne normalement
|
||||
|
||||
Scénario: Consentement GPS précis - requis pour fonctionnalités géo
|
||||
Étant donné que je refuse le consentement "GPS précis"
|
||||
Quand j'utilise l'application
|
||||
Alors je peux accéder aux contenus nationaux
|
||||
Mais les contenus géolocalisés précis (Ancré, Contextuel) ne sont pas disponibles
|
||||
Et les audio-guides nécessitent l'activation du GPS
|
||||
Et un banner permanent me rappelle que l'activation du GPS améliore l'expérience
|
||||
|
||||
# Modification des consentements
|
||||
|
||||
Scénario: Révocation d'un consentement depuis les paramètres
|
||||
Étant donné que j'ai accepté le consentement "Analytique"
|
||||
Et que j'utilise l'application depuis 3 mois
|
||||
Quand j'ouvre "Paramètres > Confidentialité > Gérer mes consentements"
|
||||
Alors je vois la liste de tous mes consentements actuels
|
||||
Et je peux révoquer le consentement "Analytique"
|
||||
Quand je révoque le consentement
|
||||
Alors un nouvel enregistrement est créé avec `accepted = false`
|
||||
Et le cookie Matomo est supprimé immédiatement
|
||||
Et les analytics sont désactivées à partir de ce moment
|
||||
|
||||
Scénario: Acceptation d'un consentement précédemment refusé
|
||||
Étant donné que j'avais refusé le consentement "GPS précis"
|
||||
Quand j'ouvre "Paramètres > Confidentialité > Gérer mes consentements"
|
||||
Et que je clique sur "Activer la géolocalisation précise"
|
||||
Alors un nouvel enregistrement est créé avec `accepted = true`
|
||||
Et la permission OS est demandée si ce n'est pas déjà fait
|
||||
Et l'application bascule en mode géolocalisation précise
|
||||
Et les contenus géolocalisés deviennent disponibles immédiatement
|
||||
|
||||
# Preuves légales pour contrôle CNIL
|
||||
|
||||
Scénario: Export de l'historique des consentements pour audit
|
||||
Étant donné qu'un contrôle CNIL est en cours
|
||||
Quand l'équipe RoadWave exporte l'historique des consentements
|
||||
Alors l'export contient pour chaque utilisateur:
|
||||
| champ | description |
|
||||
| user_id | ID anonymisé |
|
||||
| consent_type | Type de consentement |
|
||||
| version | Version des CGU/consentement |
|
||||
| accepted | Accepté ou refusé |
|
||||
| timestamp | Date et heure exacte |
|
||||
| ip_address | IP (anonymisée) au moment du consentement |
|
||||
| user_agent | Navigateur/app utilisé |
|
||||
Et l'export est au format CSV pour analyse
|
||||
Et les données prouvent la conformité RGPD
|
||||
|
||||
Scénario: Conformité recommandations CNIL
|
||||
Étant donné que le système de consentement est implémenté
|
||||
Quand un auditeur CNIL vérifie la conformité
|
||||
Alors le système respecte les critères suivants:
|
||||
| critère CNIL | respecté |
|
||||
| Consentement libre | oui |
|
||||
| Consentement spécifique (granulaire) | oui |
|
||||
| Consentement éclairé (information claire) | oui |
|
||||
| Consentement univoque (action positive) | oui |
|
||||
| Révocable à tout moment | oui |
|
||||
| Preuve du consentement conservée | oui |
|
||||
|
||||
# Opensource et self-hosted
|
||||
|
||||
Scénario: Tarteaucitron.js self-hosted
|
||||
Étant donné que l'application web utilise Tarteaucitron.js
|
||||
Quand je consulte les sources JavaScript chargées
|
||||
Alors le script Tarteaucitron.js est hébergé sur les serveurs RoadWave
|
||||
Et aucun script tiers (CDN externe) n'est chargé
|
||||
Et le code source de Tarteaucitron.js est vérifiable
|
||||
Et aucune donnée n'est envoyée à un tiers lors de l'affichage du banner
|
||||
|
||||
Scénario: Coût de la solution - 0€
|
||||
Étant donné que Tarteaucitron.js est opensource
|
||||
Et que PostgreSQL est utilisé pour le backend
|
||||
Quand on calcule le coût de la solution de consentement
|
||||
Alors le coût est de 0€
|
||||
Et la solution est entièrement maîtrisée (self-hosted)
|
||||
Et aucune dépendance à un service SaaS tiers
|
||||
@@ -0,0 +1,245 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @data-retention
|
||||
Fonctionnalité: Durée de conservation des données et purge automatique
|
||||
|
||||
# 13.6 - 5 ans inactivité → purge automatique (principe de minimisation RGPD)
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de purge automatique est actif
|
||||
|
||||
# Règles de conservation
|
||||
|
||||
Scénario: Auditeur inactif depuis 5 ans - suppression automatique
|
||||
Étant donné que je suis un auditeur (sans contenu créé)
|
||||
Et que je ne me suis pas connecté depuis le 2020-01-01
|
||||
Et que la date actuelle est 2025-01-02 (>5 ans)
|
||||
Quand le job de purge automatique s'exécute
|
||||
Alors mon compte est automatiquement supprimé
|
||||
Et toutes mes données personnelles sont effacées
|
||||
Et aucune trace ne subsiste dans la base de données
|
||||
|
||||
Scénario: Créateur avec contenus actifs - conservation indéfinie
|
||||
Étant donné que je suis un créateur
|
||||
Et que j'ai créé 10 contenus qui reçoivent encore des écoutes
|
||||
Et que je ne me suis pas connecté depuis 6 ans
|
||||
Quand le job de purge automatique s'exécute
|
||||
Alors mon compte n'est pas supprimé
|
||||
Et mes données personnelles sont conservées tant que mes contenus sont écoutés
|
||||
Et mes contenus continuent d'être diffusés normalement
|
||||
|
||||
Scénario: Créateur inactif sans écoutes - suppression automatique
|
||||
Étant donné que je suis un créateur
|
||||
Et que j'ai créé 5 contenus
|
||||
Et que je ne me suis pas connecté depuis 5 ans (depuis 2020-01-01)
|
||||
Et que mes contenus n'ont reçu aucune écoute depuis 2 ans (depuis 2023-01-01)
|
||||
Et que la date actuelle est 2025-01-02
|
||||
Quand le job de purge automatique s'exécute
|
||||
Alors mon compte est automatiquement supprimé
|
||||
Et mes contenus sont anonymisés (créateur = "Utilisateur supprimé")
|
||||
Et les fichiers audio restent disponibles mais anonymisés
|
||||
|
||||
# Notifications avant suppression
|
||||
|
||||
Scénario: Notifications par email avant purge
|
||||
Étant donné que je suis inactif depuis 4 ans et 9 mois
|
||||
Quand le système détecte que je suis éligible à la purge dans 90 jours
|
||||
Alors je reçois un email avec le sujet "Votre compte RoadWave sera supprimé dans 90 jours"
|
||||
Et l'email contient:
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Votre compte RoadWave n'a pas été utilisé depuis plus de 4 ans.
|
||||
Conformément à notre politique de conservation des données, votre compte sera automatiquement supprimé dans 90 jours si vous ne vous connectez pas.
|
||||
|
||||
Pour conserver votre compte, il suffit de vous connecter avant le [date limite].
|
||||
|
||||
Date de dernière connexion: [date]
|
||||
Date de suppression prévue: [date + 90j]
|
||||
|
||||
Si vous ne souhaitez pas conserver ce compte, aucune action n'est requise.
|
||||
"""
|
||||
Et un lien de connexion est inclus dans l'email
|
||||
|
||||
Scénario: Rappels à 90j, 30j et 7j avant suppression
|
||||
Étant donné que je suis éligible à la purge automatique
|
||||
Quand les délais s'écoulent
|
||||
Alors je reçois les emails suivants:
|
||||
| délai | sujet email |
|
||||
| 90 jours | Votre compte sera supprimé dans 90 jours |
|
||||
| 30 jours | Rappel: Votre compte sera supprimé dans 30 jours |
|
||||
| 7 jours | Dernière alerte: suppression dans 7 jours |
|
||||
Et chaque email contient un lien de connexion pour réactiver le compte
|
||||
Et les notifications push sont également envoyées si activées
|
||||
|
||||
Scénario: Connexion annule la suppression programmée
|
||||
Étant donné que je suis éligible à la purge dans 15 jours
|
||||
Et que j'ai reçu plusieurs emails d'avertissement
|
||||
Quand je me connecte à mon compte
|
||||
Alors la suppression programmée est annulée immédiatement
|
||||
Et le compteur d'inactivité est remis à zéro
|
||||
Et je reçois un email de confirmation: "Votre compte a été réactivé"
|
||||
Et je peux continuer à utiliser l'application normalement
|
||||
|
||||
# Job de purge automatique
|
||||
|
||||
Scénario: Exécution quotidienne du job de purge
|
||||
Étant donné que le système est en production
|
||||
Quand on consulte les jobs planifiés
|
||||
Alors un job "purge_inactive_accounts" est configuré
|
||||
Et le job s'exécute tous les jours à 03:00 (heure creuse)
|
||||
Et le job identifie les comptes éligibles à la purge
|
||||
Et le job traite les suppressions automatiques
|
||||
|
||||
Scénario: Critères d'éligibilité à la purge
|
||||
Étant donné que le job de purge s'exécute
|
||||
Quand le système identifie les comptes éligibles
|
||||
Alors les critères suivants sont appliqués:
|
||||
| type_compte | critères |
|
||||
| Auditeur uniquement | 5 ans sans connexion |
|
||||
| Créateur avec contenus actifs| Jamais (tant qu'écoutes) |
|
||||
| Créateur inactif | 5 ans sans connexion + 2 ans sans écoute |
|
||||
Et seuls les comptes remplissant tous les critères sont supprimés
|
||||
|
||||
Scénario: Métriques du job de purge
|
||||
Étant donné que le job de purge s'exécute le 2025-01-15
|
||||
Quand le job se termine
|
||||
Alors un rapport est généré avec:
|
||||
| métrique | exemple |
|
||||
| Comptes analysés | 150 000 |
|
||||
| Comptes éligibles à la purge | 350 |
|
||||
| Auditeurs supprimés | 300 |
|
||||
| Créateurs inactifs supprimés | 50 |
|
||||
| Créateurs conservés (actifs) | 0 |
|
||||
| Erreurs | 0 |
|
||||
| Durée d'exécution | 45s |
|
||||
Et le rapport est loggé pour audit
|
||||
|
||||
# Contenus conservés après purge
|
||||
|
||||
Scénario: Contenus de comptes purgés conservés anonymement
|
||||
Étant donné que mon compte créateur est purgé automatiquement
|
||||
Quand la suppression est effective
|
||||
Alors mes contenus créés sont conservés indéfiniment
|
||||
Et les contenus sont anonymisés (créateur = "Utilisateur supprimé")
|
||||
Et les fichiers audio restent sur le CDN
|
||||
Et les statistiques d'écoute sont préservées
|
||||
Et les utilisateurs peuvent toujours écouter mes contenus
|
||||
|
||||
# Exception: créateurs avec écoutes régulières
|
||||
|
||||
Scénario: Créateur inactif mais contenus populaires - pas de purge
|
||||
Étant donné que je suis un créateur inactif depuis 6 ans
|
||||
Mais que mes contenus reçoivent 500+ écoutes par mois
|
||||
Quand le job de purge s'exécute
|
||||
Alors mon compte n'est pas supprimé
|
||||
Et je continue de recevoir les emails d'avertissement tous les 6 mois
|
||||
Et mes contenus continuent d'être diffusés
|
||||
Et je peux me reconnecter à tout moment
|
||||
|
||||
# Définition de "écoute" pour les créateurs
|
||||
|
||||
Scénario: Qu'est-ce qu'une "écoute" pour le calcul d'inactivité
|
||||
Étant donné que je suis un créateur
|
||||
Quand le système calcule si mes contenus sont "actifs"
|
||||
Alors une "écoute" est comptabilisée si:
|
||||
| condition | comptabilisée |
|
||||
| Écoute complète (>80%) | oui |
|
||||
| Écoute partielle (>30%) | oui |
|
||||
| Skip rapide (<30%) | non |
|
||||
| Écoute par un bot (détecté) | non |
|
||||
Et au moins 1 écoute valide dans les 2 dernières années maintient le compte actif
|
||||
|
||||
# Principe de minimisation RGPD
|
||||
|
||||
Scénario: Conformité principe de minimisation
|
||||
Étant donné que le système de purge automatique est en place
|
||||
Quand un auditeur RGPD vérifie la conformité
|
||||
Alors le système respecte le principe de minimisation:
|
||||
| principe | respecté |
|
||||
| Conservation limitée dans le temps | oui |
|
||||
| Suppression automatique après inactivité | oui |
|
||||
| Délai raisonnable (5 ans) | oui |
|
||||
| Notifications préalables | oui |
|
||||
| Exception justifiée (contenus actifs) | oui |
|
||||
Et le délai de 5 ans est conforme aux standards de l'industrie
|
||||
|
||||
# Reset du compteur d'inactivité
|
||||
|
||||
Scénario: Actions qui réinitialisent le compteur d'inactivité
|
||||
Étant donné que je suis inactif depuis 4 ans
|
||||
Quand j'effectue l'une des actions suivantes:
|
||||
| action |
|
||||
| Connexion à l'application |
|
||||
| Publication d'un nouveau contenu |
|
||||
| Like d'un contenu |
|
||||
| Abonnement à un créateur |
|
||||
| Modification de mon profil |
|
||||
Alors le compteur d'inactivité est remis à zéro
|
||||
Et la suppression programmée est annulée
|
||||
Et je ne suis plus éligible à la purge pour 5 ans
|
||||
|
||||
# Logs d'audit
|
||||
|
||||
Scénario: Traçabilité des suppressions automatiques
|
||||
Étant donné qu'un compte est supprimé automatiquement
|
||||
Quand la suppression est effective
|
||||
Alors un log d'audit est créé avec:
|
||||
| champ | valeur |
|
||||
| user_id | [ID anonymisé] |
|
||||
| account_type | auditeur / créateur |
|
||||
| last_login | 2020-01-15T10:00:00Z |
|
||||
| last_content_listen | 2023-06-01T14:30:00Z |
|
||||
| purge_date | 2025-01-15T03:00:00Z |
|
||||
| notifications_sent | 3 (90j, 30j, 7j) |
|
||||
| reason | 5_years_inactivity |
|
||||
Et le log est conservé 5 ans pour audit RGPD
|
||||
Et l'user_id est pseudonymisé pour anonymat
|
||||
|
||||
# Cas particuliers
|
||||
|
||||
Scénario: Compte Premium inactif - pas de privilège spécial
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Et que je suis inactif depuis 5 ans
|
||||
Quand le job de purge s'exécute
|
||||
Alors mon compte est supprimé comme un compte gratuit
|
||||
Et l'abonnement Premium ne prolonge pas la durée de conservation
|
||||
Et aucun remboursement n'est effectué (compte inactif depuis 5 ans)
|
||||
|
||||
Scénario: Compte avec signalements de modération - purge différée
|
||||
Étant donné que je suis éligible à la purge
|
||||
Mais que j'ai des signalements de modération en cours
|
||||
Quand le job de purge s'exécute
|
||||
Alors ma purge est différée de 90 jours
|
||||
Et les signalements sont traités en priorité
|
||||
Et si les signalements aboutissent à un ban, le compte est supprimé immédiatement
|
||||
Et si les signalements sont infondés, la purge automatique reprend son cours
|
||||
|
||||
# Durée de 5 ans - justification
|
||||
|
||||
Scénario: Pourquoi 5 ans d'inactivité
|
||||
Étant donné que le délai de purge est fixé à 5 ans
|
||||
Quand on justifie ce choix
|
||||
Alors les raisons suivantes sont avancées:
|
||||
| justification |
|
||||
| Standard de l'industrie (Google, Facebook: 2-3 ans) |
|
||||
| Équilibre raisonnable entre minimisation et utilité |
|
||||
| Conforme aux recommandations CNIL |
|
||||
| Laisse une marge de réactivation pour utilisateurs |
|
||||
| Exception pour créateurs = intérêt légitime communauté |
|
||||
|
||||
# Communication transparente
|
||||
|
||||
Scénario: Politique de conservation visible dans les CGU
|
||||
Étant donné que je consulte les CGU de RoadWave
|
||||
Quand je lis la section "Conservation des données"
|
||||
Alors la politique de purge automatique est clairement expliquée:
|
||||
"""
|
||||
Conservation des données:
|
||||
- Comptes auditeurs: suppression automatique après 5 ans d'inactivité
|
||||
- Comptes créateurs: suppression après 5 ans d'inactivité + 2 ans sans écoute de leurs contenus
|
||||
- Exception: créateurs dont les contenus sont encore écoutés régulièrement
|
||||
- Notifications: 90j, 30j et 7j avant suppression
|
||||
- Contenus créés: conservés de manière anonyme après suppression du compte
|
||||
"""
|
||||
Et les utilisateurs sont informés dès l'inscription
|
||||
@@ -0,0 +1,227 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @cookies @analytics
|
||||
Fonctionnalité: Cookies et analytics avec Matomo self-hosted
|
||||
|
||||
# 13.7 - Matomo self-hosted, zéro cookie tiers (souveraineté données)
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur de l'application web RoadWave
|
||||
|
||||
# Liste des cookies utilisés
|
||||
|
||||
Scénario: Cookies strictement nécessaires - pas de consentement requis
|
||||
Étant donné que j'accède à l'application web
|
||||
Quand je me connecte
|
||||
Alors les cookies techniques suivants sont déposés:
|
||||
| cookie | type | durée | finalité | consentement |
|
||||
| session | Technique | 30j | Authentification | Non requis |
|
||||
| refresh_token | Technique | 30j | Session persistante | Non requis |
|
||||
Et ces cookies sont essentiels au fonctionnement de l'application
|
||||
Et ils sont exemptés de consentement selon l'article 82 de la loi Informatique et Libertés
|
||||
|
||||
Scénario: Cookie analytique Matomo - consentement requis
|
||||
Étant donné que j'ai accepté le consentement "Analytique"
|
||||
Quand je navigue sur l'application web
|
||||
Alors le cookie `_pk_id` est déposé
|
||||
Et la durée de conservation est de 13 mois
|
||||
Et ce cookie sert à Matomo pour analytics
|
||||
Et mon IP est automatiquement anonymisée (2 derniers octets)
|
||||
|
||||
Scénario: Refus du consentement analytique - pas de cookie Matomo
|
||||
Étant donné que j'ai refusé le consentement "Analytique"
|
||||
Quand je navigue sur l'application web
|
||||
Alors aucun cookie `_pk_id` n'est déposé
|
||||
Et aucune donnée d'usage n'est collectée
|
||||
Et l'application fonctionne normalement sans analytics
|
||||
|
||||
# Matomo self-hosted
|
||||
|
||||
Scénario: Matomo hébergé sur les serveurs RoadWave
|
||||
Étant donné que RoadWave utilise Matomo pour les analytics
|
||||
Quand on analyse l'infrastructure
|
||||
Alors Matomo est installé sur les serveurs RoadWave (Docker)
|
||||
Et aucune donnée n'est envoyée à un service tiers
|
||||
Et toutes les données restent dans l'UE
|
||||
Et l'accès à Matomo est restreint à l'équipe RoadWave
|
||||
|
||||
Scénario: IP anonymisées automatiquement
|
||||
Étant donné que Matomo collecte des données d'usage
|
||||
Quand une requête est enregistrée
|
||||
Alors l'adresse IP est automatiquement anonymisée
|
||||
Et les 2 derniers octets sont remplacés par des zéros
|
||||
Et une IP 192.168.1.100 devient 192.168.0.0
|
||||
Et cette anonymisation est irréversible
|
||||
Et elle est conforme aux recommandations CNIL
|
||||
|
||||
Scénario: Configuration Matomo conforme RGPD
|
||||
Étant donné que Matomo est configuré pour RoadWave
|
||||
Quand on vérifie les paramètres
|
||||
Alors les configurations suivantes sont activées:
|
||||
| paramètre | valeur |
|
||||
| Anonymisation IP (2 octets) | activé |
|
||||
| Respect Do Not Track | activé |
|
||||
| Suppression auto anciens logs (25 mois)| activé |
|
||||
| Géolocalisation IP désactivée | activé |
|
||||
| User ID anonymisé | activé |
|
||||
Et la configuration est RGPD-compliant
|
||||
|
||||
# Trackers interdits
|
||||
|
||||
Scénario: Aucun tracker tiers utilisé
|
||||
Étant donné que j'accède à l'application web
|
||||
Quand j'inspecte les requêtes réseau avec les DevTools
|
||||
Alors aucune requête n'est envoyée vers les domaines suivants:
|
||||
| domaine tiers interdit |
|
||||
| google-analytics.com |
|
||||
| facebook.com (Pixel) |
|
||||
| hotjar.com |
|
||||
| mixpanel.com |
|
||||
| segment.io |
|
||||
| amplitude.com |
|
||||
Et toutes les requêtes analytics vont uniquement vers matomo.roadwave.fr
|
||||
|
||||
Scénario: Conformité zéro cookie tiers
|
||||
Étant donné que j'analyse les cookies déposés sur roadwave.fr
|
||||
Quand je consulte la liste des cookies
|
||||
Alors tous les cookies sont first-party (domaine roadwave.fr)
|
||||
Et aucun cookie tiers (third-party) n'est présent
|
||||
Et cette politique respecte les recommandations CNIL 2020
|
||||
|
||||
# Alternative: Plausible (si besoin)
|
||||
|
||||
Scénario: Alternative Plausible SaaS (EU-hosted)
|
||||
Étant donné que RoadWave pourrait utiliser Plausible au lieu de Matomo
|
||||
Quand on compare les deux solutions
|
||||
Alors Plausible a les caractéristiques suivantes:
|
||||
| caractéristique | valeur |
|
||||
| Hébergement | UE (Allemagne) |
|
||||
| Conformité RGPD | Natif (pas de cookie) |
|
||||
| Coût | 9€/mois (50K pageviews) |
|
||||
| IP anonymisées | Automatique |
|
||||
| Consentement requis | Non (selon CNIL 2020) |
|
||||
Mais Matomo self-hosted reste le choix prioritaire (0€, contrôle total)
|
||||
|
||||
# Souveraineté des données
|
||||
|
||||
Scénario: Aucun transfert de données hors UE
|
||||
Étant donné que Matomo est self-hosted
|
||||
Quand on analyse les flux de données
|
||||
Alors aucune donnée d'analytics n'est transférée hors de l'UE
|
||||
Et les serveurs sont localisés en France
|
||||
Et aucun transfert vers les US (pas de Privacy Shield / DPF requis)
|
||||
Et la souveraineté des données est garantie
|
||||
|
||||
# Coût de la solution
|
||||
|
||||
Scénario: Matomo self-hosted - coût estimé
|
||||
Étant donné que Matomo est hébergé sur l'infrastructure RoadWave
|
||||
Quand on calcule le coût mensuel
|
||||
Alors le coût est d'environ 5€/mois:
|
||||
| composant | coût |
|
||||
| Serveur supplémentaire | 0€ (mutualisé) |
|
||||
| Base de données MySQL | 0€ (mutualisé) |
|
||||
| Stockage logs (25 mois) | ~5€/mois |
|
||||
| License Matomo | 0€ (opensource) |
|
||||
Et ce coût est marginal comparé à un SaaS tiers (9-50€/mois)
|
||||
|
||||
# Respect Do Not Track
|
||||
|
||||
Scénario: Respect du signal Do Not Track (DNT)
|
||||
Étant donné que mon navigateur envoie le header "DNT: 1"
|
||||
Quand j'accède à l'application web
|
||||
Alors Matomo détecte le signal DNT
|
||||
Et aucune donnée d'usage n'est collectée
|
||||
Et aucun cookie `_pk_id` n'est déposé
|
||||
Et l'application fonctionne normalement
|
||||
Et un message discret s'affiche: "Vos préférences de confidentialité sont respectées (DNT activé)"
|
||||
|
||||
# Suppression automatique des anciens logs
|
||||
|
||||
Scénario: Logs Matomo supprimés après 25 mois
|
||||
Étant donné que Matomo collecte des données d'usage
|
||||
Quand les logs atteignent 25 mois d'ancienneté
|
||||
Alors un job automatique supprime les anciens logs
|
||||
Et seules les données agrégées (rapports) sont conservées
|
||||
Et les données brutes (logs) sont supprimées définitivement
|
||||
Et cette politique respecte le principe de minimisation RGPD
|
||||
|
||||
# Métriques collectées par Matomo
|
||||
|
||||
Scénario: Données collectées par Matomo
|
||||
Étant donné que j'ai accepté le consentement "Analytique"
|
||||
Quand je navigue sur l'application web
|
||||
Alors Matomo collecte les données suivantes:
|
||||
| donnée collectée | anonymisée |
|
||||
| Pages visitées | non |
|
||||
| Durée de visite | non |
|
||||
| Navigateur / OS | non |
|
||||
| Résolution écran | non |
|
||||
| Provenance (referrer) | non |
|
||||
| IP (2 derniers octets) | oui |
|
||||
| User ID (hashé) | oui |
|
||||
Et aucune donnée personnelle identifiable n'est collectée
|
||||
|
||||
Scénario: User ID hashé pour analytics
|
||||
Étant donné que je suis connecté à l'application
|
||||
Et que j'ai accepté le consentement "Analytique"
|
||||
Quand Matomo enregistre mes actions
|
||||
Alors mon user_id est hashé (SHA-256)
|
||||
Et le hash est 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
|
||||
Et il est impossible de retrouver mon user_id original depuis ce hash
|
||||
Et ce processus garantit l'anonymat
|
||||
|
||||
# Conformité CNIL 2020
|
||||
|
||||
Scénario: Conformité recommandations CNIL sur les cookies
|
||||
Étant donné que RoadWave utilise Matomo self-hosted
|
||||
Quand un auditeur CNIL vérifie la conformité
|
||||
Alors le système respecte les recommandations CNIL 2020:
|
||||
| recommandation CNIL | respecté |
|
||||
| Consentement requis pour cookies analytics | oui |
|
||||
| IP anonymisées | oui |
|
||||
| Pas de transfert hors UE | oui |
|
||||
| Durée conservation limitée (25 mois) | oui |
|
||||
| Respect Do Not Track | oui |
|
||||
| Transparence (liste cookies dans CGU) | oui |
|
||||
|
||||
# Tarteaucitron.js - gestion du consentement
|
||||
|
||||
Scénario: Intégration Tarteaucitron.js pour gérer Matomo
|
||||
Étant donné que Tarteaucitron.js gère les consentements
|
||||
Quand je personnalise mes consentements
|
||||
Alors je vois l'option "Analytique (Matomo)"
|
||||
Et une description est affichée:
|
||||
"""
|
||||
Matomo nous aide à comprendre comment vous utilisez l'application pour l'améliorer.
|
||||
Vos données restent anonymes et ne sont jamais partagées avec des tiers.
|
||||
"""
|
||||
Et je peux activer ou désactiver Matomo indépendamment
|
||||
Et si je désactive, le cookie `_pk_id` est supprimé immédiatement
|
||||
|
||||
# Application mobile - analytics
|
||||
|
||||
Scénario: Analytics sur application mobile
|
||||
Étant donné que j'utilise l'application mobile
|
||||
Quand j'accepte le consentement "Analytique"
|
||||
Alors l'app utilise le SDK Matomo Mobile
|
||||
Et les données sont envoyées à la même instance Matomo self-hosted
|
||||
Et les mêmes règles d'anonymisation s'appliquent
|
||||
Et aucun SDK tiers (Google Analytics, Firebase) n'est utilisé
|
||||
|
||||
Scénario: Refus analytics sur mobile
|
||||
Étant donné que j'ai refusé le consentement "Analytique" sur mobile
|
||||
Quand j'utilise l'application
|
||||
Alors aucune donnée d'usage n'est collectée
|
||||
Et le SDK Matomo est désactivé
|
||||
Et l'application fonctionne normalement sans différence d'UX
|
||||
|
||||
# Opensource et auditabilité
|
||||
|
||||
Scénario: Matomo opensource et auditable
|
||||
Étant donné que Matomo est opensource
|
||||
Quand on consulte le code source
|
||||
Alors le code est disponible publiquement sur GitHub
|
||||
Et le code peut être audité par des experts indépendants
|
||||
Et aucune backdoor ou collecte cachée n'est possible
|
||||
Et cette transparence renforce la confiance utilisateur
|
||||
@@ -0,0 +1,224 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @geoip @degraded-mode
|
||||
Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
|
||||
|
||||
# 13.5 - GeoIP par défaut, GPS optionnel (progressive disclosure)
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un nouvel utilisateur
|
||||
Et que je lance l'application pour la première fois
|
||||
|
||||
# Niveaux de précision
|
||||
|
||||
Plan du Scénario: Trois niveaux de géolocalisation disponibles
|
||||
Étant donné que j'utilise le niveau de géolocalisation "<niveau>"
|
||||
Quand le système détermine ma position
|
||||
Alors la technologie utilisée est "<technologie>"
|
||||
Et les contenus accessibles sont "<contenus>"
|
||||
Et le consentement RGPD est "<consentement>"
|
||||
|
||||
Exemples:
|
||||
| niveau | technologie | contenus | consentement |
|
||||
| Pays | Aucune géoloc | Contenus nationaux uniquement | Non requis |
|
||||
| Ville | GeoIP (IP2Location) | Contenus régionaux/ville | Non requis |
|
||||
| Précis | GPS | Tous contenus (hyperlocaux inclus) | Requis |
|
||||
|
||||
# Démarrage avec GeoIP automatique
|
||||
|
||||
Scénario: GeoIP activé par défaut au premier lancement
|
||||
Étant donné que je lance l'application pour la première fois
|
||||
Et que je n'ai pas encore accepté le GPS précis
|
||||
Quand l'application démarre
|
||||
Alors le système utilise automatiquement GeoIP basé sur mon adresse IP
|
||||
Et ma position est détectée au niveau ville: "Paris, France"
|
||||
Et aucun consentement n'est requis (GeoIP ne collecte pas de données personnelles)
|
||||
Et je peux accéder aux contenus régionaux et de ville
|
||||
|
||||
Scénario: Détection de ville avec IP2Location Lite
|
||||
Étant donné que mon adresse IP est 93.184.216.34
|
||||
Quand le système utilise GeoIP IP2Location Lite
|
||||
Alors ma ville est détectée: "Paris"
|
||||
Et ma région est détectée: "Île-de-France"
|
||||
Et mon pays est détecté: "France"
|
||||
Et la précision est d'environ 80% au niveau ville
|
||||
Et aucune coordonnée GPS précise n'est révélée
|
||||
|
||||
# Banner in-app pour upgrade vers GPS
|
||||
|
||||
Scénario: Banner d'invitation à activer le GPS
|
||||
Étant donné que j'utilise l'application en mode GeoIP
|
||||
Quand je suis sur l'écran principal
|
||||
Alors un banner discret s'affiche en haut:
|
||||
"""
|
||||
Activez la géolocalisation pour découvrir du contenu près de chez vous
|
||||
[Activer]
|
||||
"""
|
||||
Et le banner n'est pas intrusif (pas de popup modale)
|
||||
Et je peux le fermer temporairement avec un bouton X
|
||||
Et le banner réapparaît tous les 7 jours si je ne l'active pas
|
||||
|
||||
Scénario: Upgrade volontaire vers GPS depuis le banner
|
||||
Étant donné que le banner d'invitation au GPS est affiché
|
||||
Quand je clique sur "Activer"
|
||||
Alors un écran de consentement GPS s'affiche
|
||||
Et l'écran explique les avantages:
|
||||
"""
|
||||
En activant la géolocalisation précise, vous pouvez:
|
||||
- Découvrir du contenu hyperlocal près de vous
|
||||
- Accéder aux audio-guides géolocalisés
|
||||
- Recevoir des notifications quand vous passez près de points d'intérêt
|
||||
"""
|
||||
Et je peux accepter ou refuser
|
||||
Et si j'accepte, la permission OS est demandée
|
||||
|
||||
# Contenus accessibles selon le niveau
|
||||
|
||||
Scénario: Contenus disponibles en mode Pays (aucune géoloc)
|
||||
Étant donné que je n'autorise aucune géolocalisation
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors seuls les contenus "National" sont disponibles
|
||||
Et les contenus géolocalisés (Ancré, Contextuel) ne sont pas proposés
|
||||
Et je vois un message: "Activez la géolocalisation pour plus de contenu local"
|
||||
|
||||
Scénario: Contenus disponibles en mode Ville (GeoIP)
|
||||
Étant donné que j'utilise le mode GeoIP et que je suis détecté à Paris
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors les contenus suivants sont disponibles:
|
||||
| type_contenu | disponible |
|
||||
| National | oui |
|
||||
| Région Île-de-France | oui |
|
||||
| Ville Paris | oui |
|
||||
| Hyperlocal (GPS) | non |
|
||||
| Audio-guides | non |
|
||||
Et je reçois des recommandations pertinentes pour Paris
|
||||
|
||||
Scénario: Tous contenus disponibles en mode Précis (GPS)
|
||||
Étant donné que j'ai activé la géolocalisation précise
|
||||
Quand le système recherche du contenu à me proposer
|
||||
Alors tous les types de contenus sont disponibles:
|
||||
| type_contenu | disponible |
|
||||
| National | oui |
|
||||
| Régional | oui |
|
||||
| Ville | oui |
|
||||
| Hyperlocal (Ancré) | oui |
|
||||
| Contextuel | oui |
|
||||
| Audio-guides | oui |
|
||||
|
||||
# Pas de consentement requis pour GeoIP
|
||||
|
||||
Scénario: GeoIP ne nécessite pas de consentement RGPD
|
||||
Étant donné que j'utilise le mode GeoIP
|
||||
Quand un auditeur CNIL vérifie la conformité
|
||||
Alors GeoIP n'est pas considéré comme une donnée personnelle
|
||||
Et l'adresse IP n'est pas conservée après détection de la ville
|
||||
Et seule la ville est stockée (non identifiant)
|
||||
Et aucun consentement n'est requis conformément au RGPD
|
||||
|
||||
# Implémentation IP2Location Lite
|
||||
|
||||
Scénario: Base de données IP2Location self-hosted
|
||||
Étant donné que RoadWave utilise IP2Location Lite
|
||||
Quand on analyse l'infrastructure
|
||||
Alors la base de données IP2Location est hébergée sur les serveurs RoadWave
|
||||
Et aucune requête n'est envoyée à un service tiers
|
||||
Et la base de données est mise à jour automatiquement chaque mois
|
||||
Et le coût est de 0€ (IP2Location Lite est gratuit)
|
||||
|
||||
Scénario: Mise à jour mensuelle de la base GeoIP
|
||||
Étant donné que IP2Location publie des mises à jour mensuelles
|
||||
Quand le 1er du mois arrive
|
||||
Alors un job automatique télécharge la nouvelle base IP2Location Lite
|
||||
Et la base est mise à jour sans interruption de service
|
||||
Et un log est créé pour traçabilité
|
||||
Et si la mise à jour échoue, une alerte est envoyée
|
||||
|
||||
# Progressive disclosure - UX dégradée acceptable
|
||||
|
||||
Scénario: UX acceptable en mode GeoIP
|
||||
Étant donné que j'utilise le mode GeoIP à Paris
|
||||
Quand je parcours l'application
|
||||
Alors je peux écouter du contenu pertinent pour Paris et l'Île-de-France
|
||||
Et l'expérience est satisfaisante même sans GPS précis
|
||||
Et je ne suis pas bloqué dans l'utilisation de l'application
|
||||
Et je peux choisir d'activer le GPS quand je le souhaite
|
||||
|
||||
Scénario: Incitation progressive à activer le GPS
|
||||
Étant donné que j'utilise le mode GeoIP depuis 2 semaines
|
||||
Et que je n'ai pas activé le GPS
|
||||
Quand je consulte un audio-guide dans les résultats de recherche
|
||||
Alors un message s'affiche:
|
||||
"""
|
||||
Cet audio-guide nécessite la géolocalisation précise pour fonctionner.
|
||||
[Activer le GPS] [Plus tard]
|
||||
"""
|
||||
Et si je clique "Plus tard", je peux continuer à utiliser l'app normalement
|
||||
Et l'incitation reste douce et non intrusive
|
||||
|
||||
# Passage d'un mode à l'autre
|
||||
|
||||
Scénario: Upgrade GeoIP vers GPS
|
||||
Étant donné que j'utilise le mode GeoIP
|
||||
Quand j'active la géolocalisation précise
|
||||
Alors le système bascule immédiatement en mode GPS
|
||||
Et les contenus hyperlocaux deviennent disponibles
|
||||
Et mon feed se rafraîchit avec du contenu plus précis
|
||||
Et un toast de confirmation s'affiche: "Géolocalisation activée"
|
||||
|
||||
Scénario: Downgrade GPS vers GeoIP
|
||||
Étant donné que j'utilise le mode GPS précis
|
||||
Quand je désactive la géolocalisation dans les paramètres OS
|
||||
Alors le système bascule automatiquement en mode GeoIP
|
||||
Et les contenus hyperlocaux ne sont plus proposés
|
||||
Et un banner s'affiche: "Géolocalisation désactivée. Seul le contenu régional est disponible."
|
||||
Et l'application continue de fonctionner normalement
|
||||
|
||||
# Détection automatique du mode
|
||||
|
||||
Scénario: Détection automatique au démarrage de l'app
|
||||
Étant donné que j'ouvre l'application
|
||||
Quand l'app vérifie les permissions de géolocalisation
|
||||
Alors le système détecte automatiquement le mode disponible:
|
||||
| permission GPS | consentement app | mode activé |
|
||||
| Refusée | Non demandé | Pays |
|
||||
| Refusée | Accepté | GeoIP |
|
||||
| Accordée | Accepté | GPS précis |
|
||||
Et le mode est appliqué sans interaction utilisateur
|
||||
|
||||
# Précision GeoIP ~80%
|
||||
|
||||
Scénario: Précision acceptable pour la plupart des cas
|
||||
Étant donné que j'habite à Lyon
|
||||
Et que mon IP est une IP résidentielle standard
|
||||
Quand le système utilise GeoIP pour me localiser
|
||||
Alors la ville détectée est "Lyon" (correct à 80%)
|
||||
Et dans 20% des cas, la ville peut être légèrement erronée (banlieue proche)
|
||||
Et cette précision est suffisante pour proposer du contenu régional pertinent
|
||||
|
||||
Scénario: GeoIP avec VPN ou proxy
|
||||
Étant donné que j'utilise un VPN avec une IP sortante à Paris
|
||||
Mais que je suis physiquement à Lyon
|
||||
Quand le système utilise GeoIP
|
||||
Alors la ville détectée est "Paris" (IP du VPN)
|
||||
Et les contenus proposés sont pour Paris
|
||||
Et je peux activer le GPS précis pour corriger la localisation
|
||||
|
||||
# Conformité RGPD
|
||||
|
||||
Scénario: Pas de donnée personnelle collectée avec GeoIP
|
||||
Étant donné que j'utilise le mode GeoIP
|
||||
Quand le système détermine ma ville via mon IP
|
||||
Alors l'adresse IP n'est pas conservée après détection
|
||||
Et seule la ville "Paris" est stockée en base de données
|
||||
Et la ville seule n'est pas une donnée personnelle (RGPD)
|
||||
Et aucun consentement n'est donc requis
|
||||
|
||||
# Coût de la solution: 0€
|
||||
|
||||
Scénario: Solution GeoIP gratuite et self-hosted
|
||||
Étant donné que RoadWave utilise IP2Location Lite
|
||||
Quand on calcule le coût de la solution
|
||||
Alors le coût est de 0€
|
||||
Et la solution est opensource
|
||||
Et la base de données est hébergée sur les serveurs RoadWave
|
||||
Et aucun coût SaaS tiers
|
||||
@@ -0,0 +1,282 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @data-portability
|
||||
Fonctionnalité: Portabilité des données (Article 20 RGPD)
|
||||
|
||||
# 13.3 - Export JSON + HTML + ZIP, génération asynchrone
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que j'ai utilisé l'application depuis 6 mois
|
||||
|
||||
Scénario: Demande d'export depuis les paramètres
|
||||
Étant donné que je suis dans "Paramètres > Confidentialité"
|
||||
Quand je clique sur "Exporter mes données"
|
||||
Alors une page d'information s'affiche expliquant:
|
||||
"""
|
||||
Vous allez recevoir une archive contenant toutes vos données personnelles:
|
||||
- Profil et informations de compte
|
||||
- Historique d'écoute complet
|
||||
- Contenus audio que vous avez créés
|
||||
- Abonnements et likes
|
||||
- Centres d'intérêt
|
||||
- Historique des consentements
|
||||
|
||||
L'export sera généré sous 48h maximum et vous recevrez un email avec un lien de téléchargement valide 7 jours.
|
||||
"""
|
||||
Et un bouton "Confirmer l'export" est disponible
|
||||
|
||||
Scénario: Confirmation et démarrage de l'export
|
||||
Étant donné que je clique sur "Confirmer l'export"
|
||||
Quand la demande est validée
|
||||
Alors un message de confirmation s'affiche:
|
||||
"""
|
||||
Votre demande d'export a été prise en compte. Vous recevrez un email sous 48h avec un lien de téléchargement.
|
||||
"""
|
||||
Et un worker background démarre la génération de l'export
|
||||
Et le statut de l'export est "En cours de génération"
|
||||
Et je peux voir le statut dans "Paramètres > Confidentialité > Mes exports"
|
||||
|
||||
# Structure de l'export
|
||||
|
||||
Scénario: Contenu de l'archive ZIP
|
||||
Étant donné que mon export est généré
|
||||
Quand je télécharge et ouvre l'archive
|
||||
Alors l'archive a la structure suivante:
|
||||
"""
|
||||
export-roadwave-[user_id]-[date].zip
|
||||
├── export.json # Machine-readable (JSON complet)
|
||||
├── index.html # Human-readable (stylé, navigation)
|
||||
├── audio/
|
||||
│ ├── content-123.opus # Mes contenus créés
|
||||
│ ├── content-456.opus
|
||||
│ └── ...
|
||||
└── README.txt # Instructions et informations
|
||||
"""
|
||||
Et tous les fichiers sont inclus
|
||||
|
||||
Scénario: Contenu du fichier export.json
|
||||
Étant donné que j'ouvre le fichier export.json
|
||||
Quand j'analyse le contenu
|
||||
Alors le JSON contient les sections suivantes:
|
||||
| section | description |
|
||||
| profile | Email, pseudo, date inscription, bio |
|
||||
| listening_history | Historique complet d'écoute |
|
||||
| created_contents | Métadonnées des contenus créés |
|
||||
| subscriptions | Liste des créateurs suivis |
|
||||
| likes | Liste des contenus likés |
|
||||
| interest_gauges | Valeurs des jauges d'intérêt |
|
||||
| consent_history | Historique des consentements |
|
||||
| premium_subscription | Informations abonnement Premium |
|
||||
Et le JSON est formaté de manière lisible (indentation)
|
||||
Et toutes les dates sont au format ISO 8601
|
||||
|
||||
Scénario: Contenu du fichier index.html
|
||||
Étant donné que j'ouvre le fichier index.html dans un navigateur
|
||||
Quand la page se charge
|
||||
Alors je vois un site web stylé avec navigation
|
||||
Et les sections suivantes sont affichées:
|
||||
| section | contenu |
|
||||
| Mon profil | Email, pseudo, date inscription, statistiques |
|
||||
| Historique d'écoute | Liste paginée avec dates, titres, durées |
|
||||
| Mes contenus | Liste avec lectures audio intégrées |
|
||||
| Mes abonnements | Grille des créateurs suivis |
|
||||
| Mes likes | Liste des contenus likés avec liens |
|
||||
| Centres d'intérêt | Graphiques des jauges |
|
||||
| Consentements | Historique des acceptations/refus |
|
||||
Et la navigation est intuitive (menu latéral)
|
||||
Et le design est responsive (mobile/desktop)
|
||||
|
||||
Scénario: Fichiers audio inclus dans l'export
|
||||
Étant donné que j'ai créé 5 contenus audio
|
||||
Quand mon export est généré
|
||||
Alors le dossier `audio/` contient mes 5 fichiers
|
||||
Et les fichiers sont au format Opus original
|
||||
Et chaque fichier est nommé: `content-[id].opus`
|
||||
Et les fichiers audio correspondent aux métadonnées dans export.json
|
||||
|
||||
Scénario: Fichier README.txt explicatif
|
||||
Étant donné que j'ouvre le fichier README.txt
|
||||
Quand je lis le contenu
|
||||
Alors le fichier explique:
|
||||
"""
|
||||
Bienvenue dans votre export de données RoadWave.
|
||||
|
||||
Contenu de l'archive:
|
||||
- export.json: Toutes vos données au format JSON (machine-readable)
|
||||
- index.html: Visualisation web de vos données (ouvrir dans un navigateur)
|
||||
- audio/: Vos contenus audio créés au format Opus
|
||||
|
||||
Cet export a été généré le [date] et contient toutes vos données personnelles conformément à l'article 20 du RGPD.
|
||||
|
||||
Pour toute question: dpo@roadwave.fr
|
||||
"""
|
||||
|
||||
# Données exportées en détail
|
||||
|
||||
Plan du Scénario: Données de profil exportées
|
||||
Étant donné que mon export est généré
|
||||
Quand j'ouvre export.json et lis la section "profile"
|
||||
Alors je trouve les données suivantes:
|
||||
| champ | exemple |
|
||||
| email | user@example.com |
|
||||
| pseudo | @roadwave_user |
|
||||
| date_inscription | 2025-01-15T10:30:00Z |
|
||||
| bio | Passionné d'automobile... |
|
||||
| avatar_url | https://cdn.roadwave.fr/... |
|
||||
| compte_verifie | false |
|
||||
| premium | true |
|
||||
|
||||
Scénario: Historique d'écoute exporté
|
||||
Étant donné que j'ai écouté 150 contenus depuis 6 mois
|
||||
Quand mon export est généré
|
||||
Alors la section "listening_history" contient 150 entrées
|
||||
Et chaque entrée contient:
|
||||
| champ | exemple |
|
||||
| content_id | C123 |
|
||||
| content_title | Histoire de la Tour Eiffel |
|
||||
| creator_name | @historien_paris |
|
||||
| listened_at | 2025-01-20T15:30:00Z |
|
||||
| duration_listened | 180 (secondes) |
|
||||
| completion_rate | 0.85 (85%) |
|
||||
| location | [geohash ou coords précises]|
|
||||
Et les contenus sont triés par date décroissante
|
||||
|
||||
Scénario: Centres d'intérêt exportés
|
||||
Étant donné que mes jauges d'intérêt sont:
|
||||
| catégorie | valeur |
|
||||
| Automobile | 78% |
|
||||
| Voyage | 65% |
|
||||
| Musique | 52% |
|
||||
| Politique | 30% |
|
||||
Quand mon export est généré
|
||||
Alors la section "interest_gauges" contient ces valeurs
|
||||
Et chaque jauge indique la date de dernière modification
|
||||
|
||||
Scénario: Historique des consentements exporté
|
||||
Étant donné que j'ai modifié mes consentements plusieurs fois
|
||||
Quand mon export est généré
|
||||
Alors la section "consent_history" contient:
|
||||
| date | consent_type | accepted | version |
|
||||
| 2025-01-15T10:00 | Fonctionnel | oui | 1 |
|
||||
| 2025-01-15T10:00 | Analytique | oui | 1 |
|
||||
| 2025-01-15T10:00 | Marketing | non | 1 |
|
||||
| 2025-03-20T14:30 | Analytique | non | 1 |
|
||||
Et l'historique complet est visible
|
||||
|
||||
# Génération asynchrone
|
||||
|
||||
Scénario: Génération asynchrone pour éviter timeout
|
||||
Étant donné que j'ai beaucoup de données (500 contenus créés, 10 000 écoutes)
|
||||
Quand je demande un export
|
||||
Alors la génération se fait en arrière-plan via un worker
|
||||
Et la page web ne timeout pas
|
||||
Et je peux continuer à utiliser l'application pendant la génération
|
||||
Et je reçois un email quand l'export est prêt
|
||||
|
||||
Scénario: Délai de génération conforme RGPD
|
||||
Étant donné que je demande un export le 2025-01-20 à 10:00
|
||||
Quand le worker génère l'export
|
||||
Alors l'export est disponible maximum 48h plus tard (avant le 2025-01-22 à 10:00)
|
||||
Et la plupart des exports sont prêts en moins de 6h
|
||||
Et le délai respecte l'article 20 du RGPD
|
||||
|
||||
Scénario: Email de notification avec lien de téléchargement
|
||||
Étant donné que mon export est terminé
|
||||
Quand le worker finalise la génération
|
||||
Alors je reçois un email avec le sujet "Votre export de données RoadWave est prêt"
|
||||
Et l'email contient:
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Votre export de données est prêt. Vous pouvez le télécharger via le lien ci-dessous:
|
||||
|
||||
[Télécharger mon export] (lien sécurisé, expire dans 7 jours)
|
||||
|
||||
Taille de l'archive: 45 MB
|
||||
Contenu: Profil, historique, contenus audio, abonnements, likes, centres d'intérêt
|
||||
|
||||
Pour toute question: dpo@roadwave.fr
|
||||
"""
|
||||
Et le lien de téléchargement est sécurisé (token unique)
|
||||
|
||||
Scénario: Lien de téléchargement expire après 7 jours
|
||||
Étant donné que mon export a été généré le 2025-01-20
|
||||
Et que je reçois le lien de téléchargement
|
||||
Quand j'essaie d'accéder au lien le 2025-01-28 (8 jours plus tard)
|
||||
Alors le lien est expiré
|
||||
Et je reçois un message "Ce lien a expiré. Veuillez demander un nouvel export."
|
||||
Et je peux demander un nouvel export si nécessaire
|
||||
|
||||
# Limite anti-abus
|
||||
|
||||
Scénario: Limite de 1 export par mois
|
||||
Étant donné que j'ai demandé un export le 2025-01-15
|
||||
Quand j'essaie de demander un nouvel export le 2025-01-20
|
||||
Alors je reçois un message d'erreur:
|
||||
"""
|
||||
Vous avez déjà demandé un export ce mois-ci. Vous pourrez demander un nouvel export à partir du 2025-02-15.
|
||||
"""
|
||||
Et le bouton "Confirmer l'export" est désactivé
|
||||
Et la date du prochain export possible est affichée
|
||||
|
||||
Scénario: Nouvel export possible après 1 mois
|
||||
Étant donné que j'ai demandé un export le 2025-01-15
|
||||
Quand la date atteint le 2025-02-15
|
||||
Alors je peux demander un nouvel export
|
||||
Et le bouton "Confirmer l'export" est actif
|
||||
Et aucune limite ne s'applique
|
||||
|
||||
# Sécurité
|
||||
|
||||
Scénario: Lien de téléchargement sécurisé avec token unique
|
||||
Étant donné que mon export est prêt
|
||||
Quand je reçois le lien de téléchargement
|
||||
Alors le lien contient un token unique et non devinable
|
||||
Et le format du lien est: `https://roadwave.fr/exports/download/[token_unique]`
|
||||
Et le token est valide uniquement pour mon compte
|
||||
Et le token expire après 7 jours ou après 3 téléchargements
|
||||
|
||||
Scénario: Vérification de l'authentification avant téléchargement
|
||||
Étant donné que je reçois le lien d'export
|
||||
Quand je clique sur le lien
|
||||
Alors le système vérifie que je suis connecté
|
||||
Et si je ne suis pas connecté, je suis redirigé vers la page de connexion
|
||||
Et après connexion, le téléchargement démarre automatiquement
|
||||
Et seul le propriétaire du compte peut télécharger l'export
|
||||
|
||||
# Conformité RGPD Article 20
|
||||
|
||||
Scénario: Conformité portabilité des données
|
||||
Étant donné que mon export est généré
|
||||
Quand un auditeur RGPD vérifie la conformité
|
||||
Alors l'export respecte les exigences de l'article 20:
|
||||
| exigence RGPD | respecté |
|
||||
| Format structuré (JSON) | oui |
|
||||
| Format couramment utilisé | oui |
|
||||
| Format lisible par machine | oui |
|
||||
| Format interopérable | oui |
|
||||
| Délai raisonnable (48h max) | oui |
|
||||
| Exhaustivité des données | oui |
|
||||
| Gratuité pour l'utilisateur | oui |
|
||||
|
||||
Scénario: Gratuité de l'export
|
||||
Étant donné que je demande un export de mes données
|
||||
Quand l'export est généré et téléchargé
|
||||
Alors aucun coût n'est facturé
|
||||
Et l'export est entièrement gratuit
|
||||
Et aucune inscription Premium n'est requise
|
||||
Et le droit à la portabilité est accessible à tous les utilisateurs
|
||||
|
||||
# Statut de l'export visible
|
||||
|
||||
Scénario: Suivi du statut de génération
|
||||
Étant donné que j'ai demandé un export
|
||||
Quand j'ouvre "Paramètres > Confidentialité > Mes exports"
|
||||
Alors je vois le statut actuel:
|
||||
| statut | description |
|
||||
| En cours de génération| Worker en train de générer l'archive |
|
||||
| Prêt au téléchargement| Lien de téléchargement disponible |
|
||||
| Expiré | Lien expiré (>7j), nouvel export requis |
|
||||
Et la date de demande est affichée
|
||||
Et la taille estimée de l'archive est visible
|
||||
@@ -0,0 +1,254 @@
|
||||
# language: fr
|
||||
|
||||
@rgpd @account-deletion
|
||||
Fonctionnalité: Suppression du compte utilisateur (Article 17 RGPD - Droit à l'effacement)
|
||||
|
||||
# 13.4 - Grace period 30j + anonymisation contenus
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur connecté
|
||||
Et que j'ai utilisé l'application depuis plusieurs mois
|
||||
|
||||
Scénario: Demande de suppression depuis les paramètres
|
||||
Étant donné que je suis dans "Paramètres > Compte"
|
||||
Quand je clique sur "Supprimer mon compte"
|
||||
Alors une page d'avertissement s'affiche avec le message:
|
||||
"""
|
||||
Attention: La suppression de votre compte est définitive.
|
||||
|
||||
Que se passe-t-il lors de la suppression?
|
||||
- Votre compte sera désactivé immédiatement (connexion impossible)
|
||||
- Vos données personnelles seront supprimées dans 30 jours
|
||||
- Vos contenus créés resteront disponibles de manière anonyme
|
||||
- Vous pouvez annuler la suppression dans les 30 jours
|
||||
|
||||
Voulez-vous vraiment continuer?
|
||||
"""
|
||||
Et deux boutons sont disponibles: "Annuler" et "Confirmer la suppression"
|
||||
|
||||
Scénario: Confirmation de suppression avec mot de passe
|
||||
Étant donné que je clique sur "Confirmer la suppression"
|
||||
Quand un formulaire de confirmation s'affiche
|
||||
Alors je dois entrer mon mot de passe pour confirmer
|
||||
Et je dois cocher "Je comprends que cette action est définitive"
|
||||
Et un captcha peut être requis pour éviter les suppressions automatisées
|
||||
Quand je valide le formulaire
|
||||
Alors la suppression est initiée
|
||||
|
||||
# Désactivation immédiate
|
||||
|
||||
Scénario: Compte désactivé immédiatement après confirmation
|
||||
Étant donné que j'ai confirmé la suppression de mon compte
|
||||
Quand la demande est traitée
|
||||
Alors mon compte est désactivé immédiatement
|
||||
Et je suis déconnecté de toutes mes sessions
|
||||
Et je ne peux plus me reconnecter
|
||||
Et si j'essaie de me connecter, je reçois le message:
|
||||
"""
|
||||
Votre compte est en cours de suppression. Si vous souhaitez annuler, cliquez sur le lien reçu par email.
|
||||
"""
|
||||
|
||||
Scénario: Contenus cachés pendant le grace period
|
||||
Étant donné que mon compte est en cours de suppression
|
||||
Quand un autre utilisateur recherche mes contenus
|
||||
Alors mes contenus ne sont plus diffusés dans l'application
|
||||
Et mes contenus n'apparaissent plus dans les recherches
|
||||
Et mes contenus ne sont plus recommandés
|
||||
Mais mes contenus ne sont pas encore supprimés définitivement
|
||||
|
||||
# Email de confirmation avec lien d'annulation
|
||||
|
||||
Scénario: Email de confirmation envoyé immédiatement
|
||||
Étant donné que j'ai confirmé la suppression de mon compte
|
||||
Quand la demande est traitée
|
||||
Alors je reçois un email avec le sujet "Confirmation de suppression de votre compte RoadWave"
|
||||
Et l'email contient:
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Votre compte RoadWave est en cours de suppression. Vos données seront définitivement supprimées le [date + 30j].
|
||||
|
||||
Si vous changez d'avis, vous pouvez annuler la suppression en cliquant sur le lien ci-dessous (valide 30 jours):
|
||||
|
||||
[Annuler la suppression]
|
||||
|
||||
Que se passe-t-il ensuite?
|
||||
- Votre compte est désactivé immédiatement
|
||||
- Vos contenus sont masqués de l'application
|
||||
- Vos données seront supprimées dans 30 jours
|
||||
- Vous pouvez annuler à tout moment pendant ces 30 jours
|
||||
|
||||
Pour toute question: dpo@roadwave.fr
|
||||
"""
|
||||
Et le lien d'annulation est valide 30 jours
|
||||
|
||||
# Grace period de 30 jours
|
||||
|
||||
Scénario: Annulation de la suppression dans les 30 jours
|
||||
Étant donné que j'ai demandé la suppression de mon compte le 2025-01-20
|
||||
Et que je reçois l'email de confirmation
|
||||
Quand je clique sur le lien "Annuler la suppression" le 2025-02-05 (16 jours plus tard)
|
||||
Alors mon compte est réactivé immédiatement
|
||||
Et je peux me reconnecter normalement
|
||||
Et mes contenus redeviennent visibles dans l'application
|
||||
Et toutes mes données sont restaurées
|
||||
Et je reçois un email de confirmation: "Votre compte a été réactivé"
|
||||
|
||||
Scénario: Lien d'annulation expire après 30 jours
|
||||
Étant donné que j'ai demandé la suppression de mon compte le 2025-01-20
|
||||
Quand j'essaie de cliquer sur le lien d'annulation le 2025-02-25 (36 jours plus tard)
|
||||
Alors le lien est expiré
|
||||
Et je reçois un message "Ce lien a expiré. Votre compte a été définitivement supprimé."
|
||||
Et la suppression effective a déjà eu lieu
|
||||
|
||||
# Suppression effective après 30 jours
|
||||
|
||||
Scénario: Suppression effective sans annulation
|
||||
Étant donné que j'ai demandé la suppression de mon compte le 2025-01-20
|
||||
Et que je n'ai pas cliqué sur le lien d'annulation
|
||||
Quand la date atteint le 2025-02-19 (30 jours plus tard)
|
||||
Alors un job automatique exécute la suppression définitive
|
||||
Et toutes mes données personnelles sont supprimées
|
||||
|
||||
# Données supprimées
|
||||
|
||||
Scénario: Liste des données supprimées définitivement
|
||||
Étant donné que la suppression effective est exécutée
|
||||
Quand le job de suppression se termine
|
||||
Alors les données suivantes sont supprimées:
|
||||
| données | supprimé |
|
||||
| Compte utilisateur (email, mdp) | oui |
|
||||
| Profil (pseudo, bio, avatar) | oui |
|
||||
| Historique d'écoute | oui |
|
||||
| Historique GPS | oui |
|
||||
| Centres d'intérêt (jauges) | oui |
|
||||
| Sessions et tokens | oui |
|
||||
| Likes et abonnements | oui |
|
||||
| Notifications non lues | oui |
|
||||
| Historique consentements | oui |
|
||||
| Données de paiement | oui |
|
||||
Et ces suppressions sont irréversibles
|
||||
|
||||
# Contenus conservés anonymement
|
||||
|
||||
Scénario: Anonymisation des contenus créés
|
||||
Étant donné que j'ai créé 10 contenus audio
|
||||
Quand la suppression effective est exécutée
|
||||
Alors mes contenus audio restent disponibles dans l'application
|
||||
Et le nom du créateur devient "Utilisateur supprimé"
|
||||
Et mon pseudo n'est plus visible
|
||||
Et les métadonnées (titre, description, tags, géolocalisation) sont conservées
|
||||
Et les fichiers audio restent sur le CDN
|
||||
Et les statistiques d'écoute sont conservées
|
||||
|
||||
Scénario: Justification de l'anonymisation (intérêt légitime)
|
||||
Étant donné que mes contenus sont conservés anonymement
|
||||
Quand un auditeur RGPD vérifie la conformité
|
||||
Alors la conservation est justifiée par l'intérêt légitime de la communauté
|
||||
Et les contenus ne contiennent plus de données personnelles identifiables
|
||||
Et la suppression complète nuirait à l'expérience des autres utilisateurs
|
||||
Et cette pratique est conforme au RGPD si anonymisation réelle
|
||||
|
||||
Scénario: Contenu anonymisé visible pour les autres utilisateurs
|
||||
Étant donné que mon compte a été supprimé
|
||||
Et que mes contenus ont été anonymisés
|
||||
Quand un utilisateur consulte un de mes anciens contenus
|
||||
Alors le créateur affiché est "Utilisateur supprimé"
|
||||
Et le profil du créateur n'est plus accessible
|
||||
Et le contenu reste écoutable normalement
|
||||
Et les likes et statistiques sont conservés
|
||||
|
||||
# Likes et abonnements supprimés
|
||||
|
||||
Scénario: Suppression de mes likes avec conservation des compteurs
|
||||
Étant donné que j'avais liké 50 contenus
|
||||
Quand la suppression effective est exécutée
|
||||
Alors mes likes sont supprimés de la base de données
|
||||
Mais les compteurs de likes sur les contenus sont préservés
|
||||
Et les créateurs ne perdent pas leurs statistiques
|
||||
Et seule la relation "user X a liké content Y" est supprimée
|
||||
|
||||
Scénario: Suppression de mes abonnements
|
||||
Étant donné que je suivais 20 créateurs
|
||||
Quand la suppression effective est exécutée
|
||||
Alors mes abonnements sont supprimés
|
||||
Et les compteurs d'abonnés des créateurs sont décrémentés de 1
|
||||
Et les créateurs ne reçoivent pas de notification de désabonnement
|
||||
|
||||
# Sessions et tokens révoqués
|
||||
|
||||
Scénario: Révocation de tous les tokens immédiatement
|
||||
Étant donné que je suis connecté sur 3 appareils (mobile, tablette, web)
|
||||
Quand je demande la suppression de mon compte
|
||||
Alors tous mes tokens d'authentification sont révoqués immédiatement
|
||||
Et je suis déconnecté de tous mes appareils
|
||||
Et toute tentative de reconnexion échoue
|
||||
|
||||
# Notifications avant suppression
|
||||
|
||||
Scénario: Rappels par email pendant le grace period
|
||||
Étant donné que j'ai demandé la suppression de mon compte le 2025-01-20
|
||||
Quand le grace period s'écoule
|
||||
Alors je reçois des emails de rappel:
|
||||
| date | jours restants | sujet email |
|
||||
| 2025-02-04 | 15 jours | Plus que 15 jours pour annuler la suppression |
|
||||
| 2025-02-12 | 7 jours | Dernière semaine pour annuler la suppression |
|
||||
| 2025-02-17 | 2 jours | Attention: suppression définitive dans 2 jours |
|
||||
Et chaque email contient le lien d'annulation
|
||||
|
||||
# Conformité RGPD Article 17
|
||||
|
||||
Scénario: Conformité droit à l'effacement
|
||||
Étant donné que la suppression de mon compte est complète
|
||||
Quand un auditeur RGPD vérifie la conformité
|
||||
Alors le processus respecte l'article 17 du RGPD:
|
||||
| exigence RGPD | respecté |
|
||||
| Suppression de toutes les données personnelles | oui |
|
||||
| Délai raisonnable (30j grace period acceptable)| oui |
|
||||
| Possibilité d'annulation (bonne pratique) | oui |
|
||||
| Anonymisation des contenus (intérêt légitime) | oui |
|
||||
| Révocation des tokens et sessions | oui |
|
||||
| Suppression irréversible | oui |
|
||||
|
||||
# Cas particuliers
|
||||
|
||||
Scénario: Suppression d'un compte Premium
|
||||
Étant donné que j'ai un abonnement Premium actif
|
||||
Quand je demande la suppression de mon compte
|
||||
Alors mon abonnement est annulé immédiatement
|
||||
Et aucun remboursement n'est effectué (conformément aux CGV)
|
||||
Et je reçois un email de confirmation d'annulation de l'abonnement
|
||||
Et le reste du processus de suppression se déroule normalement
|
||||
|
||||
Scénario: Suppression d'un compte créateur avec revenus en attente
|
||||
Étant donné que je suis un créateur avec 75€ de revenus en attente de paiement
|
||||
Quand je demande la suppression de mon compte
|
||||
Alors un message m'informe:
|
||||
"""
|
||||
Vous avez 75€ de revenus en attente. Souhaitez-vous recevoir ce paiement avant suppression?
|
||||
"""
|
||||
Et je peux choisir "Recevoir le paiement et attendre" ou "Renoncer au paiement"
|
||||
Et si je choisis "Recevoir le paiement", la suppression est repoussée de 7 jours
|
||||
|
||||
Scénario: Suppression avec signalements de modération en cours
|
||||
Étant donné que j'ai 2 signalements en cours de traitement
|
||||
Quand je demande la suppression de mon compte
|
||||
Alors les signalements sont automatiquement clôturés
|
||||
Et les contenus signalés sont masqués immédiatement
|
||||
Et aucune sanction n'est appliquée (compte déjà en suppression)
|
||||
|
||||
# Traçabilité pour audit
|
||||
|
||||
Scénario: Log de la suppression pour traçabilité
|
||||
Étant donné que la suppression effective est exécutée
|
||||
Quand le job de suppression se termine
|
||||
Alors un log est créé avec:
|
||||
| champ | valeur |
|
||||
| user_id | [ID anonymisé] |
|
||||
| deletion_requested_at | 2025-01-20T10:00:00Z |
|
||||
| deletion_executed_at | 2025-02-19T02:00:00Z |
|
||||
| deletion_cancelled | false |
|
||||
| data_deleted | [liste des tables] |
|
||||
| contents_anonymized | 10 |
|
||||
Et ce log est conservé 5 ans pour audit RGPD
|
||||
Et l'user_id est pseudonymisé pour anonymat
|
||||
1003
docs/domains/_shared/rules/ANNEXE-POST-MVP.md
Normal file
1003
docs/domains/_shared/rules/ANNEXE-POST-MVP.md
Normal file
File diff suppressed because it is too large
Load Diff
240
docs/domains/_shared/rules/authentification.md
Normal file
240
docs/domains/_shared/rules/authentification.md
Normal file
@@ -0,0 +1,240 @@
|
||||
## 1. Authentification & Inscription
|
||||
|
||||
### 1.1 Méthodes d'inscription
|
||||
|
||||
**Décision** : Email/Password uniquement (pas d'OAuth tiers)
|
||||
|
||||
- ❌ **Pas de Google, Apple, Facebook OAuth** (dépendance services US/Chine)
|
||||
- ✅ **Email + mot de passe** (formulaire natif Zitadel)
|
||||
- ✅ 2FA (Two-Factor Authentication) disponible
|
||||
- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours)
|
||||
|
||||
**Clarification technique** :
|
||||
- Zitadel utilise OAuth2/OIDC comme **protocole** (standard moderne pour mobile)
|
||||
- Mais l'authentification reste 100% **email/password natif**
|
||||
- **Aucun fournisseur externe** (Google, Apple, etc.) n'est intégré
|
||||
|
||||
**Justification** :
|
||||
- Souveraineté : pas de dépendance externe
|
||||
- RGPD : données 100% contrôlées
|
||||
- Coût : 0€ (Zitadel intégré)
|
||||
|
||||
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Vérification email
|
||||
|
||||
**Décision** : Différenciée selon le rôle utilisateur
|
||||
|
||||
#### Pour les auditeurs (écoute uniquement)
|
||||
|
||||
| État | Capacités |
|
||||
|------|-----------|
|
||||
| **Email non vérifié** | Lecture illimitée + création max 5 contenus |
|
||||
| **Email vérifié** | Toutes fonctionnalités débloquées |
|
||||
|
||||
**Paramètres** :
|
||||
- Lien de vérification expire après **7 jours**
|
||||
- Possibilité de renvoyer le lien (max 3 fois/jour)
|
||||
- Rappel in-app après création du 3ème contenu
|
||||
|
||||
**Justification** :
|
||||
- Friction minimale à l'inscription
|
||||
- Anti-spam sans bloquer l'essai du produit
|
||||
- Incitation naturelle à vérifier (déblocage)
|
||||
|
||||
#### Pour les créateurs (monétisation)
|
||||
|
||||
**Vérification obligatoire sous 7 jours** pour :
|
||||
- Accès au programme de monétisation
|
||||
- KYC et reversement des revenus (conformité Mangopay)
|
||||
- Publication illimitée de contenus
|
||||
|
||||
**Justification** :
|
||||
- **Conformité légale** : KYC obligatoire pour transferts financiers
|
||||
- **Anti-fraude** : Vérification identité réelle pour paiements
|
||||
- **Responsabilité** : RoadWave doit pouvoir prouver identité créateurs monétisés
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Données requises à l'inscription
|
||||
|
||||
**Obligatoires** :
|
||||
- ✅ Email (format validé)
|
||||
- ✅ Mot de passe (voir règles ci-dessous)
|
||||
- ✅ Pseudo (3-30 caractères, alphanumérique + underscore)
|
||||
- ✅ Date de naissance (vérification âge minimum)
|
||||
|
||||
**Optionnelles** :
|
||||
- ❌ Nom complet (privacy by design)
|
||||
- ❌ Photo de profil (avatar par défaut généré)
|
||||
- ❌ Bio (ajout ultérieur)
|
||||
|
||||
**Âge minimum** :
|
||||
- **13 ans minimum** (conformité réglementation réseaux sociaux EU)
|
||||
- Vérification à l'inscription via date de naissance
|
||||
- Blocage inscription si <13 ans avec message explicite
|
||||
|
||||
**Justification** :
|
||||
- RGPD minimal data
|
||||
- Friction réduite (4 champs max)
|
||||
- Protection mineurs (obligation légale)
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Tranches d'âge des contenus
|
||||
|
||||
**Décision** : Classification obligatoire des contenus
|
||||
|
||||
**Catégories** :
|
||||
- 🟢 **Tout public** (défaut)
|
||||
- 🟡 **13+** : contenu mature léger (débats, actualité sensible)
|
||||
- 🟠 **16+** : contenu mature (violence verbale, sujets sensibles)
|
||||
- 🔴 **18+** : contenu adulte (langage explicite, sujets réservés)
|
||||
|
||||
**Règles de diffusion** :
|
||||
- Utilisateur 13-15 ans → contenus 🟢 🟡 (Tout public + 13+)
|
||||
- Utilisateur 16-17 ans → contenus 🟢 🟡 🟠 (Tout public + 13+ + 16+)
|
||||
- Utilisateur 18+ → tous contenus 🟢 🟡 🟠 🔴
|
||||
|
||||
**Modération** :
|
||||
- Vérification obligatoire de la classification lors de la validation
|
||||
- Reclassification possible par modérateurs
|
||||
- Strike si classification volontairement incorrecte
|
||||
|
||||
**Justification** :
|
||||
- Protection mineurs (obligation légale)
|
||||
- Responsabilité plateforme
|
||||
- Coût : champ supplémentaire + règle algo
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Validation mot de passe
|
||||
|
||||
**Règles** :
|
||||
- ✅ Minimum **8 caractères**
|
||||
- ✅ Au moins **1 majuscule**
|
||||
- ✅ Au moins **1 chiffre**
|
||||
- ❌ Pas de symbole obligatoire (simplicité)
|
||||
|
||||
**Validation** :
|
||||
- Côté client (feedback temps réel)
|
||||
- Côté backend (sécurité)
|
||||
- Message d'erreur explicite par règle non respectée
|
||||
|
||||
**Justification** :
|
||||
- Standard industrie
|
||||
- Bloque 95% des mots de passe faibles
|
||||
- UX acceptable (pas trop restrictif)
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Two-Factor Authentication (2FA)
|
||||
|
||||
**Décision** : Optionnel mais recommandé
|
||||
|
||||
**Méthodes disponibles** :
|
||||
- ✅ TOTP (Time-based One-Time Password) via app (Google Authenticator, Authy)
|
||||
- ✅ Email (code 6 chiffres, expire 10 min)
|
||||
- ❌ SMS (coût élevé ~0.05€/SMS)
|
||||
|
||||
**Appareil de confiance** :
|
||||
- Option "Ne plus demander sur cet appareil" → bypass 2FA pendant **30 jours**
|
||||
- Révocable depuis paramètres compte
|
||||
- Liste des appareils de confiance visible
|
||||
|
||||
**Justification** :
|
||||
- Sécurité renforcée sans coût SMS
|
||||
- UX : appareil de confiance évite friction quotidienne
|
||||
- Zitadel natif (0€)
|
||||
|
||||
---
|
||||
|
||||
### 1.7 Tentatives de connexion
|
||||
|
||||
**Règles** :
|
||||
- Maximum **5 tentatives** par période de **15 minutes**
|
||||
- Blocage temporaire après 5 échecs
|
||||
- Compteur reset automatique après 15 min
|
||||
- Notification email si blocage (tentative suspecte)
|
||||
|
||||
**Déblocage** :
|
||||
- Automatique après 15 min
|
||||
- Ou via lien "Mot de passe oublié"
|
||||
|
||||
**Justification** :
|
||||
- Anti brute-force
|
||||
- Standard industrie (équilibre sécurité/UX)
|
||||
- Zitadel natif (0€)
|
||||
|
||||
---
|
||||
|
||||
### 1.8 Sessions et refresh tokens
|
||||
|
||||
**Durée de vie** :
|
||||
- **Access token** : 15 minutes
|
||||
- **Refresh token** : 30 jours
|
||||
|
||||
**Rotation** :
|
||||
- Refresh token rotatif (nouveau token à chaque refresh)
|
||||
- Ancien token invalidé immédiatement
|
||||
- Détection token replay attack
|
||||
|
||||
**Extension automatique** :
|
||||
- Si app utilisée, session prolongée automatiquement
|
||||
- Inactivité 30 jours → déconnexion
|
||||
|
||||
**Justification** :
|
||||
- Sécurité (token court-vie)
|
||||
- UX (pas de reconnexion fréquente)
|
||||
- Standard OAuth2/OIDC
|
||||
|
||||
---
|
||||
|
||||
### 1.9 Multi-device
|
||||
|
||||
**Décision** : Sessions simultanées illimitées
|
||||
|
||||
**Gestion** :
|
||||
- Liste des devices connectés visible (OS, navigateur, dernière connexion, IP/ville)
|
||||
- Révocation individuelle possible
|
||||
- Révocation globale "Déconnecter tous les appareils"
|
||||
|
||||
**Alertes** :
|
||||
- Notification push + email si connexion depuis nouveau device
|
||||
- Détection localisation suspecte (IP pays différent)
|
||||
|
||||
**Justification** :
|
||||
- UX maximale (écoute voiture + tablette maison + web)
|
||||
- Sécurité via transparence (utilisateur voit tout)
|
||||
- Coût : table sessions PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
### 1.10 Récupération de compte
|
||||
|
||||
**Méthode** : Email uniquement
|
||||
|
||||
**Processus** :
|
||||
1. Utilisateur clique "Mot de passe oublié"
|
||||
2. Email avec lien de reset envoyé
|
||||
3. Lien expire après **1 heure**
|
||||
4. Page de reset : nouveau mot de passe (validation règles)
|
||||
5. Confirmation + déconnexion tous devices (sauf celui en cours)
|
||||
|
||||
**Notifications** :
|
||||
- Email immédiat si changement mot de passe
|
||||
- Push si changement depuis appareil non reconnu
|
||||
|
||||
**Limite** :
|
||||
- Maximum **3 demandes/heure** (anti-spam)
|
||||
|
||||
**Justification** :
|
||||
- Standard sécurité
|
||||
- Pas de coût SMS
|
||||
- Protection contre attaque sociale
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 1
|
||||
135
docs/domains/_shared/rules/gestion-erreurs.md
Normal file
135
docs/domains/_shared/rules/gestion-erreurs.md
Normal file
@@ -0,0 +1,135 @@
|
||||
## 12. Gestion des erreurs
|
||||
|
||||
### 12.1 Aucun contenu disponible
|
||||
|
||||
**Stratégie** : Élargissement automatique progressif
|
||||
|
||||
**Flow** :
|
||||
|
||||
```
|
||||
1. Recherche rayon 50 km → aucun résultat
|
||||
2. Élargissement auto 100 km
|
||||
3. Si toujours rien → département
|
||||
4. Si toujours rien → région
|
||||
5. Dernier recours → contenu national (toujours disponible)
|
||||
```
|
||||
|
||||
**Messages adaptatifs** :
|
||||
|
||||
| Cas | Message |
|
||||
|-----|---------|
|
||||
| **Trouvé à 100 km** | "Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km)" |
|
||||
| **Trouvé département** | "Aucun contenu local disponible. Voici du contenu dans votre département" |
|
||||
| **Contenu national** | "Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser" |
|
||||
|
||||
**Justification** :
|
||||
- **UX fluide** : pas de message d'erreur bloquant "Aucun contenu"
|
||||
- **User ne reste jamais sans contenu**
|
||||
- **Contenu national = filet de sécurité** : actualités Le Monde, podcasts génériques
|
||||
|
||||
---
|
||||
|
||||
### 12.2 Contenu signalé/supprimé pendant l'écoute
|
||||
|
||||
**Décision** : Pas d'interruption brutale
|
||||
|
||||
**Flow** :
|
||||
|
||||
```
|
||||
1. Contenu supprimé côté backend (modération)
|
||||
2. Si contenu en écoute → laisser terminer lecture en cours
|
||||
3. Après fin lecture → désactiver bouton "Précédent" pour ce contenu
|
||||
4. Passage automatique suivant après 2s
|
||||
5. Toast notification discrète : "Contenu précédent retiré (violation règles)"
|
||||
```
|
||||
|
||||
**Si tentative "Précédent" manuellement** :
|
||||
- Message : "Ce contenu n'est plus disponible"
|
||||
- Retour au contenu actuel
|
||||
|
||||
**Justification** :
|
||||
- **Sécurité routière** : pas d'interruption brutale pendant conduite
|
||||
- **User informé mais pas alarmé** : message discret
|
||||
- **Empêche réécoute** : contenu modéré inaccessible
|
||||
|
||||
---
|
||||
|
||||
### 12.3 Perte de réseau
|
||||
|
||||
**Buffer adaptatif** (cf. TECHNICAL.md) :
|
||||
|
||||
| Réseau | Buffer min | Buffer cible | Buffer max |
|
||||
|--------|------------|--------------|------------|
|
||||
| **WiFi** | 5s | 30s | 120s |
|
||||
| **4G/5G** | 10s | 45s | 120s |
|
||||
| **3G** | 30s | 90s | 300s |
|
||||
|
||||
**Comportement détaillé** :
|
||||
|
||||
**Phase 1 : Connexion instable** (latence élevée, paquets perdus)
|
||||
- Aucun message immédiat
|
||||
- Lecture continue sur buffer
|
||||
- Si > 10s latence : toast discret "Connexion instable"
|
||||
|
||||
**Phase 2 : Perte totale réseau**
|
||||
- Lecture continue jusqu'à épuisement buffer
|
||||
- Toast : "Hors ligne, lecture sur buffer (30s restantes)"
|
||||
- Compte à rebours visible
|
||||
|
||||
**Phase 3 : Buffer épuisé sans reconnexion**
|
||||
- Pause automatique
|
||||
- Overlay : "Connexion perdue. Reconnexion en cours..."
|
||||
- Retry automatique toutes les 5s (max 6 tentatives = 30s)
|
||||
|
||||
**Phase 4 : Basculement mode offline** (après 30s échec)
|
||||
- Popup : "Voulez-vous continuer avec vos contenus téléchargés ?"
|
||||
- Boutons : "Réessayer" / "Mode offline"
|
||||
- Si "Mode offline" → lecture contenus téléchargés
|
||||
|
||||
**Reconnexion réussie** :
|
||||
- Reprise automatique lecture au point d'arrêt exact
|
||||
- Toast : "Connexion rétablie"
|
||||
|
||||
**Justification** :
|
||||
- **Expérience fluide zones blanches** (tunnels, campagne)
|
||||
- **Buffer généreux** : absorbe fluctuations réseau mobile
|
||||
- **Mode offline secours** : si coupure prolongée
|
||||
|
||||
---
|
||||
|
||||
### 12.4 Géolocalisation désactivée
|
||||
|
||||
**Mode dégradé automatique**
|
||||
|
||||
**Contenu disponible** :
|
||||
|
||||
| Type contenu | Disponible |
|
||||
|--------------|-----------|
|
||||
| **Contenu national** (podcasts, actualités) | ✅ |
|
||||
| **Contenu téléchargé** (offline) | ✅ |
|
||||
| **Contenus "Neutre"** géographiquement | ✅ |
|
||||
| **Contenu géolocalisé** (Ancré/Contextuel) | ❌ |
|
||||
| **Audio-guides** | ❌ |
|
||||
| **Notifications push géo-déclenchées** | ❌ |
|
||||
|
||||
**Popup au lancement** :
|
||||
- **Apparition** : Premier lancement après refus géolocalisation
|
||||
- **Message** : "RoadWave fonctionne mieux avec la géolocalisation activée. Sans elle, seul le contenu national sera disponible."
|
||||
- **Boutons** :
|
||||
- "Activer" → Redirection paramètres OS
|
||||
- "Continuer sans" → Mode dégradé
|
||||
- **Checkbox** : "Ne plus me demander"
|
||||
|
||||
**Banner permanent si refus** :
|
||||
- Bandeau haut écran : "Mode limité : géolocalisation désactivée. [Activer]"
|
||||
- Pas intrusif mais rappel constant
|
||||
- Disparaît si géolocalisation réactivée
|
||||
|
||||
**Justification** :
|
||||
- **App reste fonctionnelle** sans GPS (pas de blocage)
|
||||
- **Incitation forte** à activer (meilleure UX)
|
||||
- **Respecte choix user** (RGPD : consentement libre)
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 12
|
||||
336
docs/domains/_shared/rules/rgpd.md
Normal file
336
docs/domains/_shared/rules/rgpd.md
Normal file
@@ -0,0 +1,336 @@
|
||||
## 13. Conformité RGPD
|
||||
|
||||
### 13.1 Gestion du consentement
|
||||
|
||||
**Décision** : Tarteaucitron.js + PostgreSQL backend
|
||||
|
||||
**Implémentation web** :
|
||||
- ✅ Tarteaucitron.js (opensource, self-hosted)
|
||||
- ✅ Banner RGPD français, customisable
|
||||
- ✅ Granularité : fonctionnel / analytique / marketing
|
||||
|
||||
**Implémentation backend** :
|
||||
- Table `user_consents` avec versioning
|
||||
- Champs : user_id, consent_type, version, accepted, timestamp
|
||||
- Historique complet conservé (preuve légale)
|
||||
|
||||
**Consentements requis** :
|
||||
- **Géolocalisation précise** : obligatoire (banner + permission OS)
|
||||
- **Analytics** : optionnel (Matomo)
|
||||
- **Notifications push** : optionnel (permission OS)
|
||||
|
||||
**Justification** :
|
||||
- Opensource, 0€, conformité RGPD garantie
|
||||
- Historique backend = preuve légale en cas de contrôle
|
||||
- Granularité conforme recommandations CNIL
|
||||
|
||||
---
|
||||
|
||||
### 13.2 Anonymisation des données GPS
|
||||
|
||||
**Décision** : Geohash après 24h
|
||||
|
||||
**Processus** :
|
||||
1. Données précises conservées **24h** (recommandation personnalisée)
|
||||
2. Après 24h : conversion en geohash précision 5 (~5km²)
|
||||
3. Coordonnées originales supprimées définitivement
|
||||
|
||||
**Implémentation PostGIS** :
|
||||
```sql
|
||||
-- Job quotidien
|
||||
UPDATE location_history
|
||||
SET location = ST_SetSRID(ST_GeomFromGeoHash(ST_GeoHash(location::geography, 5)), 4326)::geography,
|
||||
anonymized = true
|
||||
WHERE created_at < NOW() - INTERVAL '24 hours' AND anonymized = false;
|
||||
```
|
||||
|
||||
**Exceptions** :
|
||||
- ✅ Historique personnel visible (liste trajets) : conservation intégrale tant que compte actif
|
||||
- ❌ Analytics globales : uniquement geohash anonyme
|
||||
|
||||
**Justification** :
|
||||
- Vraie anonymisation RGPD (CNIL compliant)
|
||||
- Permet analytics agrégées (heatmaps trafic)
|
||||
- PostGIS natif, 0€
|
||||
|
||||
---
|
||||
|
||||
### 13.3 Export des données (portabilité)
|
||||
|
||||
**Décision** : JSON + HTML + ZIP, génération asynchrone
|
||||
|
||||
**Contenu de l'export** :
|
||||
```
|
||||
export-roadwave-[user_id]-[date].zip
|
||||
├── export.json # Machine-readable
|
||||
├── index.html # Human-readable (stylé)
|
||||
├── audio/
|
||||
│ ├── content-123.opus
|
||||
│ ├── content-456.opus
|
||||
│ └── ...
|
||||
└── README.txt # Instructions
|
||||
```
|
||||
|
||||
**Données exportées** :
|
||||
- Profil utilisateur (email, pseudo, date inscription, bio)
|
||||
- Historique d'écoute (titres, dates, durées)
|
||||
- Contenus créés (audio + métadonnées)
|
||||
- Abonnements et likes
|
||||
- Centres d'intérêt (jauges)
|
||||
- Historique consentements
|
||||
|
||||
**Processus** :
|
||||
1. Demande via paramètres compte
|
||||
2. Génération asynchrone (worker background)
|
||||
3. Email avec lien download (expire **7 jours**)
|
||||
4. Délai : **48h maximum** (conformité RGPD)
|
||||
|
||||
**Limite** :
|
||||
- Maximum **1 export/mois** (anti-abus)
|
||||
|
||||
**Justification** :
|
||||
- Conformité article 20 RGPD (portabilité)
|
||||
- Double format (human + machine)
|
||||
- Worker asynchrone évite timeout
|
||||
|
||||
---
|
||||
|
||||
### 13.4 Suppression du compte
|
||||
|
||||
**Décision** : Grace period 30j + anonymisation contenus
|
||||
|
||||
**Processus** :
|
||||
1. Utilisateur clique "Supprimer mon compte"
|
||||
2. Compte désactivé immédiatement (login impossible)
|
||||
3. Contenus cachés pendant 30 jours (non diffusés)
|
||||
4. Email confirmation + lien annulation (valide 30j)
|
||||
5. Après 30j sans annulation : suppression effective
|
||||
|
||||
**Suppression effective** :
|
||||
- ✅ Compte utilisateur supprimé (données personnelles)
|
||||
- ✅ Historique d'écoute supprimé
|
||||
- ✅ GPS historique supprimé
|
||||
- ✅ Sessions et tokens révoqués
|
||||
- ⚠️ Contenus créés **anonymisés** (créateur = "Utilisateur supprimé")
|
||||
- ⚠️ Likes et abonnements supprimés (mais compteurs préservés)
|
||||
|
||||
**Contenus conservés anonymement** :
|
||||
- Audio files (CDN)
|
||||
- Métadonnées (titre, description, tags, géolocalisation)
|
||||
- Statistiques d'écoute
|
||||
|
||||
**Justification** :
|
||||
- Grace period évite suppressions impulsives
|
||||
- Anonymisation contenus = intérêt légitime communauté
|
||||
- Conforme RGPD si créateur = donnée supprimée
|
||||
|
||||
---
|
||||
|
||||
### 13.5 Mode dégradé (sans GPS précis)
|
||||
|
||||
**Décision** : GeoIP par défaut, GPS optionnel
|
||||
|
||||
**Niveaux de précision** :
|
||||
|
||||
| Niveau | Technologie | Contenus accessibles | Consentement |
|
||||
|--------|-------------|---------------------|--------------|
|
||||
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
|
||||
| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
|
||||
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
|
||||
|
||||
**Implémentation** :
|
||||
- Démarrage app : GeoIP automatique (IP → ville)
|
||||
- Banner in-app : "Activez la géolocalisation pour découvrir du contenu près de chez vous"
|
||||
- Upgrade volontaire vers GPS
|
||||
|
||||
**API GeoIP** :
|
||||
- IP2Location Lite (gratuit, self-hosted, voir [ADR-019](../adr/019-geolocalisation-ip.md))
|
||||
- Update DB mensuelle automatique
|
||||
- Précision ~80% au niveau ville
|
||||
|
||||
**Justification** :
|
||||
- RGPD : pas de consentement requis pour GeoIP (pas de donnée personnelle)
|
||||
- UX dégradée acceptable (contenus disponibles)
|
||||
- Progressive disclosure (upgrade optionnel)
|
||||
|
||||
---
|
||||
|
||||
### 13.6 Durée de conservation des données
|
||||
|
||||
**Décision** : 5 ans inactivité → purge automatique
|
||||
|
||||
**Règles** :
|
||||
|
||||
| Type de compte | Seuil inactivité | Action |
|
||||
|----------------|------------------|--------|
|
||||
| **Auditeur uniquement** | 5 ans sans connexion | Suppression automatique |
|
||||
| **Créateur avec contenus actifs** | Jamais (tant qu'écoutes) | Conservation indéfinie |
|
||||
| **Créateur inactif** | 5 ans sans connexion + 2 ans sans écoute | Suppression automatique |
|
||||
|
||||
**Notifications avant suppression** :
|
||||
- Email + push : **90 jours** avant
|
||||
- Email + push : **30 jours** avant
|
||||
- Email + push : **7 jours** avant
|
||||
- Toute connexion = reset compteur inactivité
|
||||
|
||||
**Contenu conservé** :
|
||||
- Contenus créés par comptes supprimés (anonymisés) : conservation indéfinie
|
||||
|
||||
**Justification** :
|
||||
- Conformité principe minimisation RGPD
|
||||
- 5 ans = équilibre raisonnable (standard industrie)
|
||||
- Exception créateurs actifs = intérêt légitime plateforme
|
||||
|
||||
---
|
||||
|
||||
### 13.7 Cookies et trackers web
|
||||
|
||||
**Décision** : Matomo self-hosted, zéro cookie tiers
|
||||
|
||||
**Cookies utilisés** :
|
||||
|
||||
| Cookie | Type | Durée | Finalité | Consentement |
|
||||
|--------|------|-------|----------|--------------|
|
||||
| `session` | Technique | 30j | Authentification | ❌ Non requis |
|
||||
| `refresh_token` | Technique | 30j | Session persistante | ❌ Non requis |
|
||||
| `_pk_id` | Analytique | 13 mois | Matomo (IP anonyme) | ✅ Requis |
|
||||
|
||||
**Analytics : Matomo self-hosted** :
|
||||
- Hébergé sur nos serveurs (Docker)
|
||||
- IP anonymisées automatiquement (2 derniers octets)
|
||||
- Pas de cookie si consentement refusé
|
||||
- Alternative : Plausible (SaaS EU, 9€/mois)
|
||||
|
||||
**Trackers interdits** :
|
||||
- ❌ Google Analytics
|
||||
- ❌ Facebook Pixel
|
||||
- ❌ Hotjar, Mixpanel, etc.
|
||||
|
||||
**Justification** :
|
||||
- Souveraineté données (pas de transfert US)
|
||||
- Conformité RGPD max (CNIL compatible)
|
||||
- Matomo = opensource, 0€ infra
|
||||
|
||||
---
|
||||
|
||||
### 13.8 Registre des traitements
|
||||
|
||||
**Décision** : Document Markdown versionné Git (MVP)
|
||||
|
||||
**Emplacement** :
|
||||
- `docs/rgpd/registre-traitements.md`
|
||||
- Versionné Git (historique modifications)
|
||||
|
||||
**Contenu obligatoire par traitement** :
|
||||
- Nom et finalité du traitement
|
||||
- Catégories de données collectées
|
||||
- Base légale (consentement / contrat / intérêt légitime)
|
||||
- Durée de conservation
|
||||
- Destinataires (sous-traitants, CDN, etc.)
|
||||
- Transferts hors UE (aucun prévu)
|
||||
|
||||
**Responsable** :
|
||||
- DPO / Fondateur
|
||||
- Review trimestrielle obligatoire
|
||||
- Update immédiate si nouveau traitement
|
||||
|
||||
**Migration future** :
|
||||
- Si > 100K utilisateurs : interface admin PostgreSQL
|
||||
|
||||
**Justification** :
|
||||
- Obligation RGPD Article 30
|
||||
- Markdown = simple, versionné, auditable
|
||||
- 0€
|
||||
|
||||
---
|
||||
|
||||
### 13.9 Notification violations de données (breach)
|
||||
|
||||
**Décision** : Monitoring + alertes + runbook
|
||||
|
||||
**Détection automatique** :
|
||||
|
||||
| Événement | Outil | Alerte |
|
||||
|-----------|-------|--------|
|
||||
| Erreurs backend critiques | Sentry | Discord/Slack immédiat |
|
||||
| Pic requêtes anormal | Grafana | Email équipe |
|
||||
| Accès non autorisé DB | PostgreSQL logs | SMS fondateur |
|
||||
| Authentification suspecte | Zitadel alerts | Email équipe |
|
||||
|
||||
**Procédure breach** :
|
||||
- Runbook : `docs/rgpd/procedure-breach.md`
|
||||
- Checklist 72h CNIL :
|
||||
1. H+0 : Détection et confinement
|
||||
2. H+24 : Évaluation gravité (données concernées, utilisateurs impactés)
|
||||
3. H+48 : Notification CNIL si risque pour utilisateurs
|
||||
4. H+72 : Notification utilisateurs si risque élevé
|
||||
|
||||
**Contact CNIL** :
|
||||
- Email pré-rédigé (template)
|
||||
- Formulaire en ligne (account CNIL créé)
|
||||
|
||||
**Justification** :
|
||||
- Obligation RGPD Article 33 (notification 72h)
|
||||
- Monitoring proactif évite découverte tardive
|
||||
- Sentry gratuit < 5K events/mois
|
||||
|
||||
---
|
||||
|
||||
### 13.10 DPO (Délégué à la Protection des Données)
|
||||
|
||||
**Décision** : Fondateur = DPO temporaire (MVP)
|
||||
|
||||
**Raison légale** :
|
||||
- Non obligatoire si :
|
||||
- < 250 employés
|
||||
- Pas de traitement à grande échelle de données sensibles
|
||||
- RoadWave : données localisation = sensible MAIS échelle MVP
|
||||
|
||||
**Formation** :
|
||||
- CNIL : formation gratuite en ligne (4h)
|
||||
- Certification CNIL "Atelier RGPD" (gratuit)
|
||||
|
||||
**Contact** :
|
||||
- Email : dpo@roadwave.fr
|
||||
- Publié dans CGU et mentions légales
|
||||
- Délai réponse : **1 mois** (RGPD)
|
||||
|
||||
**Migration future** :
|
||||
- Si > 100K utilisateurs : DPO externe mutualisé (~200€/mois)
|
||||
- Ou recrutement DPO interne si > 10 employés
|
||||
|
||||
**Justification** :
|
||||
- Conforme RGPD (non obligatoire en phase MVP)
|
||||
- 0€, contrôle total
|
||||
- Bonne pratique : avoir un contact identifié
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 13
|
||||
|
||||
| Mesure | Implémentation | Coût |
|
||||
|--------|----------------|------|
|
||||
| **Consentement** | Tarteaucitron.js + PostgreSQL | 0€ |
|
||||
| **Anonymisation GPS** | Geohash PostGIS (24h) | 0€ |
|
||||
| **Export données** | JSON+HTML+ZIP asynchrone | 0€ |
|
||||
| **Suppression compte** | Grace period 30j + anonymisation | 0€ |
|
||||
| **Mode dégradé** | GeoIP IP2Location + GPS optionnel | 0€ |
|
||||
| **Conservation** | Purge auto 5 ans inactivité | 0€ |
|
||||
| **Analytics** | Matomo self-hosted | ~5€/mois |
|
||||
| **Registre traitements** | Markdown Git | 0€ |
|
||||
| **Breach detection** | Sentry + Grafana + runbook | 0€ |
|
||||
| **DPO** | Fondateur formé CNIL | 0€ |
|
||||
|
||||
**Coût total RGPD : ~5€/mois**
|
||||
|
||||
---
|
||||
|
||||
## Points d'attention pour Gherkin
|
||||
|
||||
- Tester consentement géolocalisation (accept/refuse → contenus différents)
|
||||
- Tester anonymisation GPS après 24h (job cron)
|
||||
- Tester export données (génération complète + vérification contenu)
|
||||
- Tester grace period suppression (annulation possible)
|
||||
- Tester mode GeoIP (ville détectée correctement)
|
||||
- Tester purge automatique (5 ans inactivité)
|
||||
- Tester notifications avant purge (90j/30j/7j)
|
||||
Reference in New Issue
Block a user