Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2679dd381 | ||
|
|
23fe67470b | ||
|
|
ae2fc3ee6f | ||
|
|
35aaa105d0 | ||
|
|
95c65b8be1 | ||
|
|
989dc4b6cf | ||
|
|
64d6611147 | ||
|
|
5497ea793f | ||
|
|
a29718a8e8 | ||
|
|
bc9b3b6a69 | ||
|
|
20a784b15c | ||
|
|
3da7eb80a1 | ||
|
|
62fe0ed5eb | ||
|
|
4b28db3465 | ||
|
|
b52ffb8db9 | ||
|
|
fd2b0f70c5 | ||
|
|
1896c249dd | ||
|
|
e63603551d | ||
|
|
cf7a46be27 | ||
|
|
2c0522158c | ||
|
|
1a67e5ffd0 | ||
|
|
be9fc998cc | ||
|
|
5e5fcf4714 | ||
|
|
78422bb2c0 | ||
|
|
04cd6327ab | ||
|
|
563980aeb7 | ||
|
|
bd724dcb8e | ||
|
|
f6a5b9afce | ||
|
|
e82ed63904 | ||
|
|
477630d216 | ||
|
|
851832baec | ||
|
|
3bdc6c6241 | ||
|
|
448b4b6ca7 | ||
|
|
7de686ab33 | ||
|
|
159e0b2ff4 | ||
|
|
36e30bb5ab | ||
|
|
c48222cc63 | ||
|
|
a82dbfe1dc | ||
|
|
7d3b32856e | ||
|
|
a19a901ed4 | ||
|
|
ea77aa8ac7 | ||
|
|
852240b5ec | ||
|
|
718581b954 | ||
|
|
2cc9da29ff | ||
|
|
99328a845a | ||
|
|
bac0423be9 | ||
|
|
6ba0688f87 | ||
|
|
b132fb957d | ||
|
|
4e25ceab20 | ||
|
|
267f574467 | ||
|
|
2365b7f344 | ||
|
|
158690ed3e | ||
|
|
59a6d49fbb | ||
|
|
ec28b52ae5 | ||
|
|
78b723baa3 | ||
|
|
81ccbf79e6 | ||
|
|
60dce59905 | ||
|
|
5986286c3d | ||
|
|
9bb1891bc1 | ||
|
|
18c8901d69 | ||
|
|
fa6ba43888 | ||
|
|
cf26d8a244 | ||
|
|
0609f380ff | ||
|
|
a3b7c90be0 | ||
|
|
69a7bd80cc | ||
|
|
c3abdd74af |
31
.dockerignore
Normal file
31
.dockerignore
Normal file
@@ -0,0 +1,31 @@
|
||||
# Ignore backend files
|
||||
backend/**
|
||||
mobile/**
|
||||
|
||||
# Ignore git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Ignore docker files
|
||||
docker-compose.yml
|
||||
*.dockerfile
|
||||
|
||||
# Ignore node_modules and build artifacts
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
bin/
|
||||
tmp/
|
||||
|
||||
# Ignore environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Ignore IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Ignore documentation build artifacts
|
||||
site/
|
||||
docs/generated/
|
||||
output/
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -6,9 +6,9 @@
|
||||
*.dylib
|
||||
/bin/
|
||||
/dist/
|
||||
api
|
||||
worker
|
||||
migrate
|
||||
/api
|
||||
/worker
|
||||
/migrate
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -64,4 +64,6 @@ config/*.local.yaml
|
||||
# MkDocs
|
||||
site/
|
||||
.cache/
|
||||
docs/bdd/
|
||||
|
||||
# Generated documentation
|
||||
docs/generated/
|
||||
|
||||
59
CLAUDE.md
59
CLAUDE.md
@@ -2,13 +2,19 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Important Rules
|
||||
|
||||
**Git Commits**:
|
||||
- NEVER add "Co-Authored-By: Claude" to commit messages
|
||||
- Keep commit messages clean and professional without AI attribution
|
||||
|
||||
## Project Overview
|
||||
|
||||
RoadWave is a geo-localized audio social network for road users (drivers, pedestrians, tourists). Users listen to audio content (podcasts, audio guides, ads, live radio) based on their geographic location and interests.
|
||||
|
||||
**Tech Stack**:
|
||||
- Backend: Go 1.21+ with Fiber framework
|
||||
- Mobile: Flutter (see [ADR-014](docs/adr/014-frontend-mobile.md))
|
||||
- Mobile: Flutter (see [ADR-012](docs/adr/012-frontend-mobile.md))
|
||||
- Database: PostgreSQL 16+ with PostGIS extension
|
||||
- Cache: Redis 7+ with geospatial features
|
||||
- Auth: Zitadel (self-hosted IAM)
|
||||
@@ -32,11 +38,11 @@ This is a monorepo organized as follows:
|
||||
- Backend step definitions: `backend/tests/bdd/`
|
||||
- Mobile step definitions: `mobile/tests/bdd/`
|
||||
|
||||
See [ADR-016](docs/adr/016-organisation-monorepo.md) for monorepo organization rationale.
|
||||
See [ADR-014](docs/adr/014-organisation-monorepo.md) for monorepo organization rationale.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
**Modular monolith** with clear module separation ([ADR-012](docs/adr/012-architecture-backend.md)):
|
||||
**Modular monolith** with clear module separation ([ADR-010](docs/adr/010-architecture-backend.md)):
|
||||
|
||||
```
|
||||
backend/internal/
|
||||
@@ -52,7 +58,7 @@ backend/internal/
|
||||
|
||||
**Module pattern**: Each module follows `handler.go` → `service.go` → `repository.go`.
|
||||
|
||||
**Database access**: Uses `sqlc` ([ADR-013](docs/adr/013-orm-acces-donnees.md)) for type-safe Go code generation from SQL queries. This allows writing complex PostGIS spatial queries while maintaining compile-time type safety.
|
||||
**Database access**: Uses `sqlc` ([ADR-011](docs/adr/011-orm-acces-donnees.md)) for type-safe Go code generation from SQL queries. This allows writing complex PostGIS spatial queries while maintaining compile-time type safety.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -76,7 +82,7 @@ Services after `make docker-up`:
|
||||
|
||||
### Testing
|
||||
|
||||
**Test Strategy** ([ADR-015](docs/adr/015-strategie-tests.md)):
|
||||
**Test Strategy** ([ADR-013](docs/adr/013-strategie-tests.md)):
|
||||
- Unit tests: Testify (80%+ coverage target)
|
||||
- Integration tests: Testcontainers (for PostGIS queries)
|
||||
- BDD tests: Godog/Gherkin (user stories validation)
|
||||
@@ -131,6 +137,10 @@ make docs-pdf # Generate PDF of all documentation
|
||||
make docs-clean # Remove generated docs and PDF
|
||||
```
|
||||
|
||||
**First run**: The first `make docs-serve` will build a custom Docker image with mkdocs-kroki-plugin. This takes ~30s but is cached for subsequent runs.
|
||||
|
||||
**DBML Support**: The documentation now supports DBML (Database Markup Language) for database diagrams via the Kroki plugin. Use `kroki-dbml` code blocks in markdown files. See [docs/examples/dbml-example.md](docs/examples/dbml-example.md) for examples.
|
||||
|
||||
## Working with sqlc
|
||||
|
||||
When adding or modifying database queries:
|
||||
@@ -178,13 +188,42 @@ Feature: Geolocalised recommendation
|
||||
|
||||
All technical decisions are documented in Architecture Decision Records (ADRs) in `/docs/adr/`:
|
||||
|
||||
### Core Architecture
|
||||
- [ADR-001](docs/adr/001-langage-backend.md): Backend language (Go)
|
||||
- [ADR-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS)
|
||||
- [ADR-010](docs/adr/010-architecture-backend.md): Backend architecture (modular monolith)
|
||||
- [ADR-011](docs/adr/011-orm-acces-donnees.md): Data access (sqlc)
|
||||
- [ADR-012](docs/adr/012-frontend-mobile.md): Frontend mobile (Flutter)
|
||||
- [ADR-014](docs/adr/014-organisation-monorepo.md): Monorepo organization
|
||||
|
||||
### Data & Infrastructure
|
||||
- [ADR-005](docs/adr/005-base-de-donnees.md): Database (PostgreSQL + PostGIS)
|
||||
- [ADR-021](docs/adr/021-solution-cache.md): Cache solution (Redis)
|
||||
- [ADR-015](docs/adr/015-hebergement.md): Hosting (OVH)
|
||||
- [ADR-019](docs/adr/019-geolocalisation-ip.md): IP geolocation fallback
|
||||
|
||||
### Streaming & Content
|
||||
- [ADR-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS)
|
||||
- [ADR-003](docs/adr/003-codec-audio.md): Audio codec (Opus)
|
||||
- [ADR-004](docs/adr/004-cdn.md): CDN strategy
|
||||
|
||||
### Security & Auth
|
||||
- [ADR-006](docs/adr/006-chiffrement.md): Encryption (TLS 1.3)
|
||||
- [ADR-008](docs/adr/008-authentification.md): Authentication (Zitadel)
|
||||
- [ADR-012](docs/adr/012-architecture-backend.md): Backend architecture (modular monolith)
|
||||
- [ADR-013](docs/adr/013-orm-acces-donnees.md): Data access (sqlc)
|
||||
- [ADR-016](docs/adr/016-organisation-monorepo.md): Monorepo organization
|
||||
- [ADR-025](docs/adr/025-securite-secrets.md): Secrets management
|
||||
|
||||
### Testing & Quality
|
||||
- [ADR-007](docs/adr/007-tests-bdd.md): BDD tests (Gherkin)
|
||||
- [ADR-013](docs/adr/013-strategie-tests.md): Test strategy
|
||||
- [ADR-022](docs/adr/022-strategie-cicd-monorepo.md): CI/CD strategy
|
||||
|
||||
### Features & Operations
|
||||
- [ADR-009](docs/adr/009-solution-paiement.md): Payment solution (Mangopay)
|
||||
- [ADR-016](docs/adr/016-service-emailing.md): Email service (Brevo)
|
||||
- [ADR-017](docs/adr/017-notifications-geolocalisees.md): Geo notifications
|
||||
- [ADR-018](docs/adr/018-notifications-push.md): Push notifications
|
||||
- [ADR-020](docs/adr/020-librairies-flutter.md): Flutter libraries
|
||||
- [ADR-023](docs/adr/023-architecture-moderation.md): Moderation architecture
|
||||
- [ADR-024](docs/adr/024-monitoring-observabilite.md): Monitoring & observability
|
||||
|
||||
**When making architectural decisions**, check if there's an existing ADR or create a new one following the established pattern.
|
||||
|
||||
@@ -253,7 +292,7 @@ Zitadel handles authentication ([ADR-008](docs/adr/008-authentification.md)):
|
||||
|
||||
## Performance Targets
|
||||
|
||||
See [TECHNICAL.md](TECHNICAL.md) for detailed metrics:
|
||||
See [TECHNICAL.md](docs/TECHNICAL.md) for detailed metrics:
|
||||
- API latency p99: < 100ms
|
||||
- Audio start time: < 3s
|
||||
- Target availability: 99.9%
|
||||
|
||||
28
Makefile
28
Makefile
@@ -51,7 +51,7 @@ test-integration:
|
||||
## test-bdd: Run BDD tests (Godog)
|
||||
test-bdd:
|
||||
@echo "$(BLUE)Running BDD tests...$(NC)"
|
||||
@godog run features/
|
||||
@godog run docs/domains/*/features/
|
||||
|
||||
## test-coverage: Run tests with coverage report
|
||||
test-coverage:
|
||||
@@ -70,14 +70,14 @@ clean:
|
||||
## docs-clean: Remove generated documentation (BDD docs and PDF)
|
||||
docs-clean:
|
||||
@echo "$(YELLOW)Cleaning generated documentation...$(NC)"
|
||||
@rm -rf docs/bdd/ output/RoadWave_Documentation.pdf
|
||||
@docker rmi roadwave-pdf-generator 2>/dev/null || true
|
||||
@rm -rf docs/generated/ site/
|
||||
@docker rmi roadwave-pdf-generator roadwave-mkdocs 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Documentation cleaned$(NC)"
|
||||
|
||||
## docker-up: Start all Docker services
|
||||
docker-up:
|
||||
@echo "$(BLUE)Starting Docker services...$(NC)"
|
||||
@docker compose up -d
|
||||
@cd backend && docker compose up -d
|
||||
@echo "$(GREEN)✓ Services started$(NC)"
|
||||
@echo "$(YELLOW)API: http://localhost:8080$(NC)"
|
||||
@echo "$(YELLOW)Zitadel: http://localhost:8081$(NC)"
|
||||
@@ -86,12 +86,12 @@ docker-up:
|
||||
## docker-down: Stop all Docker services
|
||||
docker-down:
|
||||
@echo "$(YELLOW)Stopping Docker services...$(NC)"
|
||||
@docker compose down
|
||||
@cd backend && docker compose down
|
||||
@echo "$(GREEN)✓ Services stopped$(NC)"
|
||||
|
||||
## docker-logs: Show Docker logs
|
||||
docker-logs:
|
||||
@docker compose logs -f
|
||||
@cd backend && docker compose logs -f
|
||||
|
||||
## migrate-up: Apply all migrations
|
||||
migrate-up:
|
||||
@@ -151,30 +151,34 @@ run-api:
|
||||
|
||||
## docs-serve: Start documentation server (http://localhost:8000)
|
||||
docs-serve:
|
||||
@echo "$(BLUE)Building MkDocs Docker image with plugins...$(NC)"
|
||||
@docker build -t roadwave-mkdocs -f docs/docker/mkdocs.Dockerfile . -q
|
||||
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
|
||||
@python3 scripts/generate-bdd-docs.py
|
||||
@python3 docs/scripts/generate-bdd-docs.py
|
||||
@echo "$(GREEN)✓ BDD documentation generated$(NC)"
|
||||
@echo "$(BLUE)Starting documentation server...$(NC)"
|
||||
@echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)"
|
||||
@docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material
|
||||
@docker run --rm -p 8000:8000 -v "$(PWD):/docs" roadwave-mkdocs
|
||||
|
||||
## bdd-docs: Generate BDD docs from Gherkin and serve with MkDocs (http://localhost:8000)
|
||||
bdd-docs:
|
||||
@echo "$(BLUE)Building MkDocs Docker image with plugins...$(NC)"
|
||||
@docker build -t roadwave-mkdocs -f docs/docker/mkdocs.Dockerfile . -q
|
||||
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
|
||||
@python3 scripts/generate-bdd-docs.py
|
||||
@python3 docs/scripts/generate-bdd-docs.py
|
||||
@echo "$(GREEN)✓ BDD documentation generated$(NC)"
|
||||
@echo "$(BLUE)Starting documentation server...$(NC)"
|
||||
@echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)"
|
||||
@echo "$(YELLOW)Navigate to 'Tests BDD' section$(NC)"
|
||||
@docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material
|
||||
@docker run --rm -p 8000:8000 -v "$(PWD):/docs" roadwave-mkdocs
|
||||
|
||||
## docs-pdf: Generate PDF of all documentation (output/RoadWave_Documentation.pdf)
|
||||
docs-pdf:
|
||||
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
|
||||
@python3 scripts/generate-bdd-docs.py
|
||||
@python3 docs/scripts/generate-bdd-docs.py
|
||||
@echo "$(GREEN)✓ BDD documentation generated$(NC)"
|
||||
@echo "$(BLUE)Building PDF generator Docker image...$(NC)"
|
||||
@docker build -t roadwave-pdf-generator -f scripts/Dockerfile.pdf . -q
|
||||
@docker build -t roadwave-pdf-generator -f docs/docker/pdf.Dockerfile . -q
|
||||
@echo "$(BLUE)Generating PDF documentation...$(NC)"
|
||||
@docker run --rm -u $(shell id -u):$(shell id -g) -v "$(PWD):/docs" roadwave-pdf-generator
|
||||
@echo "$(GREEN)✓ PDF generated: output/RoadWave_Documentation.pdf$(NC)"
|
||||
|
||||
@@ -4,15 +4,15 @@ services:
|
||||
# Backend API
|
||||
api:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
context: .
|
||||
dockerfile: docker/dev.Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- .:/app
|
||||
- /app/tmp
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
- ./.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
462
docs/CONTRIBUTING.md
Normal file
462
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# Guide de Contribution à la Documentation
|
||||
|
||||
Bienvenue ! Ce guide explique comment contribuer à la documentation RoadWave tout en respectant son architecture et ses conventions.
|
||||
|
||||
## Principes Fondamentaux
|
||||
|
||||
### 1. Source Unique de Vérité (Single Source of Truth)
|
||||
|
||||
Chaque information doit exister à **un seul endroit** :
|
||||
|
||||
- **Règles métier** → `docs/domains/[domain]/rules/*.md`
|
||||
- **Features BDD** → `domains/[domain]/features/**/*.feature` (sources Gherkin)
|
||||
- **Documentation BDD** → `docs/generated/bdd/` (générée automatiquement)
|
||||
- **Entités** → `docs/domains/[domain]/entities/*.md`
|
||||
- **États** → `docs/domains/[domain]/states/*.md`
|
||||
- **Séquences** → `docs/domains/[domain]/sequences/*.md`
|
||||
|
||||
❌ **Ne jamais** dupliquer une information
|
||||
✅ **Toujours** référencer via des liens
|
||||
|
||||
### 2. Langue : 100% Français
|
||||
|
||||
Tous les fichiers de documentation doivent être en français :
|
||||
|
||||
- ✅ Noms de fichiers en français (kebab-case)
|
||||
- ✅ Contenu en français
|
||||
- ✅ Navigation `mkdocs.yml` en français
|
||||
- ⚠️ Exception : code backend en anglais (convention internationale)
|
||||
|
||||
### 3. Dossier `generated/` = Read-Only
|
||||
|
||||
Le dossier `docs/generated/` contient **exclusivement** des fichiers générés automatiquement.
|
||||
|
||||
❌ **Ne JAMAIS** créer de fichiers manuels dans `generated/`
|
||||
❌ **Ne JAMAIS** éditer des fichiers dans `generated/`
|
||||
✅ Modifier les sources (`.feature`) et regénérer
|
||||
|
||||
## Ajouter une Nouvelle Feature BDD
|
||||
|
||||
### Étape 1 : Créer le fichier `.feature`
|
||||
|
||||
```bash
|
||||
# Créer dans le bon domaine
|
||||
touch domains/[domain]/features/[category]/ma-feature.feature
|
||||
```
|
||||
|
||||
**Exemple** : Feature de recherche dans le domaine recommendation
|
||||
|
||||
```bash
|
||||
touch domains/recommendation/features/recherche/recherche-avancee.feature
|
||||
```
|
||||
|
||||
### Étape 2 : Écrire la feature en Gherkin
|
||||
|
||||
```gherkin
|
||||
# language: fr
|
||||
Fonctionnalité: Recherche avancée de contenus
|
||||
En tant qu'utilisateur
|
||||
Je veux rechercher des contenus avec des filtres avancés
|
||||
Afin de trouver exactement ce que je cherche
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté
|
||||
Et que je suis sur la page de recherche
|
||||
|
||||
Scénario: Recherche par catégorie et distance
|
||||
Quand je sélectionne la catégorie "Tourisme"
|
||||
Et je définis un rayon de 10 km
|
||||
Alors je vois uniquement les contenus de type "Tourisme"
|
||||
Et tous les résultats sont à moins de 10 km
|
||||
|
||||
Scénario: Recherche avec filtres multiples
|
||||
# ... autres scénarios
|
||||
```
|
||||
|
||||
**Conventions Gherkin** :
|
||||
|
||||
- Langue française (`# language: fr`)
|
||||
- Mots-clés : `Fonctionnalité`, `Scénario`, `Étant donné`, `Quand`, `Alors`
|
||||
- Commentaires pour référencer les règles DDD
|
||||
|
||||
### Étape 3 : Regénérer la documentation
|
||||
|
||||
```bash
|
||||
make bdd-docs
|
||||
```
|
||||
|
||||
Cette commande :
|
||||
1. Parse les fichiers `.feature`
|
||||
2. Génère les `.md` dans `generated/bdd/`
|
||||
3. Démarre le serveur MkDocs (http://localhost:8000)
|
||||
|
||||
### Étape 4 : Ajouter dans `mkdocs.yml`
|
||||
|
||||
Si la catégorie n'existe pas encore, l'ajouter :
|
||||
|
||||
```yaml
|
||||
|
||||
- '🎯 Recommendation':
|
||||
# ...
|
||||
- Tests BDD:
|
||||
- 'Recherche':
|
||||
- Recherche de contenu: generated/bdd/recommendation/features/recherche/recherche.md
|
||||
- Recherche avancée: generated/bdd/recommendation/features/recherche/recherche-avancee.md # ← NOUVEAU
|
||||
```
|
||||
|
||||
### Étape 5 : Commit
|
||||
|
||||
```bash
|
||||
git add domains/recommendation/features/recherche/recherche-avancee.feature
|
||||
git add mkdocs.yml
|
||||
git commit -m "feat(bdd): ajouter feature recherche avancée
|
||||
|
||||
- Ajout recherche par catégorie et distance
|
||||
- Ajout filtres multiples
|
||||
- Couverture domaine recommendation"
|
||||
```
|
||||
|
||||
## Ajouter une Règle Métier
|
||||
|
||||
### Étape 1 : Identifier le domaine
|
||||
|
||||
Déterminer dans quel domaine DDD se situe la règle :
|
||||
|
||||
- **Shared** : Authentification, RGPD, erreurs, profil
|
||||
- **Recommendation** : Algorithme, jauges d'intérêt, recherche
|
||||
- **Content** : Audio-guides, création, streaming
|
||||
- **Moderation** : Signalements, validation, sanctions
|
||||
- **Advertising** : Publicités, ciblage
|
||||
- **Premium** : Abonnements, mode offline
|
||||
- **Monetization** : Paiements, revenus créateurs
|
||||
|
||||
### Étape 2 : Créer ou éditer le fichier de règles
|
||||
|
||||
```bash
|
||||
# Éditer fichier existant
|
||||
vim domains/[domain]/rules/[theme].md
|
||||
|
||||
# Ou créer un nouveau fichier
|
||||
touch domains/[domain]/rules/nouveau-theme.md
|
||||
```
|
||||
|
||||
### Étape 3 : Documenter la règle
|
||||
|
||||
```markdown
|
||||
# Règles - Nouveau Thème
|
||||
|
||||
## Contexte
|
||||
|
||||
Description du contexte métier...
|
||||
|
||||
## Règles
|
||||
|
||||
### Règle 1 : Titre de la règle
|
||||
|
||||
**Contexte** : Quand...
|
||||
**Règle** : L'utilisateur doit/peut/ne peut pas...
|
||||
**Justification** : Parce que...
|
||||
|
||||
**Exemple** :
|
||||
|
||||
- Cas nominal : ...
|
||||
- Cas d'erreur : ...
|
||||
|
||||
### Règle 2 : Autre règle
|
||||
|
||||
...
|
||||
|
||||
## Liens avec d'autres domaines
|
||||
|
||||
- [Entité X](../entities/entite-x.md)
|
||||
- [Séquence Y](../sequences/sequence-y.md)
|
||||
- [ADR-XXX](../../adr/XXX-decision.md)
|
||||
```
|
||||
|
||||
### Étape 4 : Créer les features BDD correspondantes
|
||||
|
||||
Les règles métier doivent être testables → créer des features BDD.
|
||||
|
||||
### Étape 5 : Ajouter dans `mkdocs.yml`
|
||||
|
||||
```yaml
|
||||
|
||||
- '🎙️ Content':
|
||||
- Règles:
|
||||
- Création & Publication: domains/content/rules/creation-publication.md
|
||||
- Nouveau Thème: domains/content/rules/nouveau-theme.md # ← NOUVEAU
|
||||
```
|
||||
|
||||
## Ajouter une Entité
|
||||
|
||||
### Créer le fichier
|
||||
|
||||
```bash
|
||||
touch domains/[domain]/entities/nouvelle-entite.md
|
||||
```
|
||||
|
||||
### Documenter l'entité
|
||||
|
||||
```markdown
|
||||
# Entité : NouvelleThing
|
||||
|
||||
## Description
|
||||
|
||||
Brève description de l'entité...
|
||||
|
||||
## Attributs
|
||||
|
||||
| Attribut | Type | Description | Contraintes |
|
||||
|----------|------|-------------|-------------|
|
||||
| `id` | UUID | Identifiant unique | PK, NOT NULL |
|
||||
| `name` | String | Nom de la chose | NOT NULL, max 255 |
|
||||
| `created_at` | Timestamp | Date de création | NOT NULL |
|
||||
|
||||
## Relations
|
||||
|
||||
- **BelongsTo** : [User](../../../_shared/entities/users.md)
|
||||
- **HasMany** : [Items](./items.md)
|
||||
|
||||
## États du Cycle de Vie
|
||||
|
||||
Voir [Lifecycle](../states/nouvelle-thing-lifecycle.md)
|
||||
|
||||
## Règles Métier
|
||||
|
||||
- [Règle de création](../rules/creation.md#nouvelle-thing)
|
||||
- [Règle de validation](../rules/validation.md#nouvelle-thing)
|
||||
|
||||
## Implémentation Backend
|
||||
|
||||
**Module** : `backend/internal/[domain]/`
|
||||
**Table** : `nouvelle_things`
|
||||
**Repository** : `nouvelle_thing_repository.go`
|
||||
|
||||
Requêtes sqlc : `backend/queries/nouvelle_thing.sql`
|
||||
```
|
||||
|
||||
### Ajouter dans `mkdocs.yml`
|
||||
|
||||
```yaml
|
||||
|
||||
- Entités:
|
||||
- Vue d'ensemble: domains/[domain]/entities/vue-ensemble.md
|
||||
- Nouvelle Entité: domains/[domain]/entities/nouvelle-entite.md # ← NOUVEAU
|
||||
```
|
||||
|
||||
## Ajouter un Diagramme
|
||||
|
||||
### Diagrammes de Séquence
|
||||
|
||||
```bash
|
||||
touch domains/[domain]/sequences/nouveau-processus.md
|
||||
```
|
||||
|
||||
```markdown
|
||||
# Séquence - Nouveau Processus
|
||||
|
||||
## Diagramme
|
||||
|
||||
\`\`\`mermaid
|
||||
sequenceDiagram
|
||||
participant U as Utilisateur
|
||||
participant A as API
|
||||
participant DB as Database
|
||||
|
||||
U->>A: POST /nouvelle-action
|
||||
A->>DB: Vérifier permissions
|
||||
DB-->>A: OK
|
||||
A->>DB: INSERT nouvelle_thing
|
||||
A-->>U: 201 Created
|
||||
\`\`\`
|
||||
|
||||
## Légende
|
||||
|
||||
**Acteurs** :
|
||||
|
||||
- Utilisateur : Client de l'API
|
||||
- API : Backend RoadWave
|
||||
- Database : PostgreSQL
|
||||
|
||||
**Étapes clés** :
|
||||
1. Vérification des permissions
|
||||
2. Insertion en base
|
||||
3. Retour confirmation
|
||||
|
||||
## Cas d'Erreur
|
||||
|
||||
### Permissions insuffisantes
|
||||
...
|
||||
|
||||
### Contrainte violée
|
||||
...
|
||||
```
|
||||
|
||||
### Diagrammes d'États
|
||||
|
||||
```bash
|
||||
touch domains/[domain]/states/nouvelle-thing-lifecycle.md
|
||||
```
|
||||
|
||||
```markdown
|
||||
# États - Lifecycle NouvelleThing
|
||||
|
||||
## Diagramme
|
||||
|
||||
\`\`\`mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Draft
|
||||
Draft --> Pending: Submit
|
||||
Pending --> Approved: Approve
|
||||
Pending --> Rejected: Reject
|
||||
Approved --> Published: Publish
|
||||
Published --> Archived: Archive
|
||||
Archived --> [*]
|
||||
\`\`\`
|
||||
|
||||
## États
|
||||
|
||||
| État | Description | Transitions Possibles |
|
||||
|------|-------------|----------------------|
|
||||
| Draft | Création en cours | → Pending |
|
||||
| Pending | En attente validation | → Approved, Rejected |
|
||||
| Approved | Validé | → Published |
|
||||
| Rejected | Refusé | → Draft (correction) |
|
||||
| Published | Publié | → Archived |
|
||||
| Archived | Archivé | (final) |
|
||||
|
||||
## Règles de Transition
|
||||
|
||||
### Draft → Pending
|
||||
**Conditions** : Tous les champs obligatoires remplis
|
||||
**Actions** : Notification modérateurs
|
||||
```
|
||||
|
||||
## Conventions de Nommage
|
||||
|
||||
### Fichiers Markdown
|
||||
|
||||
```bash
|
||||
# Format : kebab-case-francais.md
|
||||
✅ export-donnees.md
|
||||
✅ suppression-compte.md
|
||||
✅ moderation-communautaire.md
|
||||
✅ vue-ensemble.md
|
||||
|
||||
❌ data-export.md (anglais)
|
||||
❌ entities-overview.md (anglais)
|
||||
❌ ExportDonnees.md (PascalCase)
|
||||
❌ export_donnees.md (snake_case)
|
||||
```
|
||||
|
||||
### Fichiers Features
|
||||
|
||||
```bash
|
||||
# Format : kebab-case-francais.feature
|
||||
✅ signalement.feature
|
||||
✅ jauge-initiale.feature
|
||||
✅ creation-audio-guide.feature
|
||||
|
||||
❌ reporting.feature (anglais)
|
||||
❌ initial-gauge.feature (anglais)
|
||||
```
|
||||
|
||||
### Répertoires
|
||||
|
||||
```bash
|
||||
# Format : kebab-case-anglais (convention internationale)
|
||||
✅ features/
|
||||
✅ interest-gauges/
|
||||
✅ content-creation/
|
||||
✅ rgpd-compliance/
|
||||
```
|
||||
|
||||
## Vérifications Avant Commit
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Tous les fichiers sont en français (sauf code backend)
|
||||
- [ ] Pas de fichiers dans `generated/` (seulement sources modifiées)
|
||||
- [ ] Les liens internes fonctionnent
|
||||
- [ ] `mkdocs.yml` mis à jour si nécessaire
|
||||
- [ ] Features BDD regénérées (`make bdd-docs`)
|
||||
- [ ] Message de commit descriptif
|
||||
|
||||
### Tester Localement
|
||||
|
||||
```bash
|
||||
# Vérifier syntaxe YAML
|
||||
python3 -c "import yaml; yaml.safe_load(open('mkdocs.yml'))"
|
||||
|
||||
# Tester la navigation
|
||||
make docs-serve
|
||||
# → http://localhost:8000
|
||||
```
|
||||
|
||||
### Vérifier les Liens
|
||||
|
||||
```bash
|
||||
# Chercher liens cassés vers anciens noms
|
||||
grep -r "entities-overview\.md" docs/
|
||||
grep -r "user-account-lifecycle\.md" docs/
|
||||
grep -r "data-export\.md" docs/
|
||||
|
||||
# Ne devrait rien retourner
|
||||
```
|
||||
|
||||
## Messages de Commit
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
type(scope): description courte
|
||||
|
||||
Corps optionnel avec détails...
|
||||
|
||||
Refs: #issue-number
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- `feat(bdd)` : Nouvelle feature BDD
|
||||
- `docs(rules)` : Nouvelle règle métier
|
||||
- `docs(entity)` : Nouvelle entité
|
||||
- `fix(link)` : Correction de lien
|
||||
- `refactor(nav)` : Réorganisation navigation
|
||||
|
||||
### Exemples
|
||||
|
||||
```bash
|
||||
# Feature BDD
|
||||
git commit -m "feat(bdd): ajouter recherche avancée dans recommendation"
|
||||
|
||||
# Règle métier
|
||||
git commit -m "docs(rules): documenter règles de modération préventive"
|
||||
|
||||
# Entité
|
||||
git commit -m "docs(entity): ajouter entité Campaign pour advertising"
|
||||
|
||||
# Fix
|
||||
git commit -m "fix(link): corriger lien vers vue-ensemble.md"
|
||||
```
|
||||
|
||||
## Ressources
|
||||
|
||||
- **Architecture DDD** : [domains/README.md](domains/README.md)
|
||||
- **Tests BDD** : [ADR-007](adr/007-tests-bdd.md)
|
||||
- **ADRs** : [adr/README.md](adr/README.md)
|
||||
|
||||
## Support
|
||||
|
||||
Questions ? Consultez :
|
||||
1. Cette documentation
|
||||
2. Les ADRs existants
|
||||
3. Les exemples dans les domaines existants
|
||||
4. L'équipe RoadWave
|
||||
|
||||
## Auteurs
|
||||
|
||||
- Équipe RoadWave
|
||||
- Claude Sonnet 4.5 (documentation)
|
||||
|
||||
Dernière mise à jour : 2026-02-08
|
||||
@@ -1,849 +0,0 @@
|
||||
# Analyse des Incohérences entre ADR et Règles Métier
|
||||
|
||||
**Date d'analyse** : 2026-01-28
|
||||
**Analysé par** : Audit Architecture RoadWave
|
||||
**Scope** : 18 ADR × Règles Métier (17 fichiers)
|
||||
|
||||
---
|
||||
|
||||
## Résumé Exécutif
|
||||
|
||||
Cette analyse a identifié **15 incohérences** entre les décisions d'architecture (ADR) et les règles métier du projet RoadWave.
|
||||
|
||||
### Répartition par Sévérité
|
||||
|
||||
| Sévérité | Nombre | % Total | Statut | Action Required |
|
||||
|----------|--------|---------|--------|-----------------|
|
||||
| 🔴 **CRITICAL** | 2 | 14% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
|
||||
| 🟠 **HIGH** | 2 | 14% | ✅ **RÉSOLU** (2 résolus, 1 annulé) | ~~Résolution Sprint 1-2~~ |
|
||||
| 🟡 **MODERATE** | 9 | 64% | ✅ **RÉSOLU** (6 résolus, 2 annulés, 1 documenté) | ~~Résolution Sprint 3-5~~ |
|
||||
| 🟢 **LOW** | 1 | 7% | ✅ **ANNULÉ** (Faux problème) | ~~À clarifier lors du développement~~ |
|
||||
|
||||
### Impact par Domaine
|
||||
|
||||
| Domaine | Nombre d'incohérences | Criticité maximale |
|
||||
|---------|----------------------|-------------------|
|
||||
| Streaming & Géolocalisation | 3 | 🔴 CRITICAL |
|
||||
| Données & Infrastructure | 2 | 🟠 HIGH |
|
||||
| Authentification & Sécurité | 2 | 🟠 HIGH |
|
||||
| Tests & Qualité | 2 | 🟡 MODERATE |
|
||||
| Coûts & Déploiement | 3 | 🟡 MODERATE |
|
||||
| UX & Engagement | 2 | 🟡 MODERATE |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Incohérences Critiques (Blocantes)
|
||||
|
||||
### #1 : HLS ne supporte pas les Notifications Push en Arrière-plan
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-017 créé)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-002 (Protocole Streaming) |
|
||||
| **Règle métier** | Règle 05, section 5.1.2 (Mode Piéton, lignes 86-120) |
|
||||
| **Conflit** | HLS est unidirectionnel (serveur→client), ne peut pas envoyer de push quand l'app est fermée |
|
||||
| **Impact** | Mode piéton non fonctionnel : notifications "Point d'intérêt à 200m" impossibles |
|
||||
|
||||
**Scénario d'échec** :
|
||||
```
|
||||
Utilisateur: Marie se promène, app fermée
|
||||
Position: 150m de la Tour Eiffel
|
||||
Attendu: Push notification "🗼 À proximité: Histoire de la Tour Eiffel"
|
||||
Réel: Rien (HLS ne peut pas notifier)
|
||||
```
|
||||
|
||||
**Solution implémentée** :
|
||||
- ✅ **ADR-017** : Architecture hybride WebSocket + Firebase Cloud Messaging
|
||||
- Phase 1 (MVP) : Push serveur via FCM/APNS
|
||||
- Phase 2 : Geofencing natif iOS/Android pour mode offline
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Implémenter endpoint WebSocket `/ws/location`
|
||||
- [ ] Backend : Worker PostGIS avec requête `ST_DWithin` (30s interval)
|
||||
- [ ] Mobile : Intégrer Firebase SDK (`firebase_messaging`)
|
||||
- [ ] Tests : Validation en conditions réelles (10 testeurs, Paris)
|
||||
|
||||
---
|
||||
|
||||
### #2 : Latence HLS Incompatible avec ETA de 7 Secondes
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-002 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-002 (Protocole Streaming, lignes 40-41) |
|
||||
| **Règle métier** | Règle 05 (lignes 16-20), Règle 17 (lignes 25-30, 120-124) |
|
||||
| **Conflit** | ETA de 7s avant le point, mais HLS a 5-30s de latence → audio démarre APRÈS avoir dépassé le point |
|
||||
| **Impact** | UX catastrophique : utilisateur entend "Vous êtes devant le château" 100m APRÈS l'avoir dépassé |
|
||||
|
||||
**Calcul du problème** (90 km/h = 25 m/s) :
|
||||
```
|
||||
t=0s → Notification "Suivant: Château dans 7s" (175m avant)
|
||||
t=7s → Utilisateur arrive au château
|
||||
t=15s → HLS démarre (latence 15s)
|
||||
Résultat: Audio démarre 200m APRÈS le point ❌
|
||||
```
|
||||
|
||||
**Solution implémentée** :
|
||||
- ✅ **ADR-002 mis à jour** : Section "Gestion de la Latence et Synchronisation Géolocalisée"
|
||||
- Pre-buffering à ETA=30s (15 premières secondes en cache local)
|
||||
- ETA adaptatif : 5s si cache prêt, 15s sinon
|
||||
- Mesure dynamique de latence HLS par utilisateur
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Endpoint `/api/v1/audio/poi/:id/intro` (retourne 15s d'audio)
|
||||
- [ ] Mobile : Service `PreBufferService` avec cache local (max 100 MB)
|
||||
- [ ] Mobile : Loader visuel avec progression si buffer > 3s
|
||||
- [ ] Tests : Validation synchronisation ±10m du POI
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Incohérences Importantes (Sprint 1-2)
|
||||
|
||||
### #3 : Souveraineté des Données (Français vs Suisse)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-008 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-004 (CDN, ligne 26), ADR-008 (Auth, mis à jour) |
|
||||
| **Règle métier** | Règle 02 (RGPD, section 13.10) |
|
||||
| **Conflit** | ADR-004 revendique "100% souveraineté française" mais ADR-008 utilisait Zitadel (entreprise suisse) |
|
||||
| **Impact** | Contradiction marketing + risque juridique si promesse "100% français" |
|
||||
|
||||
**Solution implémentée** : **Self-hosting Zitadel sur OVH France**
|
||||
|
||||
- ✅ Container Docker sur le même VPS OVH (Gravelines, France)
|
||||
- ✅ Base de données PostgreSQL partagée (schéma séparé pour Zitadel)
|
||||
- ✅ Aucune donnée ne transite par des serveurs tiers
|
||||
- ✅ Souveraineté totale garantie : 100% des données en France
|
||||
- ✅ Cohérence complète avec ADR-004 (CDN 100% français)
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ ADR-008 mis à jour avec architecture self-hosted détaillée
|
||||
- ✅ TECHNICAL.md mis à jour (tableau + diagramme architecture)
|
||||
- ✅ Clarification : Zitadel est open source, donc aucune dépendance à une entreprise suisse
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] Décision validée : Self-host sur OVH
|
||||
- [x] ADR-008 mis à jour avec architecture self-hosted
|
||||
- [x] TECHNICAL.md mis à jour
|
||||
|
||||
---
|
||||
|
||||
### #4 : ORM sqlc vs Types PostGIS
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-011 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-011 (section "Gestion des Types PostGIS") |
|
||||
| **Règle métier** | N/A (problème technique pur) |
|
||||
| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement |
|
||||
| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée |
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**Wrappers typés + fonctions de conversion PostGIS** :
|
||||
|
||||
1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique
|
||||
2. **Patterns SQL recommandés** :
|
||||
- `ST_AsGeoJSON(location)::jsonb` → struct `GeoJSON` typée (frontend)
|
||||
- `ST_AsText(location)` → string `WKT` (debug/logging)
|
||||
- `ST_Distance()::float8` → natif Go float64
|
||||
3. **Index GIST** sur colonnes géographiques pour performance
|
||||
4. **Architecture conversion** :
|
||||
```
|
||||
SQL PostGIS → ST_AsGeoJSON() → json.RawMessage → GeoJSON (strongly-typed)
|
||||
```
|
||||
|
||||
**Code Pattern** :
|
||||
|
||||
```go
|
||||
// internal/geo/types.go
|
||||
type GeoJSON struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates [2]float64 `json:"coordinates"`
|
||||
}
|
||||
|
||||
func (g *GeoJSON) Scan(value interface{}) error {
|
||||
bytes, _ := value.([]byte)
|
||||
return json.Unmarshal(bytes, g)
|
||||
}
|
||||
```
|
||||
|
||||
```sql
|
||||
-- queries/poi.sql
|
||||
SELECT id, ST_AsGeoJSON(location)::jsonb as location,
|
||||
ST_Distance(location, $1::geography) as distance_meters
|
||||
FROM points_of_interest
|
||||
WHERE ST_DWithin(location, $1::geography, $2);
|
||||
```
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Créer package `backend/internal/geo` avec wrappers
|
||||
- [ ] Ajouter migrations index GIST (`CREATE INDEX idx_poi_gist ON pois USING GIST(location)`)
|
||||
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
|
||||
- [ ] Documenter patterns dans `backend/README.md`
|
||||
|
||||
**Référence** : [ADR-011 - Gestion des Types PostGIS](docs/adr/011-orm-acces-donnees.md#gestion-des-types-postgis)
|
||||
|
||||
---
|
||||
|
||||
### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours)
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-005 (BDD, ligne 60) |
|
||||
| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) |
|
||||
| **Conflit** | ~~Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours~~ |
|
||||
| **Impact** | ~~En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés~~ |
|
||||
|
||||
**Raison de l'annulation** : Le mode offline ne concerne **pas les POI** (Points d'Intérêt) mais uniquement le contenu audio déjà téléchargé. La détection de POI proches nécessite par nature une connexion active pour la géolocalisation en temps réel. Il n'y a donc pas d'incohérence entre le cache Redis (pour mode connecté) et le mode offline (pour lecture audio hors ligne).
|
||||
|
||||
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
|
||||
|
||||
---
|
||||
|
||||
### #6 : Package Geofencing vs Permissions iOS/Android
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Stratégie de permissions progressive implémentée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-010 (Frontend Mobile, mis à jour) |
|
||||
| **Règle métier** | Règle 05 (section 5.1.2, mis à jour), Règle 02 (RGPD) |
|
||||
| **Conflit** | ~~Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles"~~ |
|
||||
| **Impact** | ~~Risque de rejet App Store/Play Store si permissions obligatoires mal gérées~~ |
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**Stratégie de permissions progressive en 2 étapes** :
|
||||
|
||||
```dart
|
||||
enum LocationPermissionLevel {
|
||||
denied, // Pas de permission
|
||||
whenInUse, // "Quand l'app est ouverte" (iOS)
|
||||
always, // "Toujours" (iOS) / Background (Android)
|
||||
}
|
||||
|
||||
class GeofencingService {
|
||||
Future<void> requestPermissions() async {
|
||||
// Étape 1: Demander "When In Use" (moins intrusif)
|
||||
var status = await Permission.locationWhenInUse.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
// Mode basique: détection seulement app ouverte
|
||||
_enableBasicGeofencing();
|
||||
|
||||
// Étape 2 (optionnelle): Proposer upgrade vers "Always"
|
||||
_showUpgradePermissionDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> upgradeToAlwaysPermission() async {
|
||||
// Demandé seulement si utilisateur veut mode piéton complet
|
||||
await Permission.locationAlways.request();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-010 mis à jour avec section complète "Stratégie de Permissions"
|
||||
- [x] ✅ Règle 05 (section 5.1.2) mise à jour avec clarifications permissions progressive
|
||||
- [x] ✅ Documentation détaillée créée : `/docs/mobile/permissions-strategy.md`
|
||||
- [x] ✅ Plan de validation TestFlight créé : `/docs/mobile/testflight-validation-plan.md`
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ Permissions demandées en 2 étapes : "When In Use" (onboarding) → "Always" (optionnel, mode piéton)
|
||||
- ✅ Écran d'éducation obligatoire avant demande "Always" (requis pour validation stores)
|
||||
- ✅ Fallback gracieux à tous niveaux : app utilisable même sans permission arrière-plan
|
||||
- ✅ Mode dégradé (GeoIP) si toutes permissions refusées
|
||||
- ✅ Configuration iOS/Android complète avec textes validés RGPD
|
||||
- ✅ Plan de validation beta (TestFlight + Play Console Internal Testing)
|
||||
|
||||
**Références** :
|
||||
- [ADR-010 - Stratégie de Permissions](../adr/010-frontend-mobile.md#stratégie-de-permissions-iosandroid)
|
||||
- [Documentation Permissions](../mobile/permissions-strategy.md)
|
||||
- [Plan Validation TestFlight](../mobile/testflight-validation-plan.md)
|
||||
- [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Incohérences Modérées (Sprint 3-5)
|
||||
|
||||
### #7 : Points vs Pourcentages dans les Jauges
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Terminologie unifiée : points de pourcentage absolus)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | Règle 05 (section 5.3) (Commandes Volant, mis à jour) |
|
||||
| **Règle métier** | Règle 03 (Centres d'intérêt, mis à jour) |
|
||||
| **Conflit** | ~~ADR dit "+2 **points**", Règle dit "+2**%**" pour même action~~ |
|
||||
| **Impact** | ~~Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ?~~ |
|
||||
|
||||
**Solution adoptée** : **Option A (points de pourcentage absolus)**
|
||||
|
||||
**Calcul confirmé** :
|
||||
```
|
||||
Jauge "Automobile" = 45%
|
||||
Utilisateur écoute 85% d'un podcast voiture
|
||||
→ Like renforcé : +2%
|
||||
→ 45 + 2 = 47% ✅
|
||||
|
||||
NOT 45 × 1.02 = 45.9% ❌
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- ✅ **Progression linéaire** : Intuitive et prévisible
|
||||
- ✅ **Équité** : Tous les utilisateurs progressent à la même vitesse
|
||||
- ✅ **Gamification standard** : Cohérent avec Duolingo, Spotify, Strava
|
||||
- ✅ **Simplicité technique** : Addition simple, pas de risque d'overflow
|
||||
- ✅ **Prédictibilité UX** : "+2%" signifie vraiment +2 points de pourcentage
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Règle 05 (section 5.3) mis à jour : "points" → "+2%" avec note explicite "points de pourcentage absolus"
|
||||
- [x] ✅ Règle 05 (section 5.3) : Section "Implémentation Technique" ajoutée (architecture 2 services)
|
||||
- [x] ✅ Règle 03 : Note ajoutée clarifiant calcul absolu vs relatif
|
||||
- [x] ✅ Règle 03 : Exemples de calcul vérifiés et cohérents
|
||||
- [x] ✅ Référence croisée Règle 05 (section 5.3) ↔ Règle 03
|
||||
- [x] ✅ ADR-010 supprimé : Contenu consolidé dans Règle 05 (métier) pour éviter redondance
|
||||
|
||||
**Changements apportés** :
|
||||
|
||||
**Règle 05 (section 5.3)** :
|
||||
- Règles reformulées : "+2 **points**" → "**+2%**" (points de pourcentage absolus)
|
||||
- Note explicite ajoutée : "Par exemple, si jauge = 45%, +2% → 47%"
|
||||
- Nouvelle section "Implémentation Technique" avec architecture 2 services (Calculation + Update)
|
||||
- Pattern de calcul correct (addition) vs incorrect (multiplication)
|
||||
- Exemples de calcul concrets
|
||||
|
||||
**Règle 03** :
|
||||
- Tableau mis à jour : valeurs en gras (**+2%**, **+1%**, etc.)
|
||||
- Note importante ajoutée : "points de pourcentage absolus, PAS relatifs"
|
||||
- Exemple anti-pattern : "NOT 45 × 1.02 = 45.9% ❌"
|
||||
- Référence croisée vers Règle 05 (section 5.3) pour implémentation
|
||||
|
||||
**Références** :
|
||||
- [Règle 05 - Implémentation Technique](../regles-metier/05-interactions-navigation.md#implémentation-technique-backend)
|
||||
- [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
|
||||
|
||||
---
|
||||
|
||||
### #8 : OAuth2 Complexe vs Email/Password Simple
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Clarification : OAuth2 = protocole, PAS providers tiers)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-008 (Auth, mis à jour) |
|
||||
| **Règle métier** | Règle 01 (Auth, mis à jour) |
|
||||
| **Conflit** | ~~ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement"~~ |
|
||||
| **Impact** | ~~Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici~~ |
|
||||
|
||||
**Clarification** : Il y avait une **confusion terminologique** entre :
|
||||
- **OAuth2 PKCE** (protocole d'authentification moderne pour mobile) ✅ Utilisé
|
||||
- **OAuth providers tiers** (Google, Apple, Facebook) ❌ **Pas utilisés**
|
||||
|
||||
**Solution adoptée** :
|
||||
|
||||
RoadWave utilise **Zitadel self-hosted** avec **email/password natif uniquement** :
|
||||
|
||||
| Aspect | Détail |
|
||||
|--------|--------|
|
||||
| **Méthode d'authentification** | Email + mot de passe (formulaire natif Zitadel) |
|
||||
| **Protocole technique** | OAuth2 PKCE (entre app mobile et Zitadel) |
|
||||
| **Fournisseurs tiers** | ❌ Aucun (pas de Google, Apple, Facebook) |
|
||||
|
||||
**Pourquoi OAuth2 PKCE alors ?** :
|
||||
- ✅ **Standard moderne** pour auth mobile (sécurisé, refresh tokens)
|
||||
- ✅ **Protocole**, pas un provider externe
|
||||
- ✅ Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
- ✅ Zitadel implémente OAuth2/OIDC comme protocole, mais auth reste email/password
|
||||
|
||||
**Flow d'authentification** :
|
||||
```
|
||||
User → Formulaire email/password (app mobile)
|
||||
→ Zitadel (OAuth2 PKCE protocol)
|
||||
→ Validation email/password natif
|
||||
→ JWT access token + refresh token
|
||||
→ Go API (validation JWT locale)
|
||||
```
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-008 : Section "OAuth2 PKCE : Protocole vs Fournisseurs Tiers" ajoutée
|
||||
- [x] ✅ ADR-008 : Architecture clarifiée ("Email/Pass native" dans diagramme)
|
||||
- [x] ✅ ADR-008 : Note explicite : "OAuth2 PKCE = protocole, PAS providers tiers"
|
||||
- [x] ✅ Règle 01 : Clarification technique ajoutée + référence croisée ADR-008
|
||||
|
||||
**Références** :
|
||||
- [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers)
|
||||
- [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)
|
||||
|
||||
---
|
||||
|
||||
### #9 : GeoIP Database (MaxMind)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-019 créé)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-019 (créé) |
|
||||
| **Règle métier** | Règle 02 (RGPD, mis à jour) |
|
||||
| **Conflit** | ~~Règle citait "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019~~ |
|
||||
| **Impact** | ~~Coût caché potentiel~~ |
|
||||
|
||||
**Historique** :
|
||||
- **Avant 2019** : GeoLite2 database téléchargeable gratuitement
|
||||
- **Après 2019** : Compte requis + limite 1000 requêtes/jour (gratuit)
|
||||
- **Dépassement** : 0.003$/requête
|
||||
|
||||
**Utilisation RoadWave** :
|
||||
- Mode dégradé (sans GPS) → GeoIP pour localisation approximative
|
||||
- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour)
|
||||
|
||||
**Solution implémentée** : **IP2Location Lite (self-hosted)**
|
||||
|
||||
| Option | Coût/mois | Précision | Maintenance |
|
||||
|--------|-----------|-----------|-------------|
|
||||
| **IP2Location Lite** ✅ | Gratuit | ±50 km | Maj mensuelle |
|
||||
| MaxMind API | ~10€ | ±50 km | Nulle |
|
||||
| Self-hosted MaxMind | Gratuit | ±50 km | Compte requis |
|
||||
|
||||
**Architecture** :
|
||||
```
|
||||
[Backend Go] → [GeoIP Service]
|
||||
↓
|
||||
[IP2Location SQLite DB]
|
||||
(màj mensuelle via cron)
|
||||
```
|
||||
|
||||
**Avantages** :
|
||||
- ✅ Gratuit (pas de limite de requêtes)
|
||||
- ✅ Self-hosted (souveraineté des données, cohérence avec ADR-004)
|
||||
- ✅ Pas de compte tiers requis
|
||||
- ✅ Base de données SQLite légère (50-100 MB)
|
||||
- ✅ Mise à jour mensuelle automatisable
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-019 créé : Service de Géolocalisation par IP
|
||||
- [x] ✅ Règle 02 mise à jour (ligne 147 et 317)
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Implémenter service GeoIP avec IP2Location
|
||||
- [ ] DevOps : Cron job màj mensuelle de la DB
|
||||
|
||||
**Référence** : [ADR-019 - Service de Géolocalisation par IP](../adr/019-geolocalisation-ip.md)
|
||||
|
||||
---
|
||||
|
||||
### #10 : Tests BDD Synchronisés (Backend + Mobile)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Catégorisation features implémentée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-007 (mis à jour), ADR-011 (Stratégie, lignes 59-62) |
|
||||
| **Règle métier** | Toutes (Gherkin) |
|
||||
| **Conflit** | ~~Features partagées `/features`, step definitions séparées → qui exécute quoi ?~~ |
|
||||
| **Impact** | ~~Risque de divergence backend/mobile si tests pas synchronisés~~ |
|
||||
|
||||
**Architecture initiale** :
|
||||
|
||||
```
|
||||
/features/*.feature (mélangées par domaine)
|
||||
/backend/tests/bdd/ (step definitions Go)
|
||||
/mobile/tests/bdd/ (step definitions Dart)
|
||||
```
|
||||
|
||||
**Solution implémentée** : **Catégorisation en 3 couches**
|
||||
|
||||
```
|
||||
/features/
|
||||
/api/ → Backend uniquement (tests API REST)
|
||||
├── authentication/ # REST endpoints, validation email, 2FA
|
||||
├── recommendation/ # Algorithm backend, scoring GPS
|
||||
├── rgpd-compliance/ # GDPR API (delete, export, consent)
|
||||
├── content-creation/ # Upload, encoding, validation API
|
||||
├── moderation/ # Moderation workflow API
|
||||
├── monetisation/ # Payments, KYC, payouts API
|
||||
├── premium/ # Subscription API
|
||||
├── radio-live/ # Live streaming backend
|
||||
└── publicites/ # Ads API, budget, metrics
|
||||
|
||||
/ui/ → Mobile uniquement (tests interface)
|
||||
├── audio-guides/ # Audio player UI, modes (piéton, vélo)
|
||||
├── navigation/ # Steering wheel, voice commands, UI
|
||||
├── interest-gauges/ # Gauge visualization, progression
|
||||
├── mode-offline/ # Download UI, sync status
|
||||
├── partage/ # Share dialog
|
||||
├── profil/ # Creator profile screen
|
||||
└── recherche/ # Search bar, filters UI
|
||||
|
||||
/e2e/ → End-to-end (backend + mobile ensemble)
|
||||
├── abonnements/ # Full subscription flow (Mangopay + Zitadel + UI)
|
||||
└── error-handling/ # Network errors, GPS disabled (backend + mobile)
|
||||
```
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ 93 features réorganisées en 3 catégories (api/ui/e2e)
|
||||
- ✅ ADR-007 mis à jour avec section complète "Convention de Catégorisation"
|
||||
- ✅ ADR-014 mis à jour avec stratégie CI/CD path filters (documentée, implémentation reportée)
|
||||
- ✅ Historique Git préservé via `git mv` (pas de perte d'historique)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Réorganiser `/features` en 3 catégories (api, ui, e2e)
|
||||
- [x] ✅ Mettre à jour ADR-007 avec convention de nommage et exemples
|
||||
- [x] ⏸️ CI/CD : Documenté dans ADR-014 (implémentation reportée jusqu'au développement backend/mobile)
|
||||
|
||||
**Références** :
|
||||
- [ADR-007 - Convention de Catégorisation](../adr/007-tests-bdd.md#convention-de-catégorisation)
|
||||
- [ADR-020 - Stratégie CI/CD Path Filters](../adr/020-strategie-cicd-monorepo.md)
|
||||
|
||||
---
|
||||
|
||||
### #11 : 70/30 Split Paiements (Vérification Manquante)
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème - Documentation complète et cohérente)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-009 (Paiement, lignes 32-52) |
|
||||
| **Règle métier** | Règle 18 (Monétisation créateurs, section 9.4.B, lignes 121-165) ✅ **Existe et complète** |
|
||||
| **Conflit** | ~~ADR assume 70/30 split sans référence règle métier~~ **Aucun conflit** |
|
||||
| **Impact** | ~~Risque de mauvaise répartition revenus créateurs~~ **Aucun impact** |
|
||||
|
||||
**Vérification complète** :
|
||||
|
||||
✅ **ADR-009 spécifie** :
|
||||
- 70% créateur
|
||||
- 30% plateforme
|
||||
- Diagramme explicite : "Créateur A 70%", "Créateur B 70%", "Plateforme 30%"
|
||||
|
||||
✅ **Règle 18 (section 9.4.B, lignes 121-165) spécifie** :
|
||||
- **Formule exacte** : "70% au créateur, 30% à la plateforme"
|
||||
- **Répartition proportionnelle** : au temps d'écoute effectif
|
||||
- **Exemple concret** :
|
||||
```
|
||||
Utilisateur Premium = 4.99€/mois
|
||||
├─ 3.49€ reversés aux créateurs (70%)
|
||||
└─ 1.50€ gardés par plateforme (30%)
|
||||
```
|
||||
- **Calcul détaillé** (lignes 132-136) :
|
||||
- Si user écoute 3 créateurs : Creator A (50%) → 1.75€, Creator B (30%) → 1.05€, Creator C (20%) → 0.70€
|
||||
- **Requête SQL fournie** (lignes 140-151) : implémentation technique de la distribution proportionnelle
|
||||
- **Comparaison industrie** (lignes 153-157) :
|
||||
- YouTube Premium : 70/30
|
||||
- Spotify : 70/30
|
||||
- Apple Music : 52/48 (moins favorable)
|
||||
- RoadWave : 70/30 (standard)
|
||||
- **Justifications business** (lignes 159-163) :
|
||||
- Ratio standard industrie (prouvé et équitable)
|
||||
- Incitation qualité : créateurs avec plus d'écoutes gagnent plus
|
||||
- Équité : pas de "winner takes all", chaque créateur reçoit sa part
|
||||
- Marge plateforme : 30% couvre l'absence de revenus publicitaires sur Premium
|
||||
|
||||
**Conclusion** : Il n'y a **aucune incohérence**. ADR-009 et Règle 18 sont **parfaitement alignés** et se complètent :
|
||||
- ADR-009 documente l'**implémentation technique** (Mangopay, split payments)
|
||||
- Règle 18 documente la **logique métier** (formule, exemples, justifications, comparaisons)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Règle 18 lue et analysée complètement
|
||||
- [x] ✅ Vérification 70/30 : **cohérent** entre ADR-009 et Règle 18
|
||||
- [x] ❌ Mise à jour ADR-009 : **non requise** (déjà correct)
|
||||
|
||||
**Aucune action requise** : Ce point peut être fermé définitivement.
|
||||
|
||||
---
|
||||
|
||||
### #12 : Monorepo Path Filters vs Features Partagées
|
||||
|
||||
**Statut** : ⏸️ **DOCUMENTÉ** (Implémentation CI/CD reportée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-014 (Monorepo, mis à jour) |
|
||||
| **Règle métier** | N/A (problème CI/CD) |
|
||||
| **Conflit initial** | ~~Path filters pour éviter rebuild tout, mais features partagées déclenchent tout~~ |
|
||||
| **Impact** | ~~Optimisation CI/CD inefficace~~ → **Résolu par catégorisation #10** |
|
||||
|
||||
**Problème initial** :
|
||||
|
||||
```yaml
|
||||
# .github/workflows/backend.yml
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/**' # ❌ Change sur n'importe quel .feature → rebuild backend
|
||||
```
|
||||
|
||||
**Solution implémentée** : Path filters **par catégorie** (dépend de #10 ✅ résolu)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/backend.yml (architecture documentée)
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/api/**' # ✅ Seulement features API
|
||||
- 'features/e2e/**' # ✅ E2E impacte backend
|
||||
|
||||
# .github/workflows/mobile.yml (architecture documentée)
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
- 'features/ui/**' # ✅ Seulement features UI
|
||||
- 'features/e2e/**' # ✅ E2E impacte mobile
|
||||
```
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ Catégorisation features (point #10) : **résolue** → permet path filters sélectifs
|
||||
- ✅ ADR-014 mis à jour avec section complète "Stratégie CI/CD avec Path Filters"
|
||||
- Architecture workflows séparés (backend.yml, mobile.yml, shared.yml)
|
||||
- Configuration path filters détaillée
|
||||
- Tableau de déclenchement par type de modification
|
||||
- Avantages (rebuild sélectif, économie ~70% temps CI, parallélisation)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Catégorisation features implémentée (résolution #10)
|
||||
- [x] ✅ ADR-014 mis à jour avec stratégie path filters complète
|
||||
- [x] ⏸️ Implémentation workflows CI/CD : **Reportée jusqu'à l'implémentation du code backend/mobile**
|
||||
|
||||
**Note importante** : Le projet est actuellement en **phase de documentation uniquement** (aucun code backend/mobile implémenté). L'implémentation des workflows CI/CD sera faite lors du Sprint d'implémentation backend/mobile.
|
||||
|
||||
**Références** :
|
||||
- [ADR-020 - Stratégie CI/CD Path Filters](../adr/020-strategie-cicd-monorepo.md)
|
||||
- Point #10 résolu (catégorisation features)
|
||||
|
||||
---
|
||||
|
||||
### #13 : Coûts Email (Transition Free → Paid)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Périmètre réduit : emails techniques uniquement)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-016 (mis à jour) |
|
||||
| **Règle métier** | N/A (économique) |
|
||||
| **Conflit initial** | ~~ADR citait "gratuit" mais volume estimé dépassait 9000 emails/mois~~ |
|
||||
| **Impact initial** | ~~Coût surprise lors de la croissance~~ |
|
||||
|
||||
**Décision** : **Limiter aux emails techniques uniquement** (pas de notifications, alertes marketing, newsletters)
|
||||
|
||||
**Périmètre strict** :
|
||||
- ✅ Authentification (vérification email, reset password, changement email)
|
||||
- ✅ Sécurité (alertes connexion inhabituelle)
|
||||
- ✅ Modération (strikes, suspensions)
|
||||
- ✅ RGPD (confirmation suppression, export données)
|
||||
- ❌ **Pas de notifications sociales** (écoutes, likes, commentaires)
|
||||
- ❌ **Pas d'alertes marketing** (recommandations, nouvelles sorties)
|
||||
- ❌ **Pas de newsletters/promotions**
|
||||
- ❌ **Pas d'emails paiements créateurs** (Mangopay envoie déjà ses propres emails)
|
||||
|
||||
**Calcul révisé** (emails techniques uniquement) :
|
||||
```
|
||||
Emails par utilisateur/mois (régime stable):
|
||||
- Vérification email (nouveaux users): 0.1 (10% croissance)
|
||||
- Reset password: 0.1 (10% des users)
|
||||
- Changement email: 0.05 (5%)
|
||||
- Alertes sécurité: 0.02 (2%)
|
||||
- Modération: 0.01 (1%)
|
||||
|
||||
Total: ~0.28 emails/user/mois
|
||||
|
||||
10K users × 0.28 = 2800 emails/mois = 93 emails/jour
|
||||
→ Largement sous le tier gratuit (300/jour) ✅
|
||||
```
|
||||
|
||||
**Projection de coûts révisée** :
|
||||
|
||||
| Phase | Utilisateurs | Emails/jour moyen | Coût Brevo |
|
||||
|-------|--------------|-------------------|------------|
|
||||
| MVP | 0-10K | 93/jour | **Gratuit** ✅ |
|
||||
| Growth | 10K-50K | 467/jour | 19€/mois (Lite) |
|
||||
| Scale | 50K-100K | 933/jour | 49€/mois (Business) |
|
||||
|
||||
**Gestion des pics** :
|
||||
- Rate limiting : 250 emails/heure (batch processing)
|
||||
- Redis queue pour lisser l'envoi sur 24-48h
|
||||
- Upgrade temporaire Lite (19€) si pic > 300/jour sur 3+ jours
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-016 mis à jour avec périmètre strict et projection coûts
|
||||
- [x] ✅ Clarification : pas d'emails notifications/marketing/paiements
|
||||
- [x] ✅ Stratégie gestion pics d'inscription documentée
|
||||
|
||||
**Référence** : [ADR-016 - Service d'Emailing Transactionnel](../adr/016-service-emailing.md)
|
||||
|
||||
---
|
||||
|
||||
### #14 : Kubernetes vs VPS MVP
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Vision clarifiée : K8s est un bonus, pas la raison principale)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-015 (mis à jour), ADR-001 (mis à jour) |
|
||||
| **Règle métier** | N/A (infrastructure) |
|
||||
| **Conflit initial** | ~~ADR-001 justifiait Go pour "Kubernetes first-class", mais ADR-015 utilisait VPS simple~~ |
|
||||
| **Impact initial** | ~~Ambiguïté : Go choisi pour K8s mais K8s pas utilisé en MVP~~ |
|
||||
|
||||
**Analyse** :
|
||||
|
||||
- **ADR-001 initial** : Mentionnait "Kubernetes first-class" dans tooling natif
|
||||
- **ADR-015 initial** : MVP sur OVH VPS Essential (Docker Compose), K8s à "100K+ users"
|
||||
- **Problème perçu** : Incohérence entre choix Go (pour K8s) et infra MVP (pas K8s)
|
||||
|
||||
**Clarification apportée** :
|
||||
|
||||
Go est choisi **principalement** pour :
|
||||
1. ✅ **Simplicité** et time-to-market (MVP 8 semaines vs 12+ Rust)
|
||||
2. ✅ **Écosystème mature** (PostGIS, WebRTC, Zitadel, BDD tests)
|
||||
3. ✅ **Performance concurrentielle** (1M conn/serveur suffisant)
|
||||
4. ✅ **Typing fort** et tooling natif (pprof, race detector)
|
||||
|
||||
Kubernetes est un **bonus** pour scalabilité future (Phase 3 : 100K+ users), **pas la raison principale**.
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**ADR-001** : Note ajoutée clarifiant que :
|
||||
- K8s n'est **pas utilisé en MVP** (Docker Compose suffit pour 0-20K users)
|
||||
- Go choisi **principalement** pour simplicité, écosystème, performance
|
||||
- Support K8s = **bonus** scalabilité future, pas driver du choix
|
||||
|
||||
**ADR-015** : Section complète "Roadmap Infrastructure" ajoutée :
|
||||
|
||||
| Phase | Users | Infrastructure | Trigger principal |
|
||||
|-------|-------|----------------|-------------------|
|
||||
| **MVP** | 0-20K | OVH VPS + Docker Compose | Aucun (démarrage) |
|
||||
| **Croissance** | 20-100K | Scaleway managé | CPU > 70% OU MRR > 2000€ |
|
||||
| **Scale** | 100K+ | Scaleway Kubernetes | Auto-scaling OU multi-région |
|
||||
|
||||
**Triggers de migration détaillés** :
|
||||
- Phase 2 : CPU > 70%, latence p99 > 100ms, MRR > 2000€
|
||||
- Phase 3 : Auto-scaling requis, multi-région, > 5 services backend, DevOps dédié
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-001 mis à jour : Note explicite "K8s = bonus, pas raison principale"
|
||||
- [x] ✅ ADR-015 : Section "Roadmap Infrastructure" complète (3 phases + triggers)
|
||||
- [x] ✅ Cohérence architecture : Vision long-terme clarifiée sans sur-architecture MVP
|
||||
|
||||
**Références** :
|
||||
- [ADR-001 - Justification Go (K8s bonus)](../adr/001-langage-backend.md#pourquoi-go-plutôt-que-rust-)
|
||||
- [ADR-015 - Roadmap Infrastructure](../adr/015-hebergement.md#roadmap-infrastructure)
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Incohérences Mineures (Clarification)
|
||||
|
||||
### #15 : Unlike Manuel sur Contenu Auto-liké
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème - séparation mode voiture/piéton)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | Règle 05 (section 5.3) (ligne 15-21) |
|
||||
| **Règle métier** | Règle 05 (lignes 343-346, "Disponibilité"), Règle 03 (lignes 93-99) |
|
||||
| **Conflit initial** | ~~Auto-like +2% documenté, mais unlike manuel non spécifié~~ |
|
||||
| **Impact initial** | ~~Ambiguïté : faut-il annuler (+2%) si unlike ?~~ |
|
||||
|
||||
**Raison de l'annulation** : Le scénario du conflit **ne peut pas se produire** car les deux fonctionnalités sont **mutuellement exclusives** selon le mode de déplacement :
|
||||
|
||||
**Séparation stricte par mode** (Règle 05, lignes 343-346) :
|
||||
- **Mode voiture** (vitesse ≥ 5 km/h) :
|
||||
- ✅ Auto-like actif (basé sur temps d'écoute)
|
||||
- ❌ **Pas de bouton Unlike** (aucune action manuelle, sécurité routière)
|
||||
- **Mode piéton** (vitesse < 5 km/h) :
|
||||
- ✅ Bouton Like/Unlike disponible (interactions manuelles)
|
||||
- ❌ **Pas d'auto-like** (seulement actions explicites)
|
||||
|
||||
**Scénario impossible** :
|
||||
```
|
||||
1. Utilisateur écoute 85% en mode voiture → auto-like → jauge +2%
|
||||
→ Pas de bouton Unlike (mode conduite) ❌
|
||||
|
||||
2. Utilisateur en mode piéton → bouton Unlike disponible
|
||||
→ Pas d'auto-like (seulement like manuel) ❌
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- L'écoute longue (85%) **éveille la curiosité** (justifie auto-like en mode voiture)
|
||||
- Le unlike ne se fait **qu'en mode piéton** (où il n'y a pas d'auto-like)
|
||||
- Les deux systèmes sont **isolés** et ne peuvent pas interagir
|
||||
|
||||
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
|
||||
|
||||
---
|
||||
|
||||
## Plan d'Action Global
|
||||
|
||||
### Phase 1 : Résolutions Critiques (Avant Implémentation)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Deadline |
|
||||
|---|-------|-------------|--------|----------|
|
||||
| 1 | ✅ Créer ADR-017 (Notifications) | Architecture | 2h | ✅ Fait |
|
||||
| 2 | ✅ Mettre à jour ADR-002 (Pre-buffering) | Architecture | 1h | ✅ Fait |
|
||||
| 3 | Implémenter WebSocket backend | Backend Lead | 3j | Sprint 1 |
|
||||
| 4 | Implémenter Pre-buffer mobile | Mobile Lead | 2j | Sprint 1 |
|
||||
|
||||
### Phase 2 : Résolutions Importantes (Sprint 1-2)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** |
|
||||
| 6 | ✅ Package geo types (PostGIS) | Backend | 1j | ✅ **Fait** |
|
||||
| 7 | ~~Cache 2 niveaux (Redis + SQLite)~~ | Backend + Mobile | ~~3j~~ | ❌ **Annulé** (faux problème) |
|
||||
| 8 | ✅ Stratégie permissions progressive | Mobile | 2j | ✅ **Fait** |
|
||||
|
||||
### Phase 3 : Résolutions Modérées (Sprint 3-5)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 9 | ✅ Clarification Points vs Pourcentages (Règle 05 + Règle 03, ADR-010 supprimé) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 10 | ✅ Clarification OAuth2 protocole vs providers (ADR-008 + Règle 01) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 11 | ✅ GeoIP Database (ADR-019 + Règle 02) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 12 | ✅ Réorganisation features BDD + CI/CD path filters (ADR-007, ADR-020) | QA Lead | 2j | ✅ **Fait** |
|
||||
| 13 | ✅ Projection coûts Email (ADR-016, périmètre réduit) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 14 | ✅ Clarification Kubernetes (ADR-001, ADR-015 roadmap) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 15 | ✅ Unlike Manuel (Faux problème - modes séparés) | Tech Writer | 0.5j | ❌ **Annulé** |
|
||||
|
||||
---
|
||||
|
||||
## Métriques de Suivi
|
||||
|
||||
| Métrique | Valeur Initiale | Cible | Actuel |
|
||||
|----------|----------------|-------|--------|
|
||||
| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) |
|
||||
| Incohérences HIGH | 4 | 0 | ✅ **0** (2 résolues, 1 annulée) |
|
||||
| Incohérences MODERATE | 9 | ≤2 | ✅ **0** (6 résolus, 2 annulés, 1 documenté) |
|
||||
| Incohérences LOW | 1 | 0 | ✅ **0** (1 annulée) |
|
||||
| ADR à jour | 66% (12/18) | 100% | ✅ **100%** (19/19 - ADR-016 mis à jour) |
|
||||
| Coverage documentation | N/A | >90% | ✅ **95%** |
|
||||
|
||||
**Dernière mise à jour** : 2026-02-01
|
||||
|
||||
**Détail MODERATE** :
|
||||
- ✅ **Traités (9/9)** : #7 (résolu), #8 (résolu), #9 (résolu), #10 (résolu), #11 (annulé), #12 (documenté), #13 (résolu), #14 (résolu), #15 (annulé)
|
||||
|
||||
**Détail LOW** :
|
||||
- ✅ **Traité (1/1)** : #15 (Unlike Manuel - annulé, reclassé de MODERATE → LOW puis annulé car faux problème)
|
||||
|
||||
---
|
||||
|
||||
## Contacts et Ressources
|
||||
|
||||
- **Analyse complète** : Ce document
|
||||
- **ADR-017** : `/docs/adr/017-notifications-geolocalisees.md`
|
||||
- **ADR-019** : `/docs/adr/019-geolocalisation-ip.md`
|
||||
- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md`
|
||||
- **Questions** : Créer une issue GitHub avec tag `[architecture]`
|
||||
|
||||
---
|
||||
|
||||
**Prochaine revue** : 2026-02-15 (après Sprint 2)
|
||||
282
docs/README.md
Normal file
282
docs/README.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# RoadWave
|
||||
|
||||
Réseau social audio géolocalisé pour les usagers de la route.
|
||||
|
||||
## Concept
|
||||
|
||||
RoadWave permet aux conducteurs d'écouter du contenu audio contextuel pendant leurs trajets. La navigation se fait par commandes au volant (suivant/précédent), inspirée des réseaux à scroll infini.
|
||||
|
||||
Le contenu est diffusé en fonction de la position géographique de l'utilisateur et de ses centres d'intérêt.
|
||||
|
||||
---
|
||||
|
||||
## Cas d'usage
|
||||
|
||||
| Utilisateur | Scénario |
|
||||
|-------------|----------|
|
||||
| **Conducteur** | Écoute contenu audio en conduisant, navigation par commandes au volant (suivant/précédent), reçoit notifications géolocalisées en passant près de points d'intérêt |
|
||||
| **Routier** | Écoute podcasts et radios live pendant ses trajets longue distance |
|
||||
| **Touriste à pied** | Visite guidée audio d'un musée, monument ou ville : choisit parmi plusieurs guides, navigue entre séquences à son rythme (tactile/vocal), reçoit notification push quand un audio-guide est disponible à proximité |
|
||||
| **Commerçant** | Diffuse une publicité audio ciblée GPS devant son commerce |
|
||||
| **Passionné auto** | Découvre du contenu automobile près de circuits ou concessionnaires |
|
||||
| **Habitant local** | Partage anecdotes ou bons plans géolocalisés dans son quartier |
|
||||
| **Média traditionnel** | Le Monde, Le Parisien diffusent actualités géolocalisées ou nationales |
|
||||
|
||||
---
|
||||
|
||||
## Utilisateurs
|
||||
|
||||
Tout utilisateur peut écouter et créer du contenu (rôle flexible).
|
||||
|
||||
| Rôle | Description |
|
||||
|------|-------------|
|
||||
| **Auditeur** | Écoute, like, s'abonne à des créateurs, signale des contenus |
|
||||
| **Créateur** | Publie du contenu audio géolocalisé (individus, médias traditionnels) |
|
||||
| **Publicitaire** | Diffuse des publicités ciblées géographiquement |
|
||||
| **Modérateur** | Valide et modère les contenus signalés |
|
||||
|
||||
---
|
||||
|
||||
## Types de contenu
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| **Contenu court** | Audio de quelques secondes à quelques minutes |
|
||||
| **Podcast** | Épisodes plus longs, séries thématiques |
|
||||
| **Radio live** | Diffusion en direct avec synchronisation approximative entre auditeurs |
|
||||
| **Audio-guide** | Visite guidée multiséquence (musée, monument, ville) : plusieurs séquences numérotées, navigation manuelle entre pistes, liste complète visible, guidage vocal entre points d'intérêt |
|
||||
|
||||
---
|
||||
|
||||
## Géolocalisation
|
||||
|
||||
Le créateur définit la zone de diffusion de son contenu :
|
||||
|
||||
| Niveau | Portée |
|
||||
|--------|--------|
|
||||
| **Point GPS** | Rayon précis autour d'une coordonnée |
|
||||
| **Ville** | Diffusion dans une ville |
|
||||
| **Département** | Diffusion départementale |
|
||||
| **Région** | Diffusion régionale |
|
||||
| **Pays** | Diffusion nationale |
|
||||
|
||||
**Priorité de diffusion** : plus la zone est précise, plus le contenu a de chances d'être diffusé (GPS > ville > département > région > pays).
|
||||
|
||||
---
|
||||
|
||||
## Algorithme de recommandation
|
||||
|
||||
Le contenu proposé est calculé via un **score combiné** :
|
||||
|
||||
- **Proximité géographique** : distance entre l'utilisateur et la zone du contenu
|
||||
- **Pertinence des intérêts** : correspondance avec les centres d'intérêt de l'utilisateur
|
||||
|
||||
Lorsque plusieurs contenus sont disponibles dans une zone, **seul le plus pertinent est diffusé**.
|
||||
|
||||
---
|
||||
|
||||
## Centres d'intérêt
|
||||
|
||||
Chaque utilisateur possède des **jauges d'intérêt** qui évoluent dynamiquement :
|
||||
|
||||
### Catégories
|
||||
|
||||
- Automobile
|
||||
- Voyage
|
||||
- Famille
|
||||
- Amour
|
||||
- Musique
|
||||
- Économie
|
||||
- Cryptomonnaie
|
||||
- Politique
|
||||
- *... (extensible)*
|
||||
|
||||
### Évolution des jauges
|
||||
|
||||
| Action | Effet |
|
||||
|--------|-------|
|
||||
| Temps d'écoute long | Augmente la jauge |
|
||||
| Like | Augmente la jauge |
|
||||
| Abonnement | Augmente fortement la jauge |
|
||||
| Skip rapide | Diminue la jauge |
|
||||
|
||||
Les créateurs taguent leur contenu avec des centres d'intérêt. L'algorithme privilégie les correspondances mais n'exclut pas les utilisateurs sans correspondance.
|
||||
|
||||
---
|
||||
|
||||
## Interactions
|
||||
|
||||
### Commandes au volant (conduite)
|
||||
|
||||
Interactions simplifiées pour sécurité routière maximale :
|
||||
|
||||
| Commande | Action |
|
||||
|----------|--------|
|
||||
| **Suivant** | Passer au contenu suivant |
|
||||
| **Précédent** | Revenir au contenu précédent |
|
||||
| **Play/Pause** | Mettre en pause / reprendre la lecture |
|
||||
|
||||
**Like automatique** : Le système détecte automatiquement vos préférences selon votre temps d'écoute :
|
||||
|
||||
- Écoute ≥80% du contenu → Like renforcé (+2 points jauge)
|
||||
- Écoute 30-79% du contenu → Like standard (+1 point jauge)
|
||||
- Skip après <10s → Signal négatif (-0.5 point)
|
||||
---
|
||||
|
||||
## Publicités
|
||||
|
||||
- Insertion **entre deux contenus** uniquement (jamais d'interruption)
|
||||
- Ciblage géographique : point GPS, ville, département, région ou national
|
||||
- Interface dédiée pour les publicitaires
|
||||
|
||||
---
|
||||
|
||||
## Radio live
|
||||
|
||||
- Diffusion en direct par des créateurs
|
||||
- **Buffering** pour garantir une écoute fluide
|
||||
- **Synchronisation approximative** entre les auditeurs (quelques secondes de décalage possible)
|
||||
|
||||
---
|
||||
|
||||
## Modération
|
||||
|
||||
Approche hybride combinant participation communautaire, IA et modérateurs dédiés.
|
||||
|
||||
### Contenus prohibés
|
||||
|
||||
| Catégorie | Description |
|
||||
|-----------|-------------|
|
||||
| **Haine et violence** | Incitation à la haine, violence, discrimination |
|
||||
| **Contenu sexuel** | Pornographie ou contenu sexuellement explicite |
|
||||
| **Illégalité** | Apologie du terrorisme, actes criminels |
|
||||
| **Désinformation dangereuse** | Fausses informations sur la santé, sécurité routière |
|
||||
| **Harcèlement** | Menaces, intimidation, doxxing |
|
||||
| **Droits d'auteur** | Violation de propriété intellectuelle |
|
||||
| **Fraude** | Arnaques, escroqueries |
|
||||
|
||||
### Rôles de modération
|
||||
|
||||
| Rôle | Capacités |
|
||||
|------|-----------|
|
||||
| **Auditeur lambda** | Signaler un contenu (1 clic) |
|
||||
| **Auditeur de confiance** | Signalements priorisés après historique positif |
|
||||
| **Modérateur junior** | Traiter signalements simples (spam, contenu évident) |
|
||||
| **Modérateur senior** | Cas complexes, appels, décisions de ban |
|
||||
| **Admin modération** | Définir les règles, superviser l'équipe |
|
||||
|
||||
### Flux de modération
|
||||
|
||||
```
|
||||
1. Auditeur signale → File d'attente
|
||||
2. IA pré-filtre → Cas évidents traités automatiquement
|
||||
3. Modérateur junior → Traite 80% des cas restants
|
||||
4. Modérateur senior → Cas complexes + recours
|
||||
```
|
||||
|
||||
### Outils de modération automatique
|
||||
|
||||
| Outil | Fonction |
|
||||
|-------|----------|
|
||||
| **Transcription audio** | Conversion automatique en texte pour analyse |
|
||||
| **Analyse vocale IA** | Détection de ton agressif, cris, insultes |
|
||||
| **Empreinte audio** | Détection de contenus déjà modérés (réupload) |
|
||||
| **Détection droits d'auteur** | Identification automatique de musique protégée |
|
||||
| **Filtrage mots-clés** | Liste noire de termes inappropriés |
|
||||
|
||||
### Modération préventive
|
||||
|
||||
- **Nouveaux créateurs** : validation manuelle des 3 premiers contenus
|
||||
- **Score de confiance** : évolution selon l'historique du créateur
|
||||
- **Publicités** : validation manuelle obligatoire avant diffusion
|
||||
|
||||
### Système de strikes
|
||||
|
||||
| Strike | Sanction | Durée suspension |
|
||||
|--------|----------|------------------|
|
||||
| **Strike 1** | Avertissement + suppression contenu + suspension upload | 3 jours |
|
||||
| **Strike 2** | Suppression contenu + suspension upload | 7 jours |
|
||||
| **Strike 3** | Suppression contenu + suspension upload | 30 jours |
|
||||
| **Strike 4** | Ban définitif du compte créateur | Permanent |
|
||||
|
||||
**Notes** :
|
||||
|
||||
- **Tolérance 1ère fois** (droits d'auteur uniquement) : avertissement sans strike
|
||||
- **Violations graves** (haine, illégalité, violence) : strike immédiat sans tolérance
|
||||
- **Réhabilitation** : -1 strike tous les 6 mois sans nouvelle violation
|
||||
|
||||
### Priorisation des signalements
|
||||
|
||||
| Priorité | Type de contenu |
|
||||
|----------|-----------------|
|
||||
| **CRITIQUE** | Violence, suicide, mise en danger immédiate |
|
||||
| **HAUTE** | Harcèlement, haine, désinformation |
|
||||
| **MOYENNE** | Spam, contenu inapproprié |
|
||||
| **BASSE** | Qualité audio, tags incorrects |
|
||||
|
||||
### Transparence et recours
|
||||
|
||||
- **Notification explicite** lors de suppression (raison détaillée)
|
||||
- **Processus d'appel** : le créateur peut contester une décision
|
||||
- **Délai de traitement** : 48-72h pour les recours
|
||||
- **Historique** : tableau de bord des sanctions pour le créateur
|
||||
|
||||
### Modération communautaire
|
||||
|
||||
- **Utilisateurs de confiance** : signalements priorisés après historique positif
|
||||
- **Récompenses** : badges, réduction premium pour signalements pertinents
|
||||
- Lutte contre les signalements abusifs (sanctions possibles)
|
||||
|
||||
---
|
||||
|
||||
## Modèle économique
|
||||
|
||||
### Offres
|
||||
|
||||
| Formule | Description |
|
||||
|---------|-------------|
|
||||
| **Gratuit** | Accès complet avec publicités entre les contenus |
|
||||
| **Premium** | Sans publicité + accès aux contenus exclusifs |
|
||||
|
||||
### Monétisation créateurs
|
||||
|
||||
- **Partage des revenus pub** : rémunération basée sur le nombre d'écoutes
|
||||
- **Pourboires** : les auditeurs peuvent faire des dons aux créateurs
|
||||
|
||||
---
|
||||
|
||||
## Conformité RGPD
|
||||
|
||||
### Permissions GPS
|
||||
|
||||
RoadWave adapte ses permissions selon l'usage :
|
||||
|
||||
| Mode | Permission GPS | Fonctionnalités |
|
||||
|------|----------------|-----------------|
|
||||
| **Mode voiture** | "When In Use" uniquement | Écoute contenu géolocalisé pendant conduite |
|
||||
| **Mode piéton** | "Always Location" optionnelle | Notifications audio-guides en arrière-plan |
|
||||
|
||||
**Important** : La permission "Always Location" est **optionnelle** et réservée au mode piéton pour recevoir des notifications push lorsqu'un audio-guide est disponible à proximité. Elle n'est jamais requise pour l'usage principal en voiture.
|
||||
### Données collectées
|
||||
|
||||
| Donnée | Finalité | Base légale |
|
||||
|--------|----------|-------------|
|
||||
| **Position GPS** | Diffusion de contenu géolocalisé | Consentement |
|
||||
| **Historique d'écoute** | Personnalisation des recommandations | Intérêt légitime |
|
||||
| **Centres d'intérêt** | Algorithme de recommandation | Consentement |
|
||||
| **Identité créateur** | Publication de contenu | Exécution du contrat |
|
||||
|
||||
### Droits des utilisateurs
|
||||
|
||||
- **Accès** : consulter toutes ses données personnelles
|
||||
- **Rectification** : modifier ses informations
|
||||
- **Suppression** : supprimer son compte et toutes ses données
|
||||
- **Portabilité** : exporter ses données dans un format standard
|
||||
- **Opposition** : désactiver le profilage publicitaire
|
||||
|
||||
### Mesures techniques
|
||||
|
||||
- Consentement explicite requis pour la géolocalisation
|
||||
- Anonymisation des données de localisation après 24h (sauf historique personnel)
|
||||
- Possibilité d'utiliser l'app en mode dégradé (sans géolocalisation précise)
|
||||
- Données hébergées dans l'UE
|
||||
@@ -1,31 +1,37 @@
|
||||
# RoadWave - Architecture Technique
|
||||
|
||||
> Les décisions techniques sont documentées dans [docs/adr/](docs/adr/)
|
||||
> Les décisions techniques sont documentées dans [adr/](adr/)
|
||||
|
||||
## Stack Technologique
|
||||
|
||||
| Composant | Technologie | ADR |
|
||||
|-----------|-------------|-----|
|
||||
| **Backend** | Go + Fiber | [ADR-001](docs/adr/001-langage-backend.md) |
|
||||
| **Architecture Backend** | Monolithe Modulaire | [ADR-012](docs/adr/012-architecture-backend.md) |
|
||||
| **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](docs/adr/008-authentification.md) |
|
||||
| **Streaming** | HLS | [ADR-002](docs/adr/002-protocole-streaming.md) |
|
||||
| **Codec** | Opus | [ADR-003](docs/adr/003-codec-audio.md) |
|
||||
| **CDN** | NGINX Cache (OVH VPS) | [ADR-004](docs/adr/004-cdn.md) |
|
||||
| **Storage** | OVH Object Storage | [ADR-004](docs/adr/004-cdn.md) |
|
||||
| **Hébergement MVP** | OVH VPS Essential | [ADR-017](docs/adr/017-hebergement.md) |
|
||||
| **Organisation** | Monorepo | [ADR-016](docs/adr/016-organisation-monorepo.md) |
|
||||
| **Base de données** | PostgreSQL + PostGIS | [ADR-005](docs/adr/005-base-de-donnees.md) |
|
||||
| **ORM/Accès données** | sqlc | [ADR-013](docs/adr/013-orm-acces-donnees.md) |
|
||||
| **Cache** | Redis Cluster | [ADR-005](docs/adr/005-base-de-donnees.md) |
|
||||
| **Chiffrement** | TLS 1.3 | [ADR-006](docs/adr/006-chiffrement.md) |
|
||||
| **Live** | WebRTC | [ADR-002](docs/adr/002-protocole-streaming.md) |
|
||||
| **Frontend Mobile** | Flutter | [ADR-014](docs/adr/014-frontend-mobile.md) |
|
||||
| **Tests** | Testify + Godog (Gherkin) | [ADR-015](docs/adr/015-strategie-tests.md), [ADR-007](docs/adr/007-tests-bdd.md) |
|
||||
| **Paiements** | Mangopay | [ADR-009](docs/adr/009-solution-paiement.md) |
|
||||
| **Emailing** | Brevo | [ADR-018](docs/adr/018-service-emailing.md) |
|
||||
| **Commandes volant** | Like automatique | [ADR-010](docs/adr/010-commandes-volant.md) |
|
||||
| **Conformité stores** | CarPlay, Android Auto, App/Play Store | [ADR-011](docs/adr/011-conformite-stores-carplay-android-auto.md) |
|
||||
| **Backend** | Go + Fiber | [ADR-001](adr/001-langage-backend.md) |
|
||||
| **Architecture Backend** | Monolithe Modulaire | [ADR-010](adr/010-architecture-backend.md) |
|
||||
| **Librairies Backend** | Fiber, pgx, rueidis, sqlc, etc. | [ADR-018](adr/018-librairies-go.md) |
|
||||
| **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](adr/008-authentification.md) |
|
||||
| **Streaming** | HLS | [ADR-002](adr/002-protocole-streaming.md) |
|
||||
| **Codec** | Opus | [ADR-003](adr/003-codec-audio.md) |
|
||||
| **CDN** | NGINX Cache (OVH VPS) | [ADR-004](adr/004-cdn.md) |
|
||||
| **Storage** | OVH Object Storage | [ADR-004](adr/004-cdn.md) |
|
||||
| **Hébergement MVP** | OVH VPS Essential | [ADR-015](adr/015-hebergement.md) |
|
||||
| **Organisation** | Monorepo | [ADR-014](adr/014-organisation-monorepo.md) |
|
||||
| **Base de données** | PostgreSQL + PostGIS | [ADR-005](adr/005-base-de-donnees.md) |
|
||||
| **ORM/Accès données** | sqlc | [ADR-011](adr/011-orm-acces-donnees.md) |
|
||||
| **Cache** | Redis Cluster | [ADR-021](adr/021-solution-cache.md) |
|
||||
| **Chiffrement** | TLS 1.3 | [ADR-006](adr/006-chiffrement.md) |
|
||||
| **Live** | WebRTC | [ADR-002](adr/002-protocole-streaming.md) |
|
||||
| **Frontend Mobile** | Flutter | [ADR-012](adr/012-frontend-mobile.md) |
|
||||
| **Tests** | Testify + Godog (Gherkin) | [ADR-013](adr/013-strategie-tests.md), [ADR-007](adr/007-tests-bdd.md) |
|
||||
| **Paiements** | Mangopay | [ADR-009](adr/009-solution-paiement.md) |
|
||||
| **Emailing** | Brevo | [ADR-016](adr/016-service-emailing.md) |
|
||||
| **Géolocalisation IP** | IP2Location (fallback) | [ADR-019](adr/019-geolocalisation-ip.md) |
|
||||
| **Librairies Mobile** | Flutter packages | [ADR-020](adr/020-librairies-flutter.md) |
|
||||
| **CI/CD** | GitHub Actions (monorepo) | [ADR-022](adr/022-strategie-cicd-monorepo.md) |
|
||||
| **Modération** | Architecture modération | [ADR-023](adr/023-architecture-moderation.md) |
|
||||
| **Monitoring** | Prometheus + Grafana | [ADR-024](adr/024-monitoring-observabilite.md) |
|
||||
| **Secrets** | Vault + sealed secrets | [ADR-025](adr/025-securite-secrets.md) |
|
||||
| **Notifications** | Push géolocalisées (FCM/APNS + geofencing) | [ADR-017](adr/017-notifications-geolocalisees.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -47,6 +47,7 @@ Streaming audio vers des utilisateurs mobiles en voiture, avec réseaux instable
|
||||
La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées qui doivent déclencher l'audio **au moment précis** où l'utilisateur atteint un point d'intérêt.
|
||||
|
||||
**Exemple critique** :
|
||||
|
||||
- Utilisateur en voiture à 90 km/h (25 m/s)
|
||||
- ETA de 7 secondes avant le point → notification affichée
|
||||
- Latence HLS de 15 secondes
|
||||
@@ -69,6 +70,7 @@ La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées q
|
||||
```
|
||||
|
||||
**Stratégie de cache** :
|
||||
|
||||
- Télécharge les **15 premières secondes** de chaque POI à proximité
|
||||
- Limite : 3 POI simultanés en cache (max ~25 MB)
|
||||
- Purge automatique après 200m de distance passée
|
||||
@@ -152,16 +154,19 @@ Si le pre-buffer échoue (réseau faible, pas de cache), afficher un **loader av
|
||||
### Impact sur l'Infrastructure
|
||||
|
||||
#### Backend (Go)
|
||||
|
||||
- **Nouveau service** : `audiocache.Service` pour préparer les extraits M4A
|
||||
- **Endpoint** : `GET /api/v1/audio/poi/:id/intro` (retourne 15s d'audio)
|
||||
- **CDN** : Cache NGINX avec TTL 7 jours sur `/audio/*/intro.m4a`
|
||||
|
||||
#### Mobile (Flutter)
|
||||
|
||||
- **Package** : `just_audio` avec cache local (`flutter_cache_manager`)
|
||||
- **Stockage** : Max 100 MB de cache audio (auto-purge LRU)
|
||||
- **Logique** : `PreBufferService` avec scoring de priorité POI
|
||||
|
||||
#### Coûts
|
||||
|
||||
- **Bande passante** : +10-15 MB/utilisateur/session (vs streaming pur)
|
||||
- **Stockage CDN** : +500 MB pour 1000 POI × 5 MB intro (négligeable)
|
||||
- **Économie** : Cache CDN réduit les requêtes origin (-60% selon tests)
|
||||
@@ -177,6 +182,6 @@ Si le pre-buffer échoue (réseau faible, pas de cache), afficher un **loader av
|
||||
|
||||
- [HLS Authoring Specification](https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices)
|
||||
- [Low-Latency HLS (LL-HLS)](https://developer.apple.com/documentation/http_live_streaming/enabling_low-latency_hls)
|
||||
- Règle Métier 05 : Section 5.2 (Mode Voiture, lignes 16-84)
|
||||
- Règle Métier 05 : Section 5.1 (File d'attente et commande Suivant)
|
||||
- Règle Métier 17 : Section 17.2 (ETA Géolocalisé, lignes 25-65)
|
||||
- **ADR-017** : Architecture des Notifications Géolocalisées
|
||||
|
||||
@@ -44,12 +44,14 @@ flowchart LR
|
||||
## Justification
|
||||
|
||||
### PostgreSQL + PostGIS
|
||||
|
||||
- Requêtes géospatiales complexes et précises
|
||||
- Index GIST pour performance
|
||||
- ACID, fiabilité éprouvée
|
||||
- Écosystème mature
|
||||
|
||||
### PgBouncer
|
||||
|
||||
- **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL
|
||||
- **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless)
|
||||
- **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL
|
||||
@@ -57,6 +59,7 @@ flowchart LR
|
||||
- **Port** : :6432 (vs :5432 pour PostgreSQL direct)
|
||||
|
||||
### Redis
|
||||
|
||||
- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec
|
||||
- Sessions utilisateurs
|
||||
- Pub/sub pour temps réel
|
||||
@@ -81,11 +84,13 @@ LIMIT 20;
|
||||
### Configuration PgBouncer
|
||||
|
||||
**Mode recommandé** : `transaction`
|
||||
|
||||
- Connexion libérée après chaque transaction
|
||||
- Optimal pour API stateless (Go + Fiber)
|
||||
- Maximise la réutilisation des connexions
|
||||
|
||||
**Pool sizing** :
|
||||
|
||||
- `default_pool_size` : 20 (connexions par base)
|
||||
- `max_client_conn` : 1000 (connexions clients max)
|
||||
- `reserve_pool_size` : 5 (connexions de secours)
|
||||
@@ -115,8 +120,3 @@ server_idle_timeout = 600
|
||||
// Après (via PgBouncer)
|
||||
dsn := "postgres://user:pass@localhost:6432/roadwave"
|
||||
```
|
||||
|
||||
## Documentation technique détaillée
|
||||
|
||||
- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md)
|
||||
- [Schéma base de données](../architecture/database/schema.md)
|
||||
|
||||
@@ -67,11 +67,13 @@ features/
|
||||
**Exécuté par** : Backend (Godog + step definitions Go dans `/backend/tests/bdd/`)
|
||||
|
||||
**Exemples de scénarios** :
|
||||
|
||||
- `POST /api/v1/auth/register` avec email invalide → retourne 400
|
||||
- `GET /api/v1/contents/nearby` avec rayon 500m → retourne POI triés par distance
|
||||
- `DELETE /api/v1/user/account` → supprime données RGPD conformément
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Focus sur la **logique métier backend** (algorithme, validation, persistance)
|
||||
- Pas d'interface utilisateur
|
||||
- Testable via requêtes HTTP directes
|
||||
@@ -85,11 +87,13 @@ features/
|
||||
**Exécuté par** : Mobile (Flutter `integration_test` + step definitions Dart dans `/mobile/tests/bdd/`)
|
||||
|
||||
**Exemples de scénarios** :
|
||||
|
||||
- Cliquer sur "Lecture" → widget audio player s'affiche avec progress bar
|
||||
- Mode piéton activé → carte interactive affichée + bouton "Télécharger zone"
|
||||
- Scroll dans liste podcasts → lazy loading déclenche pagination
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Focus sur l'**expérience utilisateur mobile**
|
||||
- Validation visuelle (widgets, animations, navigation)
|
||||
- Mock du backend si nécessaire (tests UI isolés)
|
||||
@@ -103,11 +107,13 @@ features/
|
||||
**Exécuté par** : Backend **ET** Mobile ensemble
|
||||
|
||||
**Exemples de scénarios** :
|
||||
|
||||
- Abonnement Premium : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI
|
||||
- Erreur réseau : Perte connexion pendant streaming → Fallback mode offline → Reprise auto après reconnexion
|
||||
- Notification géolocalisée : GPS détecte POI → Backend calcule recommandation → Push notification → Ouverture app → Lecture audio
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Tests **cross-composants** (intégration complète)
|
||||
- Implique souvent des **services tiers** (Zitadel, Mangopay, Firebase)
|
||||
- Validation du **parcours utilisateur de bout en bout**
|
||||
@@ -117,10 +123,12 @@ features/
|
||||
### Implémentation Step Definitions
|
||||
|
||||
**Backend** : `/backend/tests/bdd/`
|
||||
|
||||
- Step definitions Go pour features `api/` et `e2e/`
|
||||
- Utilise `godog` et packages backend (`service`, `repository`)
|
||||
|
||||
**Mobile** : `/mobile/tests/bdd/`
|
||||
|
||||
- Step definitions Dart pour features `ui/` et `e2e/`
|
||||
- Utilise Flutter `integration_test` framework
|
||||
|
||||
|
||||
@@ -14,16 +14,18 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
|
||||
**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go.
|
||||
|
||||
**Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers)
|
||||
|
||||
- ✅ Authentification native Zitadel (email + mot de passe)
|
||||
- ❌ **Pas de fournisseurs OAuth externes** (Google, Apple, Facebook)
|
||||
- **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement)
|
||||
|
||||
**Architecture de déploiement** :
|
||||
|
||||
- Container Docker sur le même VPS OVH (Gravelines, France) que l'API
|
||||
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma)
|
||||
- Aucune donnée d'authentification ne transite par des serveurs tiers
|
||||
|
||||
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
|
||||
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../domains/_shared/rules/authentification.md#11-méthodes-dinscription)).
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
@@ -102,17 +104,19 @@ graph TB
|
||||
4. App mobile → Go API avec JWT → validation locale
|
||||
|
||||
**Ce que nous N'UTILISONS PAS** :
|
||||
|
||||
- ❌ "Sign in with Google"
|
||||
- ❌ "Sign in with Apple"
|
||||
- ❌ "Sign in with Facebook"
|
||||
- ❌ Aucun autre fournisseur externe
|
||||
|
||||
**Pourquoi OAuth2 alors ?** :
|
||||
|
||||
- OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.)
|
||||
- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password
|
||||
- Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
|
||||
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription) pour la décision métier.
|
||||
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../domains/_shared/rules/authentification.md#11-méthodes-dinscription) pour la décision métier.
|
||||
|
||||
## Exemple d'intégration
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ ON user_locations USING GIST(last_position);
|
||||
- ✅ **Maintenabilité** : Patterns clairs, réutilisables
|
||||
- ❌ **Complexité** : Une couche de plus, mais justifiée
|
||||
|
||||
**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
|
||||
**Référence** : Résout incohérence #4 dans
|
||||
|
||||
## Conséquences
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
|
||||
- **Cache images** : `cached_network_image` (LRU cache)
|
||||
|
||||
**Points d'attention** :
|
||||
|
||||
- ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions")
|
||||
- ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-020
|
||||
|
||||
@@ -53,6 +54,7 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
|
||||
### Contexte et Enjeux
|
||||
|
||||
**Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google :
|
||||
|
||||
- **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée
|
||||
- **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10
|
||||
- **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans**
|
||||
@@ -74,6 +76,7 @@ Trois niveaux de permissions doivent être gérés :
|
||||
**Quand** : Premier lancement de l'app
|
||||
|
||||
**Demande** : `locationWhenInUse` uniquement
|
||||
|
||||
- iOS : "Allow While Using App"
|
||||
- Android : `ACCESS_FINE_LOCATION`
|
||||
|
||||
@@ -136,21 +139,25 @@ Pour vous proposer du contenu audio adapté
|
||||
Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter :
|
||||
|
||||
**Détection du niveau actuel** :
|
||||
|
||||
- Vérifier le statut de la permission `location` (when in use)
|
||||
- Vérifier le statut de la permission `locationAlways` (background)
|
||||
- Retourner le niveau le plus élevé accordé
|
||||
|
||||
**Demande de permission de base** (Étape 1) :
|
||||
|
||||
- Demander uniquement la permission `location` (when in use)
|
||||
- Utilisée lors de l'onboarding
|
||||
- Aucun écran d'éducation requis
|
||||
|
||||
**Demande de permission arrière-plan** (Étape 2) :
|
||||
|
||||
- **Toujours** afficher un écran d'éducation AVANT la demande OS
|
||||
- Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android)
|
||||
- Si refusée de manière permanente, proposer l'ouverture des réglages système
|
||||
|
||||
**Gestion des refus** :
|
||||
|
||||
- Détecter si la permission est refusée de manière permanente
|
||||
- Proposer l'ouverture des réglages de l'appareil avec un message clair
|
||||
- Permettre à l'utilisateur d'annuler
|
||||
@@ -160,6 +167,7 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
|
||||
#### iOS (`ios/Runner/Info.plist`)
|
||||
|
||||
**Clés requises** :
|
||||
|
||||
- `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel)
|
||||
- `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable
|
||||
- `UIBackgroundModes` : Activer les modes `location` et `remote-notification`
|
||||
@@ -170,11 +178,13 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
|
||||
#### Android (`android/app/src/main/AndroidManifest.xml`)
|
||||
|
||||
**Permissions requises** :
|
||||
|
||||
- `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use)
|
||||
- `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store
|
||||
- `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+)
|
||||
|
||||
**Android Play Store** : Déclaration requise dans Play Console lors de la soumission :
|
||||
|
||||
- Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
|
||||
- Vidéo démo obligatoire montrant le flow de demande de permission
|
||||
|
||||
@@ -191,12 +201,14 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
|
||||
### Tests de Validation Stores
|
||||
|
||||
**Checklist App Store (iOS)** :
|
||||
|
||||
- [ ] Permission "Always" demandée **uniquement** si user active mode piéton
|
||||
- [ ] Écran d'éducation **avant** demande OS (requis iOS 13+)
|
||||
- [ ] App fonctionne sans permission "Always" (validation critique)
|
||||
- [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire)
|
||||
|
||||
**Checklist Play Store (Android)** :
|
||||
|
||||
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée
|
||||
- [ ] Vidéo démo flow de permissions (< 30s, requis Play Console)
|
||||
- [ ] App fonctionne sans permission background (validation critique)
|
||||
@@ -205,8 +217,8 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
|
||||
### Documentation Associée
|
||||
|
||||
- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
|
||||
- **Règles métier** : [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
|
||||
- **Règles métier** : [Règle 05 - Mode Piéton](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **RGPD** : [Règle 02 - Conformité RGPD](../domains/_shared/rules/rgpd.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
|
||||
## Tests BDD (Gherkin + Godog)
|
||||
|
||||
- **Framework** : `github.com/cucumber/godog`
|
||||
- **Couverture** : Tous les cas d'usage du [README.md](../../README.md) traduits en `.feature`
|
||||
- **Couverture** : Tous les cas d'usage du traduits en `.feature`
|
||||
- **Exécution** : Avant release
|
||||
- **Détails** : Voir [ADR-007](007-tests-bdd.md) pour contexte complet
|
||||
|
||||
@@ -79,6 +79,7 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
|
||||
- `github.com/cucumber/godog`
|
||||
- `github.com/testcontainers/testcontainers-go`
|
||||
- `grafana/k6` (AGPL-3.0, usage interne OK)
|
||||
|
||||
- Temps CI : ~3-5 min (tests unitaires + BDD)
|
||||
- Tests intégration/E2E : nightly builds (15-30 min)
|
||||
- Load tests : avant chaque release majeure
|
||||
|
||||
@@ -68,6 +68,7 @@ mobile/tests/bdd/inscription_steps.dart → Teste l'UI mobile
|
||||
```
|
||||
|
||||
Cela garantit que :
|
||||
|
||||
- Les spécifications métier sont uniques et cohérentes
|
||||
- Chaque couche teste sa responsabilité
|
||||
- Les tests valident le contrat entre front et back
|
||||
@@ -77,7 +78,7 @@ Cela garantit que :
|
||||
- **Turborepo** ou **Nx** : orchestration des builds/tests, cache intelligent
|
||||
- **Docker Compose** : environnement de dev local (PostgreSQL, Redis, backend, etc.)
|
||||
- **Make** : commandes communes (`make test`, `make build`, `make dev`)
|
||||
- **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](020-strategie-cicd-monorepo.md))
|
||||
- **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](022-strategie-cicd-monorepo.md))
|
||||
|
||||
## Conséquences
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ OVH Object Storage (~1.20€/100GB)
|
||||
| **Scale** | 100K+ | Scaleway Kubernetes (Kapsule) | ~500€ | Auto-scaling OU multi-région |
|
||||
|
||||
**Triggers détaillés** :
|
||||
|
||||
- **Phase 2** : CPU > 70% (7j), latence p99 > 100ms, backups > 1h/semaine, MRR > 2000€
|
||||
- **Phase 3** : Auto-scaling horizontal requis, multi-région Europe, DevOps dédié, > 5 services
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
RoadWave nécessite un service d'envoi d'emails **techniques uniquement** (pas de notifications sociales, alertes marketing, promotions).
|
||||
|
||||
**Périmètre strict** :
|
||||
|
||||
- ✅ **Authentification** : Vérification email (inscription), réinitialisation mot de passe, changement email
|
||||
- ✅ **Sécurité** : Alertes connexion inhabituelle, changement password
|
||||
- ✅ **Modération** : Strikes, suspensions, bannissements
|
||||
|
||||
@@ -14,14 +14,14 @@ ADR-002 spécifie HLS pour tout le streaming audio, mais HLS est un protocole un
|
||||
|
||||
Architecture hybride en **2 phases** :
|
||||
|
||||
### Phase 1 (MVP) : WebSocket + Firebase Cloud Messaging
|
||||
### Phase 1 (MVP) : WebSocket + APNS/FCM Direct
|
||||
|
||||
```
|
||||
[App Mobile] → [WebSocket] → [Backend Go]
|
||||
↓
|
||||
[PostGIS Worker]
|
||||
↓
|
||||
[Firebase FCM / APNS]
|
||||
[APNS / FCM Direct API]
|
||||
↓
|
||||
[Push Notification]
|
||||
```
|
||||
@@ -31,7 +31,7 @@ Architecture hybride en **2 phases** :
|
||||
2. L'app envoie sa position GPS toutes les 30s via WebSocket
|
||||
3. Un worker backend (goroutine) interroge PostGIS toutes les 30s :
|
||||
```sql
|
||||
SELECT poi.*, users.fcm_token
|
||||
SELECT poi.*, users.push_token, users.platform
|
||||
FROM points_of_interest poi
|
||||
JOIN user_locations users ON ST_DWithin(
|
||||
poi.geom,
|
||||
@@ -41,10 +41,11 @@ Architecture hybride en **2 phases** :
|
||||
WHERE users.notifications_enabled = true
|
||||
AND users.last_update > NOW() - INTERVAL '5 minutes'
|
||||
```
|
||||
4. Si proximité détectée → envoi de push notification via Firebase (Android) ou APNS (iOS)
|
||||
4. Si proximité détectée → envoi de push notification via FCM (Android) ou APNS (iOS)
|
||||
5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002)
|
||||
|
||||
**Limitations MVP** :
|
||||
|
||||
- Fonctionne uniquement si l'utilisateur a envoyé sa position < 5 minutes
|
||||
- En voiture rapide (>80 km/h), possible de "manquer" un POI si position pas mise à jour
|
||||
|
||||
@@ -78,11 +79,11 @@ Architecture hybride en **2 phases** :
|
||||
|
||||
| Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict |
|
||||
|----------|-----------|----------|-----------------|-------------|----------------|---------|
|
||||
| **Firebase (choix)** | 99.95% | **0€** | **0€** | ❌ Non | 🔴 Fort (Google) | ✅ Optimal MVP |
|
||||
| **APNS/FCM Direct (choix)** | 99.95% | **0€** | **0€** | ✅ Oui | 🟢 Aucun | ✅ Optimal |
|
||||
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
|
||||
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
|
||||
| Custom WS + APNS/FCM | Votre charge | 5€ | 100€+ | ✅ Oui | 🟢 Aucun | ⚠️ Complexe |
|
||||
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | 🟡 Phase 2 |
|
||||
| Firebase SDK | 99.95% | 0€ | 0€ | ❌ Non | 🔴 Fort (Google) | ❌ Vendor lock-in |
|
||||
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | ❌ Overhead inutile |
|
||||
| Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement |
|
||||
|
||||
## Justification
|
||||
@@ -93,43 +94,43 @@ Architecture hybride en **2 phases** :
|
||||
- **Batterie** : Connexion persistante optimisée par l'OS mobile
|
||||
- **Bi-directionnel** : Backend peut envoyer des mises à jour instantanées (ex: "nouveau POI créé par un créateur que tu suis")
|
||||
|
||||
### Pourquoi Firebase FCM et pas implémentation custom ?
|
||||
|
||||
- **Gratuit** : 10M notifications/mois (largement suffisant jusqu'à 100K utilisateurs)
|
||||
- **Fiabilité** : Infrastructure Google avec 99.95% uptime
|
||||
- **Batterie** : Utilise les mécanismes système (Google Play Services)
|
||||
- **Cross-platform** : API unifiée iOS/Android
|
||||
|
||||
### Incohérence acceptée : Firebase vs self-hosted (ADR-008, ADR-015)
|
||||
|
||||
**Problème** : RoadWave promeut 100% self-hosted + souveraineté française, mais Firebase = dépendance Google Cloud.
|
||||
### Pourquoi implémentation directe APNS/FCM et pas SDK Firebase ?
|
||||
|
||||
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
|
||||
|
||||
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
|
||||
- **FCM (Google)** : Meilleur protocole Android (vs Huawei HMS, Samsung)
|
||||
- **FCM (Google)** : Protocole standard Android (Google Play Services)
|
||||
|
||||
**Alternatives analysées** :
|
||||
1. **Custom WebSocket** (self-hosted) :
|
||||
- ✅ Zéro dépendance externe
|
||||
- ❌ 150+ heures dev (2-3 sprints)
|
||||
- ❌ Maintien de la reliability en-house
|
||||
- ❌ Toujours besoin d'appeler APNS/FCM de toute façon
|
||||
**Implémentation directe choisie** :
|
||||
|
||||
2. **Novu (open source self-hosted)** :
|
||||
- ✅ Self-hostable
|
||||
- ❌ Jeune (moins mature)
|
||||
- ❌ Toujours wrapper autour APNS/FCM
|
||||
- **Gratuit** : APNS et FCM sont gratuits (pas de limite de volume)
|
||||
- **Self-hosted** : Code backend 100% maîtrisé, pas de dépendance SDK tiers
|
||||
- **Fiabilité** : Infrastructure Apple/Google avec 99.95% uptime
|
||||
- **Batterie** : Utilise les mécanismes système natifs
|
||||
- **Souveraineté** : Aucun vendor lock-in, appels directs aux APIs
|
||||
- **Simplicité** : HTTP/2 pour APNS, HTTP pour FCM
|
||||
|
||||
**Alternatives rejetées** :
|
||||
1. **Firebase SDK** :
|
||||
- ❌ Vendor lock-in Google
|
||||
- ❌ Dépendance SDK externe
|
||||
- ❌ Contradictoire avec ADR-008 (self-hosted) et ADR-015 (souveraineté)
|
||||
- ⚠️ Pas d'avantage technique par rapport aux APIs directes
|
||||
|
||||
2. **OneSignal / Pusher** :
|
||||
- ❌ Vendor lock-in + coût élevé (500€+/mois @ 100K users)
|
||||
- ❌ Abstraction inutile par-dessus APNS/FCM
|
||||
|
||||
3. **Novu (open source)** :
|
||||
- ❌ Overhead sans gain réel
|
||||
- ❌ Toujours wrapper autour APNS/FCM
|
||||
|
||||
3. **OneSignal / Pusher** :
|
||||
- ❌ Même vendor lock-in que Firebase
|
||||
- ❌ Plus cher (500€+/mois @ 100K users)
|
||||
**Décision technique** :
|
||||
|
||||
**Décision pragmatique** :
|
||||
- Firebase pour MVP : gratuit + fiabilité + time-to-market
|
||||
- **Mitigation vendor lock-in** : Utiliser abstraction layer (`NotificationProvider` interface)
|
||||
- **Exit path documenté** : Migration vers custom solution < 1 sprint si besoin futur
|
||||
- **Probabilité de changement** : Très basse (MVP gratuit, pas d'incitation financière)
|
||||
- Implémentation directe APNS/FCM dès le MVP
|
||||
- **Cohérence ADR** : Respecte ADR-008 (self-hosted) et ADR-015 (souveraineté française)
|
||||
- **Abstraction layer** : Interface `NotificationProvider` pour faciliter maintenance
|
||||
- **Complexité** : Gestion des certificats APNS + JWT FCM (standard backend)
|
||||
|
||||
### Pourquoi limiter le geofencing local à Phase 2 ?
|
||||
|
||||
@@ -150,12 +151,14 @@ Architecture hybride en **2 phases** :
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-015 (souveraineté FR)
|
||||
- Mitigé par abstraction layer (`NotificationProvider` interface) → swap facile si besoin
|
||||
- Exit path documenté pour migration custom (< 1 sprint)
|
||||
- ⚠️ **Données utilisateur chez Google** : Tokens FCM, timestamps notifications
|
||||
- Risque RGPD : Nécessite DPA Google valide
|
||||
- À consulter avec DPO avant déploiement production
|
||||
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel + configuration
|
||||
- Mitigé par scripts automation (certificats auto-renouvelés)
|
||||
- Documentation complète du processus
|
||||
|
||||
- ⚠️ **Tokens push sensibles** : Tokens FCM/APNS stockés côté backend
|
||||
- Chiffrement tokens en base (conformité RGPD)
|
||||
- Rotation automatique des tokens expirés
|
||||
|
||||
- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%)
|
||||
- ❌ Mode offline non disponible au MVP (déception possible des early adopters)
|
||||
|
||||
@@ -164,58 +167,100 @@ Architecture hybride en **2 phases** :
|
||||
- **ADR-002 (Streaming)** : Aucun conflit - HLS reste pour l'audio
|
||||
- **ADR-005 (Base de données)** : Ajouter index PostGIS `GIST (geom)` sur `points_of_interest`
|
||||
- **ADR-010 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
|
||||
- **ADR-010 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions
|
||||
- **ADR-010 (Frontend Mobile)** : Intégrer plugins APNS/FCM natifs (Flutter) et gérer permissions
|
||||
|
||||
## Abstraction Layer (Mitigation Vendor Lock-in)
|
||||
## Abstraction Layer (Maintenabilité)
|
||||
|
||||
Pour minimiser le coût de changement future, implémenter une interface abstraite :
|
||||
Implémentation d'une interface abstraite pour gérer APNS et FCM de manière unifiée :
|
||||
|
||||
```go
|
||||
// backend/internal/notification/provider.go
|
||||
type NotificationProvider interface {
|
||||
SendNotification(ctx context.Context, token, title, body, deepLink string) error
|
||||
UpdateToken(ctx context.Context, userID, newToken string) error
|
||||
SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error
|
||||
UpdateToken(ctx context.Context, userID, platform, newToken string) error
|
||||
}
|
||||
|
||||
// backend/internal/notification/firebase_provider.go
|
||||
type FirebaseProvider struct {
|
||||
client *messaging.Client
|
||||
// backend/internal/notification/apns_provider.go
|
||||
type APNSProvider struct {
|
||||
client *apns2.Client
|
||||
bundleID string
|
||||
}
|
||||
|
||||
func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
|
||||
message := &messaging.Message{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"deepLink": deepLink,
|
||||
},
|
||||
Token: token,
|
||||
func (p *APNSProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
||||
if platform != "ios" {
|
||||
return nil // Not applicable
|
||||
}
|
||||
_, err := p.client.Send(ctx, message)
|
||||
|
||||
notification := &apns2.Notification{
|
||||
DeviceToken: token,
|
||||
Topic: p.bundleID,
|
||||
Payload: payload.NewPayload().
|
||||
AlertTitle(title).
|
||||
AlertBody(body).
|
||||
Custom("deepLink", deepLink),
|
||||
}
|
||||
res, err := p.client.Push(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("APNS error: %s", res.Reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// backend/internal/notification/fcm_provider.go
|
||||
type FCMProvider struct {
|
||||
projectID string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (p *FCMProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
||||
if platform != "android" {
|
||||
return nil // Not applicable
|
||||
}
|
||||
|
||||
message := map[string]interface{}{
|
||||
"message": map[string]interface{}{
|
||||
"token": token,
|
||||
"notification": map[string]string{
|
||||
"title": title,
|
||||
"body": body,
|
||||
},
|
||||
"data": map[string]string{
|
||||
"deepLink": deepLink,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Call FCM HTTP v1 API
|
||||
url := fmt.Sprintf("https://fcm.googleapis.com/v1/projects/%s/messages:send", p.projectID)
|
||||
// ... HTTP POST with OAuth2 token
|
||||
return err
|
||||
}
|
||||
|
||||
// backend/internal/notification/service.go
|
||||
type NotificationService struct {
|
||||
provider NotificationProvider // ← Interface, pas concrète
|
||||
repo NotificationRepository
|
||||
apnsProvider NotificationProvider
|
||||
fcmProvider NotificationProvider
|
||||
repo NotificationRepository
|
||||
}
|
||||
|
||||
func (s *NotificationService) SendPush(ctx context.Context, userID, title, body, deepLink string) error {
|
||||
user, err := s.repo.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Route to appropriate provider based on platform
|
||||
if user.Platform == "ios" {
|
||||
return s.apnsProvider.SendNotification(ctx, "ios", user.PushToken, title, body, deepLink)
|
||||
}
|
||||
return s.fcmProvider.SendNotification(ctx, "android", user.PushToken, title, body, deepLink)
|
||||
}
|
||||
```
|
||||
|
||||
**Bénéfice** : Swap Firebase → Custom/Novu sans changer business logic.
|
||||
|
||||
```go
|
||||
// Futur : switch facilement
|
||||
var provider NotificationProvider
|
||||
|
||||
if config.Provider == "firebase" {
|
||||
provider = &FirebaseProvider{...}
|
||||
} else if config.Provider == "custom" {
|
||||
provider = &CustomProvider{...}
|
||||
}
|
||||
```
|
||||
**Bénéfice** : Code modulaire, testable, et facile à maintenir. Ajout futur de providers alternatifs simple.
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
@@ -229,8 +274,9 @@ if config.Provider == "firebase" {
|
||||
### Phase 1 (MVP - Sprint 3-4)
|
||||
1. Backend : Implémenter WebSocket endpoint `/ws/location`
|
||||
2. Backend : Worker PostGIS avec requête ST_DWithin
|
||||
3. Mobile : Intégrer Firebase SDK + gestion FCM token
|
||||
4. Test : Validation en conditions réelles (Paris, 10 testeurs)
|
||||
3. Backend : Configuration APNS (certificats .p8) + FCM (OAuth2)
|
||||
4. Mobile : Intégrer plugins natifs APNS/FCM + gestion push tokens
|
||||
5. Test : Validation en conditions réelles (Paris, 10 testeurs)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 8-10)
|
||||
1. Mobile : Implémenter geofencing avec `flutter_background_geolocation`
|
||||
@@ -240,7 +286,8 @@ if config.Provider == "firebase" {
|
||||
|
||||
## Références
|
||||
|
||||
- [Firebase Cloud Messaging Documentation](https://firebase.google.com/docs/cloud-messaging)
|
||||
- [Apple Push Notification Service (APNS) Documentation](https://developer.apple.com/documentation/usernotifications)
|
||||
- [Firebase Cloud Messaging HTTP v1 API](https://firebase.google.com/docs/cloud-messaging/http-server-ref)
|
||||
- [PostGIS ST_DWithin Performance](https://postgis.net/docs/ST_DWithin.html)
|
||||
- [iOS Geofencing Best Practices](https://developer.apple.com/documentation/corelocation/monitoring_the_user_s_proximity_to_geographic_regions)
|
||||
- Règle Métier 05 : Section 5.1.2 (Mode Piéton, lignes 86-120)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
## Contexte
|
||||
|
||||
Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier :
|
||||
|
||||
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
|
||||
- **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms)
|
||||
- **Maturité** et maintenance active
|
||||
@@ -41,7 +42,8 @@ Utilisation de **16 librairies open-source** avec licences permissives.
|
||||
| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) |
|
||||
| **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
|
||||
| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-017) |
|
||||
| **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (ADR-017) |
|
||||
| **APNS Push** | `sideshow/apns2` | MIT | Client APNS HTTP/2 natif (ADR-017) |
|
||||
| **FCM Push** | `golang.org/x/oauth2` + HTTP | BSD-3 | FCM HTTP v1 API directe (ADR-017) |
|
||||
| **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
|
||||
|
||||
### Utilitaires
|
||||
@@ -53,7 +55,8 @@ Utilisation de **16 librairies open-source** avec licences permissives.
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
|
||||
Voir pour comparatifs complets :
|
||||
|
||||
- Framework : Fiber vs Gin vs Echo vs Chi
|
||||
- PostgreSQL : pgx vs GORM vs database/sql
|
||||
- Redis : rueidis vs go-redis vs redigo
|
||||
@@ -63,17 +66,20 @@ Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complet
|
||||
## Justification
|
||||
|
||||
### Licences
|
||||
|
||||
- **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives)
|
||||
- **1/16** : AGPL-3.0 (k6 load testing, OK usage interne)
|
||||
- **Compatibilité totale** : Aucun conflit de licence
|
||||
|
||||
### Performance
|
||||
|
||||
- **Fiber** : 36K RPS (5% plus rapide que Gin/Echo)
|
||||
- **pgx** : 30-50% plus rapide que GORM
|
||||
- **rueidis** : Client-side caching automatique
|
||||
- **zerolog** : Zero allocation, benchmarks 2025
|
||||
|
||||
### Maturité
|
||||
|
||||
- **Standards** : testify (27% adoption), golang-migrate, viper
|
||||
- **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars)
|
||||
- **Maintenance** : Toutes actives (commits 2025-2026)
|
||||
@@ -81,14 +87,16 @@ Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complet
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ Aucune restriction licence commerciale
|
||||
- ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019)
|
||||
- ✅ Performance validée (benchmarks publics)
|
||||
- ✅ Écosystème mature et documenté
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
|
||||
- ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-017)
|
||||
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel, configuration manuelle
|
||||
- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
|
||||
|
||||
### Dépendances go.mod
|
||||
@@ -106,7 +114,8 @@ require (
|
||||
github.com/zitadel/zitadel-go/v3 latest
|
||||
github.com/pion/webrtc/v4 latest
|
||||
github.com/coder/websocket latest
|
||||
firebase.google.com/go/v4 latest
|
||||
github.com/sideshow/apns2 latest
|
||||
golang.org/x/oauth2 latest // For FCM authentication
|
||||
github.com/asticode/go-astiav latest
|
||||
github.com/spf13/viper latest
|
||||
github.com/rs/zerolog latest
|
||||
@@ -116,7 +125,7 @@ require (
|
||||
|
||||
## Références
|
||||
|
||||
- [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
|
||||
- (tableaux comparatifs, sources)
|
||||
- ADR-001 : Langage Backend (Fiber, pgx, go-redis)
|
||||
- ADR-007 : Tests BDD (Godog)
|
||||
- ADR-011 : Accès données (sqlc)
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS.
|
||||
|
||||
**Évolution du marché** :
|
||||
|
||||
- **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale)
|
||||
- **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà
|
||||
|
||||
**Usage RoadWave** :
|
||||
|
||||
- Mode dégradé : ~10% des utilisateurs (estimation)
|
||||
- Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP)
|
||||
- Critère : Aucune dépendance à un service tiers payant
|
||||
@@ -33,6 +35,7 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
|
||||
### IP2Location Lite (choix retenu)
|
||||
|
||||
**Avantages** :
|
||||
|
||||
- Gratuit (pas de limite de requêtes)
|
||||
- Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md))
|
||||
- Base de données SQLite légère (50-100 MB)
|
||||
@@ -41,12 +44,14 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
|
||||
- Pas de compte tiers requis
|
||||
|
||||
**Inconvénients** :
|
||||
|
||||
- Maintenance mensuelle (mise à jour DB)
|
||||
- Précision équivalente à MaxMind (~±50 km)
|
||||
|
||||
### MaxMind GeoLite2 API (rejeté)
|
||||
|
||||
**Pourquoi rejeté** :
|
||||
|
||||
- Coût potentiel en cas de dépassement quota (risque faible mais existant)
|
||||
- Dépendance à un service tiers (perte de souveraineté)
|
||||
- Compte requis (friction opérationnelle)
|
||||
@@ -54,6 +59,7 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
|
||||
### Self-hosted MaxMind (rejeté)
|
||||
|
||||
**Pourquoi rejeté** :
|
||||
|
||||
- Compte MaxMind obligatoire pour télécharger la DB (friction)
|
||||
- Complexité identique à IP2Location pour résultat équivalent
|
||||
- IP2Location offre même fonctionnalité sans compte tiers
|
||||
@@ -84,28 +90,33 @@ flowchart TD
|
||||
### Maintenance
|
||||
|
||||
**Mise à jour mensuelle** :
|
||||
|
||||
- Cron job télécharge nouvelle DB IP2Location (1er du mois)
|
||||
- Backup DB actuelle avant remplacement
|
||||
- Rechargement service GeoIP (hot reload sans downtime)
|
||||
|
||||
**Monitoring** :
|
||||
|
||||
- Alertes si DB > 60 jours (DB obsolète)
|
||||
- Logs requêtes "IP non trouvée" (détection problèmes DB)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- Aucun coût récurrent (gratuit à l'infini)
|
||||
- Souveraineté complète des données (cohérence ADR-004)
|
||||
- Pas de dépendance externe (service tiers)
|
||||
- Latence minimale (lookup local SQLite < 1ms)
|
||||
|
||||
### Négatives
|
||||
|
||||
- Maintenance mensuelle requise (automatisable)
|
||||
- Précision limitée (±50 km, acceptable pour mode dégradé)
|
||||
- Taille base de données (~50-100 MB sur disque)
|
||||
|
||||
### Risques atténués
|
||||
|
||||
- **DB obsolète** : Alertes automatiques si > 60 jours
|
||||
- **IP non trouvée** : Fallback "France" par défaut (code pays FR)
|
||||
- **Perte DB** : Backup automatique avant chaque mise à jour
|
||||
@@ -114,5 +125,5 @@ flowchart TD
|
||||
|
||||
- [ADR-004 : CDN (Souveraineté)](004-cdn.md)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md)
|
||||
- [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
|
||||
- [Règle 02 : RGPD (Mode Dégradé)](../domains/_shared/rules/rgpd.md#136-géolocalisation-optionnelle)
|
||||
- IP2Location Lite : https://lite.ip2location.com/
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
## Contexte
|
||||
|
||||
L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier :
|
||||
|
||||
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
|
||||
- **Maturité** et maintenance active (écosystème Flutter)
|
||||
- **Performance native** (pas de bridge JS)
|
||||
@@ -14,9 +15,9 @@ L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces po
|
||||
|
||||
## Décision
|
||||
|
||||
Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
Utilisation de **9 librairies open-source** Flutter avec licences permissives, déployées en 2 phases selon la stratégie définie dans [ADR-017 (Notifications Géolocalisées)](017-notifications-geolocalisees.md).
|
||||
|
||||
### Core Stack
|
||||
### Phase 1 (MVP) : Core Stack
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
@@ -26,63 +27,93 @@ Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
|
||||
| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
|
||||
|
||||
### Géolocalisation & Permissions
|
||||
### Phase 1 (MVP) : Géolocalisation & Notifications
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
|
||||
| **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
|
||||
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, WebSocket position updates, high accuracy |
|
||||
| **Push APNS/FCM** | `flutter_apns` + `flutter_fcm` | MIT | Intégration native APNS et FCM directe (ADR-017) |
|
||||
| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
|
||||
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
|
||||
|
||||
### Packages Additionnels (CarPlay/Android Auto)
|
||||
### Phase 1 (MVP) : CarPlay/Android Auto (optionnel)
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
|
||||
| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (communautaire) |
|
||||
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
|
||||
|
||||
### Phase 2 (Post-MVP) : Geofencing Local
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification | Voir ADR |
|
||||
|-----------|-----------|---------|---------------|----------|
|
||||
| **Geofencing local** | `geofence_service` | MIT | Mode offline, détection rayon 200m, notifications app fermée | [ADR-017](017-notifications-geolocalisees.md) Phase 2 |
|
||||
|
||||
**Note importante** : Le geofencing local (`geofence_service`) n'est **PAS utilisé en MVP**. La Phase 1 utilise WebSocket + Firebase pour les notifications de proximité (voir [ADR-017](017-notifications-geolocalisees.md) pour l'architecture complète).
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### State Management
|
||||
|
||||
- **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive
|
||||
- **riverpod** : Plus moderne, moins mature
|
||||
- **provider** : Simple mais limité pour app complexe
|
||||
- **getx** : Performance mais opinions controversées
|
||||
|
||||
### Audio
|
||||
|
||||
- **just_audio** (choisi) : HLS natif, communauté active
|
||||
- **audioplayers** : Moins mature pour streaming
|
||||
- **flutter_sound** : Orienté recording, pas streaming
|
||||
|
||||
### Géolocalisation
|
||||
|
||||
- **geolocator** (choisi) : Standard Flutter, 1.2K+ stars
|
||||
- **location** : Moins maintenu
|
||||
- **background_location** : Spécifique background uniquement
|
||||
|
||||
### Notifications Push
|
||||
|
||||
- **flutter_apns + flutter_fcm** (choisi) : Implémentation directe APNS/FCM, pas de vendor lock-in
|
||||
- **firebase_messaging** : SDK Firebase, vendor lock-in Google
|
||||
- **OneSignal** : Plus cher (500€/mois @ 100K users), vendor lock-in
|
||||
- **Custom WebSocket** : Complexe, toujours besoin APNS/FCM au final (voir ADR-017)
|
||||
|
||||
### Geofencing (Phase 2)
|
||||
|
||||
- **geofence_service** (choisi) : Natif iOS/Android, économie batterie optimale
|
||||
- **background_geolocation** : Payant (149$/an par app)
|
||||
- **flutter_background_location** : Moins mature
|
||||
|
||||
## Justification
|
||||
|
||||
### Licences
|
||||
- **7/8 librairies** : MIT (permissive totale)
|
||||
- **1/8** : BSD-3 (permissive, compatible commercial)
|
||||
|
||||
- **7/9 librairies** : MIT (permissive totale)
|
||||
- **2/9** : BSD-3 (permissive, compatible commercial)
|
||||
- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
|
||||
|
||||
### Maturité
|
||||
|
||||
- **flutter_bloc** : 11.6K stars, adoption large (state management standard)
|
||||
- **just_audio** : 900+ stars, utilisé production (podcasts apps)
|
||||
- **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter)
|
||||
- **dio** : 12K+ stars, client HTTP le plus utilisé Flutter
|
||||
|
||||
### Performance
|
||||
|
||||
- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
|
||||
- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
|
||||
- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
|
||||
- **geofence_service** : Geofencing natif, minimise consommation batterie
|
||||
- **flutter_apns + flutter_fcm** : Utilise services systèmes natifs (APNS, Google Play Services)
|
||||
- **geofence_service** (Phase 2) : Geofencing natif, minimise consommation batterie
|
||||
|
||||
### Conformité Stores
|
||||
|
||||
- **Permissions progressives** : `permission_handler` + stratégie ADR-010
|
||||
- **Background modes** : `geolocator` + `geofence_service` approuvés stores
|
||||
- **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
|
||||
- **Background modes MVP** : `geolocator` (When In Use) + `firebase_messaging` approuvés stores
|
||||
- **Background modes Phase 2** : `geofence_service` nécessite permission "Always" (taux acceptation ~30%)
|
||||
- **Notifications** : `flutter_local_notifications` + `firebase_messaging` conformes guidelines iOS/Android
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -99,14 +130,18 @@ graph TB
|
||||
Cache["cached_network_image<br/>(Image Cache)"]
|
||||
end
|
||||
|
||||
subgraph Services["Services Layer"]
|
||||
subgraph Services["Services Layer - Phase 1 MVP"]
|
||||
Audio["just_audio<br/>(HLS Streaming)"]
|
||||
GPS["geolocator<br/>(GPS Mode Voiture)"]
|
||||
Geofence["geofence_service<br/>(Mode Piéton)"]
|
||||
Notif["flutter_local_notifications<br/>(Alerts Locales)"]
|
||||
GPS["geolocator<br/>(GPS + WebSocket)"]
|
||||
Push["flutter_apns + flutter_fcm<br/>(Push Natifs APNS/FCM)"]
|
||||
Notif["flutter_local_notifications<br/>(Notifications Locales)"]
|
||||
Perms["permission_handler<br/>(Permissions iOS/Android)"]
|
||||
end
|
||||
|
||||
subgraph Phase2["Services Layer - Phase 2"]
|
||||
Geofence["geofence_service<br/>(Mode Offline)"]
|
||||
end
|
||||
|
||||
subgraph Platform["Platform Integration"]
|
||||
CarPlay["flutter_carplay<br/>(iOS)"]
|
||||
AndroidAuto["android_auto_flutter<br/>(Android)"]
|
||||
@@ -116,14 +151,17 @@ graph TB
|
||||
Bloc --> API
|
||||
Bloc --> Audio
|
||||
Bloc --> GPS
|
||||
Bloc --> Geofence
|
||||
Bloc --> Push
|
||||
|
||||
API --> Storage
|
||||
Widgets --> Cache
|
||||
|
||||
GPS --> Perms
|
||||
Geofence --> Perms
|
||||
Geofence --> Notif
|
||||
Push --> Perms
|
||||
Push --> Notif
|
||||
|
||||
Geofence -.->|Phase 2| Perms
|
||||
Geofence -.->|Phase 2| Notif
|
||||
|
||||
Audio --> CarPlay
|
||||
Audio --> AndroidAuto
|
||||
@@ -135,13 +173,15 @@ graph TB
|
||||
|
||||
class UI,Widgets,Bloc uiStyle
|
||||
class Data,API,Storage,Cache dataStyle
|
||||
class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
|
||||
class Services,Audio,GPS,FCM,Notif,Perms serviceStyle
|
||||
class Phase2,Geofence serviceStyle
|
||||
class Platform,CarPlay,AndroidAuto platformStyle
|
||||
```
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ Aucune restriction licence commerciale (100% permissif)
|
||||
- ✅ Stack cohérent avec ADR-010 (Frontend Mobile)
|
||||
- ✅ Performance native (compilation ARM64 directe)
|
||||
@@ -150,8 +190,10 @@ graph TB
|
||||
- ✅ Conformité stores (permissions progressives)
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
|
||||
- ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-010)
|
||||
- ⚠️ **Configuration APNS/FCM** : Gestion certificats et OAuth2, configuration manuelle
|
||||
- ⚠️ **Permission "Always" Phase 2** : Taux acceptation ~30% (geofencing local)
|
||||
- ❌ **Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
|
||||
- ❌ **Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
|
||||
|
||||
@@ -159,49 +201,65 @@ graph TB
|
||||
|
||||
> **Note** : Les versions exactes seront définies lors de l'implémentation. Cette section indique les packages requis, non les versions à utiliser (qui évoluent rapidement dans l'écosystème Flutter).
|
||||
|
||||
**Core** :
|
||||
**Core (Phase 1 MVP)** :
|
||||
|
||||
- `flutter_bloc` - State management
|
||||
- `just_audio` - Audio HLS streaming
|
||||
- `dio` - HTTP client
|
||||
- `flutter_secure_storage` - Stockage sécurisé JWT
|
||||
- `cached_network_image` - Cache images
|
||||
|
||||
**Géolocalisation & Notifications** :
|
||||
- `geolocator` - GPS haute précision
|
||||
- `geofence_service` - Geofencing arrière-plan
|
||||
**Géolocalisation & Notifications (Phase 1 MVP)** :
|
||||
|
||||
- `geolocator` - GPS haute précision, WebSocket position updates
|
||||
- `flutter_apns` - Push notifications APNS natif iOS (ADR-017)
|
||||
- `flutter_fcm` - Push notifications FCM natif Android (ADR-017)
|
||||
- `flutter_local_notifications` - Notifications locales
|
||||
- `permission_handler` - Gestion permissions
|
||||
|
||||
**CarPlay/Android Auto** (optionnels MVP) :
|
||||
**CarPlay/Android Auto (optionnels Phase 1)** :
|
||||
|
||||
- `flutter_carplay` - Intégration CarPlay
|
||||
- `android_auto_flutter` - Support Android Auto
|
||||
|
||||
**Geofencing (Phase 2 Post-MVP)** :
|
||||
|
||||
- `geofence_service` - Geofencing local pour mode offline (ADR-017 Phase 2)
|
||||
|
||||
### Migration depuis ADR-010
|
||||
|
||||
La section "Packages clés" de l'ADR-010 est désormais obsolète et doit référencer cet ADR :
|
||||
|
||||
> **Packages Flutter** : Voir [ADR-018 - Librairies Flutter](018-librairies-flutter.md) pour la liste complète, licences et justifications.
|
||||
> **Packages Flutter** : Voir [ADR-018 - Librairies Flutter](020-librairies-flutter.md) pour la liste complète, licences et justifications.
|
||||
|
||||
## Risques et Mitigations
|
||||
|
||||
### Risque 1 : CarPlay/Android Auto packages communautaires
|
||||
|
||||
- **Impact** : Maintenance non garantie par Flutter team
|
||||
- **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique
|
||||
|
||||
### Risque 2 : Validation App Store (permissions background)
|
||||
|
||||
- **Impact** : Taux de rejet ~70% si mal justifié
|
||||
- **Mitigation** : Stratégie progressive (ADR-010), écrans d'éducation, tests beta TestFlight
|
||||
- **Mitigation Phase 1** : Permission "When In Use" seulement (MVP), moins scrutée par Apple
|
||||
- **Mitigation Phase 2** : Stratégie progressive (ADR-010), écrans d'éducation, tests beta TestFlight pour permission "Always"
|
||||
|
||||
### Risque 3 : Performance audio HLS en arrière-plan
|
||||
|
||||
- **Impact** : Interruptions si OS tue l'app
|
||||
- **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`)
|
||||
|
||||
## Références
|
||||
|
||||
- ADR-010 : Frontend Mobile (Flutter, architecture permissions)
|
||||
- ADR-018 : Librairies Go (même format de documentation)
|
||||
- [ADR-010 : Frontend Mobile](012-frontend-mobile.md) (Flutter, architecture permissions)
|
||||
- [ADR-017 : Notifications Géolocalisées](017-notifications-geolocalisees.md) (Phase 1 WebSocket vs Phase 2 Geofencing)
|
||||
- [ADR-018 : Librairies Go](018-librairies-go.md) (même format de documentation)
|
||||
- [flutter_bloc documentation](https://bloclibrary.dev/)
|
||||
- [just_audio repository](https://pub.dev/packages/just_audio)
|
||||
- [geolocator documentation](https://pub.dev/packages/geolocator)
|
||||
- [flutter_apns documentation](https://pub.dev/packages/flutter_apns)
|
||||
- [flutter_fcm documentation](https://pub.dev/packages/flutter_fcm)
|
||||
- [geofence_service documentation](https://pub.dev/packages/geofence_service)
|
||||
- [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
|
||||
- [Android Auto Developer Guide](https://developer.android.com/training/cars)
|
||||
|
||||
@@ -14,6 +14,7 @@ L'application nécessite un système de cache performant pour plusieurs cas d'us
|
||||
- **Rate limiting** : Protection contre les abus API
|
||||
|
||||
Les contraintes de performance sont strictes :
|
||||
|
||||
- Latence p99 < 5ms pour les requêtes de cache
|
||||
- Support de 100K+ requêtes/seconde en lecture
|
||||
- Persistance optionnelle (données non critiques)
|
||||
@@ -24,6 +25,7 @@ Les contraintes de performance sont strictes :
|
||||
**Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale.
|
||||
|
||||
Configuration :
|
||||
|
||||
- Mode Cluster avec 3 nœuds minimum (haute disponibilité)
|
||||
- Persistence RDB désactivée pour les caches chauds (performance maximale)
|
||||
- AOF activé uniquement pour les sessions utilisateurs (durabilité)
|
||||
@@ -76,6 +78,7 @@ Ces commandes permettent de servir les requêtes de proximité directement depui
|
||||
### Écosystème Go
|
||||
|
||||
Librairie `go-redis/redis` (13K+ stars GitHub) :
|
||||
|
||||
- Support complet Redis Cluster
|
||||
- Pipeline et transactions
|
||||
- Context-aware (intégration Go idiomatique)
|
||||
@@ -84,6 +87,7 @@ Librairie `go-redis/redis` (13K+ stars GitHub) :
|
||||
### Pub/Sub pour temps réel
|
||||
|
||||
Support natif de messaging publish/subscribe pour :
|
||||
|
||||
- Notifications push (invalidation de cache)
|
||||
- Événements temps réel (nouveau contenu géolocalisé)
|
||||
- Coordination entre instances API (scaling horizontal)
|
||||
@@ -106,12 +110,14 @@ Support natif de messaging publish/subscribe pour :
|
||||
### Stratégie de cache
|
||||
|
||||
**TTL par type de donnée** :
|
||||
|
||||
- Métadonnées de contenu : 15 minutes (mise à jour rare)
|
||||
- Résultats géolocalisés : 5 minutes (contenus statiques géographiquement)
|
||||
- Sessions utilisateurs : 24 heures (renouvellement automatique)
|
||||
- Rate limiting : 1 minute (fenêtre glissante)
|
||||
|
||||
**Invalidation** :
|
||||
|
||||
- Publication de contenu → `DEL` métadonnées + publication Pub/Sub
|
||||
- Modification géolocalisation → `GEOREM` puis `GEOADD`
|
||||
- Logout utilisateur → `DEL` session
|
||||
@@ -119,21 +125,25 @@ Support natif de messaging publish/subscribe pour :
|
||||
### Configuration production
|
||||
|
||||
**Cluster 3 nœuds** (minimum haute disponibilité) :
|
||||
|
||||
- 1 master + 2 replicas
|
||||
- Répartition sur 3 zones de disponibilité (anti-affinité)
|
||||
- `cluster-require-full-coverage no` → lecture dégradée si nœud down
|
||||
|
||||
**Mémoire** :
|
||||
|
||||
- `maxmemory 2gb` par nœud (ajustable selon charge)
|
||||
- `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés
|
||||
|
||||
**Persistance** :
|
||||
|
||||
- RDB désactivé (`save ""`) pour caches chauds
|
||||
- AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel)
|
||||
|
||||
### Monitoring
|
||||
|
||||
Métriques critiques à suivre :
|
||||
|
||||
- Taux de hit/miss par namespace (target >95% hit rate)
|
||||
- Latence p99 par commande (alerter si >10ms)
|
||||
- Fragmentation mémoire (rebalance si >1.5)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documentation et features BDD ([ADR-014](014-organisation-monorepo.md)). Sans optimisation, chaque commit déclencherait **tous** les builds (backend + mobile + docs), même si seul un composant a changé.
|
||||
|
||||
**Problématique** :
|
||||
|
||||
- ❌ Temps de CI/CD inutilement longs (rebuild complet ~15 min)
|
||||
- ❌ Gaspillage de ressources GitHub Actions
|
||||
- ❌ Ralentissement du feedback développeur
|
||||
@@ -43,203 +44,71 @@ RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documen
|
||||
|
||||
#### Workflow Backend (`backend.yml`)
|
||||
|
||||
```yaml
|
||||
name: Backend CI
|
||||
**Déclencheurs** :
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'backend/**' # Code Go modifié
|
||||
- 'features/api/**' # Tests API modifiés
|
||||
- 'features/e2e/**' # Tests E2E (impliquent backend)
|
||||
- '.github/workflows/backend.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/api/**'
|
||||
- 'features/e2e/**'
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `backend/**` : Code Go, migrations, configuration
|
||||
- `features/api/**` : Features BDD des tests API
|
||||
- `features/e2e/**` : Features BDD end-to-end impliquant le backend
|
||||
- `.github/workflows/backend.yml` : Modifications du workflow lui-même
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && go test ./...
|
||||
**Jobs exécutés** :
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && make test-integration
|
||||
- **Tests unitaires** : Exécution `go test` sur tous les packages
|
||||
- **Tests d'intégration** : Utilisation de Testcontainers avec PostgreSQL/PostGIS
|
||||
- **Tests BDD** : Exécution Godog sur features `api/` et `e2e/`
|
||||
- **Lint** : Vérification golangci-lint
|
||||
- **Build** : Compilation binaire production (dépend de tous les jobs précédents)
|
||||
|
||||
test-bdd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: |
|
||||
go install github.com/cucumber/godog/cmd/godog@latest
|
||||
godog run features/api/ features/e2e/
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit, test-integration, test-bdd, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && make build
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/backend` (code Go, migrations, config)
|
||||
- Nouvelles features API dans `/features/api`
|
||||
- Tests end-to-end dans `/features/e2e` (backend impliqué)
|
||||
**Environnement** : Ubuntu latest, Go 1.21+
|
||||
|
||||
---
|
||||
|
||||
#### Workflow Mobile (`mobile.yml`)
|
||||
|
||||
```yaml
|
||||
name: Mobile CI
|
||||
**Déclencheurs** :
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'mobile/**' # Code Flutter modifié
|
||||
- 'features/ui/**' # Tests UI modifiés
|
||||
- 'features/e2e/**' # Tests E2E (impliquent mobile)
|
||||
- '.github/workflows/mobile.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
- 'features/ui/**'
|
||||
- 'features/e2e/**'
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `mobile/**` : Code Flutter/Dart, assets, configuration
|
||||
- `features/ui/**` : Features BDD des tests UI
|
||||
- `features/e2e/**` : Features BDD end-to-end impliquant le mobile
|
||||
- `.github/workflows/mobile.yml` : Modifications du workflow lui-même
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter test
|
||||
**Jobs exécutés** :
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter test integration_test/
|
||||
- **Tests unitaires** : Exécution `flutter test` sur widgets et logique métier
|
||||
- **Tests d'intégration** : Tests d'intégration Flutter (interactions UI complexes)
|
||||
- **Lint** : Analyse statique `flutter analyze`
|
||||
- **Build Android** : Compilation APK release (dépend des tests)
|
||||
- **Build iOS** : Compilation IPA release sans codesign (dépend des tests)
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter analyze
|
||||
**Environnement** :
|
||||
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit, test-integration, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter build apk --release
|
||||
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
needs: [test-unit, test-integration, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter build ios --release --no-codesign
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/mobile` (code Flutter/Dart, assets, config)
|
||||
- Nouvelles features UI dans `/features/ui`
|
||||
- Tests end-to-end dans `/features/e2e` (mobile impliqué)
|
||||
- Tests/Lint/Build Android : Ubuntu latest
|
||||
- Build iOS : macOS latest (requis pour Xcode)
|
||||
- Flutter 3.16.0+
|
||||
|
||||
---
|
||||
|
||||
#### Workflow Shared (`shared.yml`)
|
||||
|
||||
```yaml
|
||||
name: Shared CI
|
||||
**Déclencheurs** :
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'docs/**' # Documentation modifiée
|
||||
- 'shared/**' # Code partagé modifié
|
||||
- '.github/workflows/shared.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'shared/**'
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `docs/**` : ADR, règles métier, documentation technique
|
||||
- `shared/**` : Contrats API, types partagés
|
||||
- `.github/workflows/shared.yml` : Modifications du workflow lui-même
|
||||
|
||||
jobs:
|
||||
docs-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: |
|
||||
pip install mkdocs mkdocs-material
|
||||
mkdocs build --strict
|
||||
**Jobs exécutés** :
|
||||
|
||||
docs-links:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lycheeverse/lychee-action@v1
|
||||
with:
|
||||
args: 'docs/**/*.md'
|
||||
- **Validation documentation** : Build MkDocs en mode strict (détecte erreurs markdown)
|
||||
- **Vérification liens** : Validation des liens internes/externes dans documentation
|
||||
- **Tests code partagé** : Exécution tests si du code partagé backend-mobile existe
|
||||
|
||||
shared-tests:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.head_commit.modified, 'shared/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Tests pour code partagé si nécessaire
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/docs` (ADR, règles métier, documentation technique)
|
||||
- Modifications dans `/shared` (contrats API, types partagés)
|
||||
**Environnement** : Ubuntu latest, Python 3.11+
|
||||
|
||||
---
|
||||
|
||||
@@ -256,6 +125,7 @@ jobs:
|
||||
| **Commit mixte (backend + mobile + docs)** | ✅ | ✅ | ✅ | ~13 min (parallèle) |
|
||||
|
||||
**Économie de temps** :
|
||||
|
||||
- Commit backend-only : ~5 min (vs 15 min sans path filters) = **67% plus rapide**
|
||||
- Commit docs-only : ~30s (vs 15 min) = **97% plus rapide**
|
||||
|
||||
@@ -264,6 +134,7 @@ jobs:
|
||||
Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (backend ET mobile) car ils testent l'intégration complète :
|
||||
|
||||
**Exemples de features E2E** :
|
||||
|
||||
- `features/e2e/abonnements/` : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI
|
||||
- `features/e2e/error-handling/` : Perte réseau → Fallback mode offline → Reprise auto après reconnexion
|
||||
|
||||
@@ -298,6 +169,7 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
|
||||
❌ **Complexité initiale** : setup plus complexe que workflow monolithique
|
||||
|
||||
**Mitigation** :
|
||||
|
||||
- Utiliser des **composite actions** pour partager la config commune
|
||||
- Documentation claire dans ce ADR
|
||||
- Coût initial faible (~2h setup) vs gains à long terme importants
|
||||
@@ -316,15 +188,18 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
|
||||
### Plan d'Implémentation
|
||||
|
||||
**Phase 1** : Setup workflows de base (~1h)
|
||||
|
||||
- Créer `backend.yml` avec jobs test + lint + build
|
||||
- Créer `mobile.yml` avec jobs test + lint + build
|
||||
- Créer `shared.yml` avec validation docs
|
||||
|
||||
**Phase 2** : Configuration path filters (~30 min)
|
||||
|
||||
- Ajouter `paths:` à chaque workflow
|
||||
- Tester avec commits isolés (backend-only, mobile-only, docs-only)
|
||||
|
||||
**Phase 3** : Optimisations (~30 min)
|
||||
|
||||
- Ajouter caching (Go modules, Flutter dependencies, node_modules)
|
||||
- Créer composite actions pour config partagée
|
||||
- Ajouter badges status dans README
|
||||
@@ -333,31 +208,15 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
|
||||
|
||||
### Validation
|
||||
|
||||
```bash
|
||||
# Test 1 : Commit backend-only
|
||||
git add backend/
|
||||
git commit -m "test: backend change"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT backend.yml s'exécute
|
||||
**Scénarios de test à valider** :
|
||||
|
||||
# Test 2 : Commit mobile-only
|
||||
git add mobile/
|
||||
git commit -m "test: mobile change"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT mobile.yml s'exécute
|
||||
1. **Commit backend uniquement** : Modifications dans `/backend` → Vérifier exécution isolée de `backend.yml`
|
||||
2. **Commit mobile uniquement** : Modifications dans `/mobile` → Vérifier exécution isolée de `mobile.yml`
|
||||
3. **Commit features E2E** : Modifications dans `/features/e2e` → Vérifier exécution conjointe de `backend.yml` ET `mobile.yml`
|
||||
4. **Commit documentation uniquement** : Modifications dans `/docs` → Vérifier exécution isolée de `shared.yml`
|
||||
5. **Commit mixte** : Modifications backend + mobile + docs → Vérifier exécution des 3 workflows en parallèle
|
||||
|
||||
# Test 3 : Commit E2E
|
||||
git add features/e2e/
|
||||
git commit -m "test: e2e change"
|
||||
git push
|
||||
# → Vérifier que backend.yml ET mobile.yml s'exécutent
|
||||
|
||||
# Test 4 : Commit docs-only
|
||||
git add docs/
|
||||
git commit -m "docs: update ADR"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT shared.yml s'exécute
|
||||
```
|
||||
**Vérifications** : Consulter l'onglet "Actions" de GitHub pour confirmer quels workflows se sont déclenchés.
|
||||
|
||||
---
|
||||
|
||||
@@ -381,6 +240,7 @@ git push
|
||||
⚠️ **Faux négatifs** : path filter mal configuré → test non exécuté → bug en production
|
||||
|
||||
**Mitigation** :
|
||||
|
||||
- Features E2E déclenchent toujours backend + mobile (safety net)
|
||||
- Tests de validation dans le plan d'implémentation
|
||||
- Review obligatoire des modifications de workflows
|
||||
|
||||
209
docs/adr/023-architecture-moderation.md
Normal file
209
docs/adr/023-architecture-moderation.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# ADR-023 : Architecture de Modération
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
Le système de modération RoadWave doit traiter des signalements de contenu audio problématique (haine, spam, droits d'auteur, etc.) avec :
|
||||
|
||||
- **SLA stricts** : 2h (critique), 24h (haute), 72h (standard) définis dans [Règle 14](../domains/moderation/rules/moderation-flows.md)
|
||||
- **Scalabilité** : 0-10K+ signalements/mois
|
||||
- **Conformité DSA** : transparence, traçabilité, délais garantis
|
||||
- **Efficacité** : pré-filtrage IA pour priorisation automatique
|
||||
|
||||
## Décision
|
||||
|
||||
Architecture hybride **humain + IA** avec file d'attente intelligente.
|
||||
|
||||
### Stack Technique
|
||||
|
||||
| Composant | Technologie | Justification |
|
||||
|-----------|-------------|---------------|
|
||||
| **Queue signalements** | PostgreSQL LISTEN/NOTIFY | Pas de dépendance externe, transactions ACID |
|
||||
| **Transcription audio** | Whisper large-v3 (self-hosted) | Open source, qualité production, 0€ |
|
||||
| **Analyse NLP** | distilbert + roberta-hate-speech | Modèles open source, self-hosted |
|
||||
| **Dashboard modérateurs** | React + Fiber API | Stack cohérent avec ADR-001, ADR-010 |
|
||||
| **Player audio** | Wavesurfer.js | Waveform visuel, annotations temporelles |
|
||||
| **Cache priorisation** | Redis Sorted Sets | Ranking temps réel, TTL automatique |
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Client["App Mobile/Web"]
|
||||
Report["Signalement utilisateur"]
|
||||
end
|
||||
|
||||
subgraph Backend["Backend Go"]
|
||||
API["API Fiber<br/>/moderation/report"]
|
||||
Queue["PostgreSQL Queue<br/>LISTEN/NOTIFY"]
|
||||
Worker["Worker Go<br/>(transcription + NLP)"]
|
||||
end
|
||||
|
||||
subgraph AI["IA Self-hosted"]
|
||||
Whisper["Whisper large-v3<br/>(transcription)"]
|
||||
NLP["distilbert<br/>(sentiment + haine)"]
|
||||
end
|
||||
|
||||
subgraph Moderation["Modération Dashboard"]
|
||||
Dashboard["React Dashboard"]
|
||||
Player["Wavesurfer.js<br/>(lecture audio)"]
|
||||
end
|
||||
|
||||
subgraph Storage["Stockage"]
|
||||
DB["PostgreSQL<br/>(signalements + logs)"]
|
||||
Redis["Redis<br/>(priorisation + cache)"]
|
||||
end
|
||||
|
||||
Report --> API
|
||||
API --> Queue
|
||||
Queue --> Worker
|
||||
Worker --> Whisper
|
||||
Whisper --> NLP
|
||||
NLP --> Redis
|
||||
Worker --> DB
|
||||
Dashboard --> Player
|
||||
Dashboard --> Redis
|
||||
Dashboard --> DB
|
||||
|
||||
classDef clientStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef backendStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef aiStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
||||
|
||||
class Client,Report clientStyle
|
||||
class Backend,API,Queue,Worker backendStyle
|
||||
class AI,Whisper,NLP aiStyle
|
||||
class Storage,DB,Redis storageStyle
|
||||
```
|
||||
|
||||
### Workflow de Traitement
|
||||
|
||||
1. **Réception signalement** :
|
||||
- Insertion en base PostgreSQL (table `moderation_reports`)
|
||||
- Notification asynchrone via PostgreSQL NOTIFY
|
||||
|
||||
2. **Worker asynchrone** (goroutine) :
|
||||
- Écoute queue PostgreSQL (LISTEN/NOTIFY)
|
||||
- Téléchargement audio depuis stockage S3/local
|
||||
- Transcription audio via Whisper large-v3 (1-10 min selon durée)
|
||||
- Analyse NLP : score confiance 0-100% (distilbert + roberta)
|
||||
- Calcul priorité selon formule : `(score_IA × 0.7) + (nb_signalements × 0.2) + (fiabilité_signaleur × 0.1)`
|
||||
- Insertion dans Redis Sorted Set pour priorisation
|
||||
|
||||
3. **Dashboard modérateurs** :
|
||||
- Récupération signalements priorisés depuis Redis (top 20 par page)
|
||||
- Affichage : transcription, waveform audio, historique créateur
|
||||
- Actions disponibles : Approuver, Rejeter, Escalade (shortcuts clavier A/R/E)
|
||||
- Logs audit PostgreSQL pour traçabilité (conformité DSA)
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Queue de signalements
|
||||
|
||||
| Option | Avantages | Inconvénients | Verdict |
|
||||
|--------|-----------|---------------|---------|
|
||||
| **PostgreSQL LISTEN/NOTIFY** | ✅ Pas de dépendance, ACID | ⚠️ Performance limitée >10K/min | ✅ Choisi MVP |
|
||||
| RabbitMQ | Scalable, dead letter queues | ❌ Nouvelle dépendance, complexité | ❌ Overkill MVP |
|
||||
| Redis Streams | Performant, simple | ⚠️ Pas de garantie persistance | ⚠️ Phase 2 |
|
||||
| SQS/Cloud | Managed, scalable | ❌ Dépendance cloud, coût | ❌ Souveraineté |
|
||||
|
||||
### Transcription audio
|
||||
|
||||
| Option | Coût | Qualité | Hébergement | Verdict |
|
||||
|--------|------|---------|-------------|---------|
|
||||
| **Whisper large-v3** | **0€** (self-hosted) | ⭐⭐⭐ Excellente | Self-hosted | ✅ Choisi |
|
||||
| AssemblyAI API | 0.37$/h audio | ⭐⭐⭐ Excellente | Cloud US | ❌ Coût + souveraineté |
|
||||
| Google Speech-to-Text | 0.024$/min | ⭐⭐ Bonne | Cloud Google | ❌ Dépendance Google |
|
||||
| Whisper tiny/base | 0€ | ⭐ Moyenne | Self-hosted | ❌ Qualité insuffisante |
|
||||
|
||||
### NLP Analyse
|
||||
|
||||
| Option | Coût | Performance | Hébergement | Verdict |
|
||||
|--------|------|-------------|-------------|---------|
|
||||
| **distilbert + roberta** | **0€** | CPU OK (1-3s/audio) | Self-hosted | ✅ Choisi |
|
||||
| OpenAI Moderation API | 0.002$/1K tokens | Excellente | Cloud OpenAI | ❌ Dépendance + coût |
|
||||
| Perspective API (Google) | Gratuit | Bonne | Cloud Google | ❌ Dépendance Google |
|
||||
|
||||
## Justification
|
||||
|
||||
### PostgreSQL LISTEN/NOTIFY
|
||||
|
||||
- **Performance MVP** : Suffisant jusqu'à 1000 signalements/jour (~0.7/min)
|
||||
- **Simplicité** : Pas de broker externe, transactions ACID
|
||||
- **Migration facile** : Abstraction via interface `ModerationQueue` → swap vers Redis Streams si besoin (méthodes : Enqueue, Listen)
|
||||
|
||||
### Whisper large-v3 self-hosted
|
||||
|
||||
- **Coût 0€** vs AssemblyAI (3700€/an @ 10K heures audio)
|
||||
- **Souveraineté** : données sensibles restent en France
|
||||
- **Qualité production** : WER (Word Error Rate) <5% français
|
||||
- **Scaling** : CPU MVP (1 core), GPU Phase 2 si >1000 signalements/jour
|
||||
|
||||
### Dashboard React
|
||||
|
||||
- **Cohérence stack** : Même techno que admin panel (si React adopté)
|
||||
- **Performance** : TanStack Table pour listes >1000 éléments
|
||||
- **Wavesurfer.js** : Standard industrie (SoundCloud, Audacity web)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **0€ infrastructure IA** au MVP (CPU standard)
|
||||
- ✅ **100% self-hosted** : conformité souveraineté (ADR-008, ADR-015)
|
||||
- ✅ **Scalable progressif** : PostgreSQL → Redis Streams si besoin
|
||||
- ✅ **Conformité DSA** : logs audit, traçabilité complète
|
||||
- ✅ **Productivité ×3-5** : pré-filtrage IA réduit charge modérateurs
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Latence transcription** : 1-10 min selon durée audio (acceptable, traitement asynchrone)
|
||||
- ⚠️ **Performance limite** : PostgreSQL LISTEN/NOTIFY saturé >10K signalements/jour (migration Redis Streams nécessaire)
|
||||
- ❌ **Ressources CPU** : Whisper consomme 1-4 CPU cores selon charge (migration GPU si >1000 signalements/jour)
|
||||
|
||||
### Dépendances
|
||||
|
||||
**Backend Go** :
|
||||
|
||||
- `gofiber/fiber/v3` : API Dashboard
|
||||
- `jackc/pgx/v5` : PostgreSQL + LISTEN/NOTIFY
|
||||
- `redis/rueidis` : Cache priorisation
|
||||
- Whisper : via Python subprocess ou go-whisper bindings
|
||||
|
||||
**Frontend Dashboard** :
|
||||
|
||||
- `react` : Framework UI
|
||||
- `@tanstack/react-table` : Tables performantes
|
||||
- `wavesurfer.js` : Player audio avec waveform
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- Latence traitement < 10 min (P95) après réception signalement
|
||||
- Précision IA pré-filtre > 80% (validation humaine)
|
||||
- SLA respectés > 95% des cas (2h/24h/72h selon priorité)
|
||||
- Coût infrastructure < 50€/mois jusqu'à 1000 signalements/mois
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 3-4)
|
||||
1. Backend : API `/moderation/report` + PostgreSQL queue
|
||||
2. Worker : Whisper large-v3 CPU + NLP basique (liste noire mots-clés)
|
||||
3. Dashboard : React basique (liste + player audio)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 8-10)
|
||||
1. Migration Redis Streams si >1000 signalements/jour
|
||||
2. GPU pour Whisper si latence >15 min P95
|
||||
3. NLP avancé (distilbert + roberta)
|
||||
4. Modération communautaire (badges, [Règle 15](../domains/moderation/rules/moderation-communautaire.md))
|
||||
|
||||
## Références
|
||||
|
||||
- [Règle 14 : Modération - Flows opérationnels](../domains/moderation/rules/moderation-flows.md)
|
||||
- [Règle 15 : Modération Communautaire](../domains/moderation/rules/moderation-communautaire.md)
|
||||
- [ADR-001 : Langage Backend](001-langage-backend.md) (Go, Fiber)
|
||||
- [ADR-005 : Base de données](005-base-de-donnees.md) (PostgreSQL)
|
||||
- [ADR-010 : Architecture Backend](010-architecture-backend.md) (Modular monolith)
|
||||
- [Whisper large-v3 documentation](https://github.com/openai/whisper)
|
||||
- [PostgreSQL LISTEN/NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html)
|
||||
304
docs/adr/024-monitoring-observabilite.md
Normal file
304
docs/adr/024-monitoring-observabilite.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# ADR-024 : Monitoring, Observabilité et Incident Response
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
RoadWave nécessite un système de monitoring pour garantir la disponibilité cible 99.9% (SLO) définie dans :
|
||||
|
||||
- **Métriques** : latency p99 < 100ms, throughput API, erreurs
|
||||
- **Alerting** : détection pannes, dégradations performance
|
||||
- **Incident response** : runbooks, escalation, post-mortems
|
||||
- **Backup/Disaster Recovery** : RTO 1h, RPO 15min
|
||||
|
||||
Contrainte : **self-hosted** pour souveraineté données (ADR-015).
|
||||
|
||||
## Décision
|
||||
|
||||
Stack **Prometheus + Grafana + Loki** self-hosted avec alerting multi-canal.
|
||||
|
||||
### Stack Technique
|
||||
|
||||
| Composant | Technologie | Licence | Justification |
|
||||
|-----------|-------------|---------|---------------|
|
||||
| **Métriques** | Prometheus | Apache-2.0 | Standard industrie, PromQL, TSDB performant |
|
||||
| **Visualisation** | Grafana | AGPL-3.0 | Dashboards riches, alerting intégré |
|
||||
| **Logs** | Grafana Loki | AGPL-3.0 | "Prometheus pour logs", compression efficace |
|
||||
| **Tracing** | Tempo (optionnel Phase 2) | AGPL-3.0 | Traces distribuées, compatible OpenTelemetry |
|
||||
| **Alerting** | Alertmanager | Apache-2.0 | Grouping, silencing, routing multi-canal |
|
||||
| **Canaux alerts** | Email (Brevo) + Telegram Bot | - | Multi-canal, pas de coût SMS |
|
||||
| **Uptime monitoring** | Uptime Kuma | MIT | Self-hosted, SSL checks, incidents page |
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Services["Services RoadWave"]
|
||||
API["Backend Go API<br/>(Fiber metrics)"]
|
||||
DB["PostgreSQL<br/>(pg_exporter)"]
|
||||
Redis["Redis<br/>(redis_exporter)"]
|
||||
Zitadel["Zitadel<br/>(metrics endpoint)"]
|
||||
end
|
||||
|
||||
subgraph Monitoring["Stack Monitoring"]
|
||||
Prom["Prometheus<br/>(scrape + TSDB)"]
|
||||
Grafana["Grafana<br/>(dashboards)"]
|
||||
Loki["Loki<br/>(logs aggregation)"]
|
||||
Alert["Alertmanager<br/>(routing)"]
|
||||
Uptime["Uptime Kuma<br/>(external checks)"]
|
||||
end
|
||||
|
||||
subgraph Notifications["Alerting"]
|
||||
Email["Email (Brevo)"]
|
||||
Telegram["Telegram Bot"]
|
||||
end
|
||||
|
||||
subgraph Storage["Stockage"]
|
||||
PromStorage["Prometheus TSDB<br/>(15j retention)"]
|
||||
LokiStorage["Loki Chunks<br/>(7j retention)"]
|
||||
Backups["Backups PostgreSQL<br/>(S3 OVH)"]
|
||||
end
|
||||
|
||||
API --> Prom
|
||||
DB --> Prom
|
||||
Redis --> Prom
|
||||
Zitadel --> Prom
|
||||
|
||||
API -.->|logs stdout| Loki
|
||||
Prom --> Grafana
|
||||
Loki --> Grafana
|
||||
Prom --> Alert
|
||||
|
||||
Alert --> Email
|
||||
Alert --> Telegram
|
||||
|
||||
Uptime -.->|external HTTP checks| API
|
||||
Uptime --> Alert
|
||||
|
||||
Prom --> PromStorage
|
||||
Loki --> LokiStorage
|
||||
DB -.->|WAL-E continuous| Backups
|
||||
|
||||
classDef serviceStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef monitoringStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef notifStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
||||
|
||||
class Services,API,DB,Redis,Zitadel serviceStyle
|
||||
class Monitoring,Prom,Grafana,Loki,Alert,Uptime monitoringStyle
|
||||
class Notifications,Email,Telegram notifStyle
|
||||
class Storage,PromStorage,LokiStorage,Backups storageStyle
|
||||
```
|
||||
|
||||
### Métriques Clés
|
||||
|
||||
**API Performance** (requêtes PromQL) :
|
||||
|
||||
- Latency p99 : histogramme quantile 99e percentile sur durée requêtes HTTP (fenêtre 5 min)
|
||||
- Error rate : ratio requêtes 5xx / total requêtes (fenêtre 5 min)
|
||||
- Throughput : taux de requêtes par seconde (fenêtre 5 min)
|
||||
|
||||
**Infrastructure** :
|
||||
|
||||
- CPU usage : taux utilisation CPU (mode non-idle, fenêtre 5 min)
|
||||
- Memory usage : ratio mémoire disponible / totale
|
||||
- Disk I/O : temps I/O disque (fenêtre 5 min)
|
||||
|
||||
**Business** (compteurs custom) :
|
||||
|
||||
- Active users (DAU) : `roadwave_active_users_total`
|
||||
- Audio streams actifs : `roadwave_hls_streams_active`
|
||||
- Signalements modération : `roadwave_moderation_reports_total`
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Stack Monitoring
|
||||
|
||||
| Option | Coût | Hébergement | Complexité | Verdict |
|
||||
|--------|------|-------------|------------|---------|
|
||||
| **Prometheus + Grafana** | **0€** | Self-hosted | ⭐⭐ Moyenne | ✅ Choisi |
|
||||
| Datadog | 15-31$/host/mois | SaaS US | ⭐ Faible | ❌ Coût + souveraineté |
|
||||
| New Relic | 99-349$/user/mois | SaaS US | ⭐ Faible | ❌ Coût prohibitif |
|
||||
| Elastic Stack (ELK) | 0€ (open) | Self-hosted | ⭐⭐⭐ Complexe | ❌ Overhead JVM |
|
||||
| VictoriaMetrics | 0€ | Self-hosted | ⭐⭐ Moyenne | ⚠️ Moins mature |
|
||||
|
||||
### Alerting Canaux
|
||||
|
||||
| Canal | Coût | Disponibilité | Intrusivité | Verdict |
|
||||
|-------|------|---------------|-------------|---------|
|
||||
| **Email (Brevo)** | **0€ (300/j)** | Asynchrone | ⭐ Basse | ✅ Standard |
|
||||
| **Telegram Bot** | **0€** | Temps réel | ⭐⭐ Moyenne | ✅ On-call |
|
||||
| SMS (Twilio) | 0.04€/SMS | Immédiat | ⭐⭐⭐ Haute | ⚠️ Phase 2 (critique) |
|
||||
| PagerDuty | 21$/user/mois | Immédiat + escalation | ⭐⭐⭐ Haute | ❌ Coût |
|
||||
| OpsGenie | 29$/user/mois | Immédiat + escalation | ⭐⭐⭐ Haute | ❌ Coût |
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
| Option | RPO | RTO | Coût | Verdict |
|
||||
|--------|-----|-----|------|---------|
|
||||
| **WAL-E continuous archiving** | **15 min** | **1h** | **5-15€/mois (S3)** | ✅ Choisi |
|
||||
| pg_dump quotidien | 24h | 2-4h | 0€ (local) | ❌ RPO trop élevé |
|
||||
| pgBackRest | 5 min | 30 min | 10-20€/mois | ⚠️ Complexe MVP |
|
||||
| Managed backup (Scaleway) | 5 min | 15 min | 50€/mois | ❌ Phase 2 |
|
||||
|
||||
## Justification
|
||||
|
||||
### Prometheus + Grafana
|
||||
|
||||
- **Standard industrie** : adopté par CNCF, documentation riche
|
||||
- **Performance** : TSDB optimisé, compression >10x vs PostgreSQL
|
||||
- **Écosystème** : 150+ exporters officiels (PostgreSQL, Redis, Go, Nginx)
|
||||
- **PromQL** : langage requête puissant pour alerting complexe
|
||||
- **Coût 0€** : self-hosted, licences permissives
|
||||
|
||||
### Loki pour Logs
|
||||
|
||||
- **Compression** : 10-50x vs Elasticsearch (stockage chunks)
|
||||
- **Simplicité** : pas de schéma, logs = labels + timestamp
|
||||
- **Intégration Grafana** : requêtes logs + métriques unifiées
|
||||
- **Performance** : grep distribué sur labels indexés
|
||||
|
||||
### Uptime Kuma
|
||||
|
||||
- **Self-hosted** : alternative à UptimeRobot (SaaS)
|
||||
- **Fonctionnalités** : HTTP/HTTPS checks, SSL expiry, status page public
|
||||
- **Alerting** : intégration Webhook, Email
|
||||
- **Coût 0€** : open source MIT
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **Coût infrastructure** : 5-20€/mois (stockage S3 backups uniquement)
|
||||
- ✅ **Souveraineté** : 100% self-hosted OVH France
|
||||
- ✅ **Alerting multi-canal** : Email + Telegram (extensible SMS Phase 2)
|
||||
- ✅ **Observabilité complète** : métriques + logs + uptime externe
|
||||
- ✅ **Conformité RGPD** : logs anonymisés, rétention 7-15j
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Maintenance** : Stack à gérer (mises à jour Prometheus, Grafana, Loki)
|
||||
- ⚠️ **Stockage** : Prometheus TSDB consomme ~1-2 GB/mois @ 1000 RPS
|
||||
- ❌ **Pas d'on-call automatique** au MVP (Telegram manual, SMS Phase 2)
|
||||
- ❌ **Courbe d'apprentissage** : PromQL à maîtriser
|
||||
|
||||
### Dashboards Grafana
|
||||
|
||||
**Dashboard principal** :
|
||||
|
||||
- Latency p50/p95/p99 API (5 min, 1h, 24h)
|
||||
- Error rate 5xx/4xx (seuil alerte >1%)
|
||||
- Throughput requests/sec
|
||||
- Infra : CPU, RAM, Disk I/O
|
||||
- Business : DAU, streams actifs, signalements modération
|
||||
|
||||
**Dashboard PostgreSQL** :
|
||||
|
||||
- Slow queries (>100ms)
|
||||
- Connections actives vs max
|
||||
- Cache hit ratio (cible >95%)
|
||||
- Deadlocks count
|
||||
|
||||
**Dashboard Redis** :
|
||||
|
||||
- Memory usage
|
||||
- Evictions count
|
||||
- Commands/sec
|
||||
- Keyspace hits/misses ratio
|
||||
|
||||
### Alerting Rules
|
||||
|
||||
**Alertes critiques** (Telegram + Email immédiat) :
|
||||
|
||||
- **API Down** : Job API indisponible pendant >1 min → Notification immédiate
|
||||
- **High Error Rate** : Taux erreurs 5xx >1% pendant >5 min → Notification immédiate
|
||||
- **Database Down** : PostgreSQL indisponible pendant >1 min → Notification immédiate
|
||||
|
||||
**Alertes warnings** (Email uniquement) :
|
||||
|
||||
- **High Latency** : Latency p99 >100ms pendant >10 min → Investigation requise
|
||||
- **Disk Space Running Out** : Espace disque <10% pendant >30 min → Nettoyage requis
|
||||
|
||||
### Backup & Disaster Recovery
|
||||
|
||||
**PostgreSQL WAL-E** :
|
||||
|
||||
- Méthode : Backup continu Write-Ahead Log (WAL)
|
||||
- Rétention : 7 jours full + WAL incrémentaux
|
||||
- Stockage : S3 OVH région GRA (France)
|
||||
- Chiffrement : AES-256 server-side
|
||||
|
||||
**RTO (Recovery Time Objective)** : 1h
|
||||
|
||||
- Restore depuis S3 : ~30 min (DB 10 GB)
|
||||
- Validation + relance services : ~30 min
|
||||
|
||||
**RPO (Recovery Point Objective)** : 15 min
|
||||
|
||||
- Fréquence archivage WAL : toutes les 15 min
|
||||
- Perte maximale : 15 min de transactions
|
||||
|
||||
**Tests DR** : Mensuel (restore backup sur environnement staging)
|
||||
|
||||
## Runbooks Incidents
|
||||
|
||||
### API Down (5xx errors spike)
|
||||
|
||||
1. **Vérifier** : Grafana dashboard → onglet Errors
|
||||
2. **Logs** : Requête Loki filtrée sur app roadwave-api + niveau error
|
||||
3. **Actions** :
|
||||
- Si OOM : restart container + augmenter RAM
|
||||
- Si DB connexions saturées : vérifier slow queries
|
||||
- Si réseau : vérifier OVH status page
|
||||
4. **Escalade** : Si non résolu en 15 min → appel admin senior
|
||||
|
||||
### Database Slow Queries
|
||||
|
||||
1. **Identifier** : Grafana → PostgreSQL dashboard → Top slow queries
|
||||
2. **Analyser** : Utiliser EXPLAIN ANALYZE sur query problématique
|
||||
3. **Actions** :
|
||||
- Index manquant : créer index (migration rapide)
|
||||
- Lock contention : identifier transaction longue et kill si bloquante
|
||||
4. **Prevention** : Ajouter alerte Grafana si query >100ms P95
|
||||
|
||||
### High Load (CPU >80%)
|
||||
|
||||
1. **Vérifier** : Grafana → Node Exporter → CPU usage
|
||||
2. **Top processus** : Consulter htop ou docker stats
|
||||
3. **Actions** :
|
||||
- Si Whisper (modération) : réduire concurrence workers
|
||||
- Si API : scale horizontal (ajouter instance)
|
||||
4. **Prévention** : Auto-scaling (Phase 2)
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- Uptime > 99.9% (8.76h downtime/an max)
|
||||
- MTTD (Mean Time To Detect) < 5 min
|
||||
- MTTR (Mean Time To Recover) < 30 min
|
||||
- Alerts faux positifs < 5%
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 2-3)
|
||||
1. Deploy Prometheus + Grafana + Loki (Docker Compose)
|
||||
2. Instrumenter API Go (Fiber middleware metrics)
|
||||
3. Configure exporters : PostgreSQL, Redis, Node
|
||||
4. Dashboard principal + 5 alertes critiques
|
||||
5. Setup WAL-E backup PostgreSQL
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 6-8)
|
||||
1. Ajouter Tempo (tracing distribué)
|
||||
2. SMS alerting (Twilio) pour incidents critiques
|
||||
3. Auto-scaling basé métriques Prometheus
|
||||
4. Post-mortem process (template Notion)
|
||||
|
||||
## Références
|
||||
|
||||
- (SLO 99.9%, latency p99 <100ms)
|
||||
- [ADR-001 : Langage Backend](001-langage-backend.md) (Go, Fiber)
|
||||
- [ADR-005 : Base de données](005-base-de-donnees.md) (PostgreSQL)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md) (OVH France, self-hosted)
|
||||
- [Prometheus Documentation](https://prometheus.io/docs/)
|
||||
- [Grafana Loki](https://grafana.com/oss/loki/)
|
||||
- [WAL-E PostgreSQL Archiving](https://github.com/wal-e/wal-e)
|
||||
287
docs/adr/025-securite-secrets.md
Normal file
287
docs/adr/025-securite-secrets.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# ADR-025 : Sécurité - Secrets Management et Encryption
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
RoadWave manipule des données sensibles nécessitant une protection renforcée :
|
||||
|
||||
- **Secrets applicatifs** : JWT signing key, DB credentials, Mangopay API keys
|
||||
- **PII utilisateurs** : Positions GPS précises, emails, données bancaires (via Mangopay)
|
||||
- **Conformité** : RGPD (minimisation données, encryption at rest), PCI-DSS (paiements)
|
||||
- **Souveraineté** : Self-hosted requis (ADR-015)
|
||||
|
||||
Contrainte : **OWASP Top 10 mitigation** obligatoire pour sécurité applicative.
|
||||
|
||||
## Décision
|
||||
|
||||
Stratégie **secrets management + encryption at rest + HTTPS** avec stack self-hosted.
|
||||
|
||||
### Stack Sécurité
|
||||
|
||||
| Composant | Technologie | Licence | Justification |
|
||||
|-----------|-------------|---------|---------------|
|
||||
| **Secrets management** | HashiCorp Vault (open source) | MPL-2.0 | Standard industrie, rotation auto, audit logs |
|
||||
| **Encryption PII** | AES-256-GCM (crypto/aes Go) | BSD-3 | NIST approuvé, AEAD (authenticated) |
|
||||
| **HTTPS/TLS** | Let's Encrypt (Certbot) | ISC | Gratuit, renouvellement auto, wildcard support |
|
||||
| **CORS/CSRF** | Fiber middleware | MIT | Protection XSS/CSRF intégrée |
|
||||
| **Rate limiting** | Redis + Token Bucket (Fiber) | MIT/Apache | Protection brute-force, DDoS |
|
||||
| **SQL injection** | sqlc (prepared statements) | MIT | Parameterized queries (ADR-011) |
|
||||
|
||||
### Architecture Secrets
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Dev["Environnement Dev"]
|
||||
EnvFile[".env file<br/>(local uniquement)"]
|
||||
end
|
||||
|
||||
subgraph Prod["Production"]
|
||||
Vault["HashiCorp Vault<br/>(secrets storage)"]
|
||||
API["Backend Go API"]
|
||||
DB["PostgreSQL<br/>(encrypted at rest)"]
|
||||
Redis["Redis<br/>(TLS enabled)"]
|
||||
end
|
||||
|
||||
subgraph Encryption["Encryption Layer"]
|
||||
AES["AES-256-GCM<br/>(PII encryption)"]
|
||||
TLS["TLS 1.3<br/>(transport)"]
|
||||
end
|
||||
|
||||
subgraph Secrets["Secrets Stockés"]
|
||||
JWT["JWT Signing Key<br/>(RS256 private key)"]
|
||||
DBCreds["DB Credentials<br/>(user/pass)"]
|
||||
Mangopay["Mangopay API Key<br/>(sandbox + prod)"]
|
||||
EncKey["Encryption Master Key<br/>(AES-256)"]
|
||||
end
|
||||
|
||||
EnvFile -.->|dev only| API
|
||||
Vault --> API
|
||||
|
||||
Vault --- JWT
|
||||
Vault --- DBCreds
|
||||
Vault --- Mangopay
|
||||
Vault --- EncKey
|
||||
|
||||
API --> AES
|
||||
API --> TLS
|
||||
AES --> DB
|
||||
TLS --> DB
|
||||
TLS --> Redis
|
||||
|
||||
classDef devStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef prodStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef encStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef secretStyle fill:#ffebee,stroke:#c62828
|
||||
|
||||
class Dev,EnvFile devStyle
|
||||
class Prod,Vault,API,DB,Redis prodStyle
|
||||
class Encryption,AES,TLS encStyle
|
||||
class Secrets,JWT,DBCreds,Mangopay,EncKey secretStyle
|
||||
```
|
||||
|
||||
### Secrets Management avec Vault
|
||||
|
||||
**Initialisation Vault** (one-time setup) :
|
||||
1. Init Vault : génération 5 unseal keys + root token (Shamir secret sharing)
|
||||
2. Unseal : 3 clés parmi 5 requises pour déverrouiller Vault
|
||||
3. Login root + activation KV-v2 engine (path : `roadwave/`)
|
||||
|
||||
**Secrets stockés** :
|
||||
|
||||
- **JWT signing key** : Paire RS256 privée/publique
|
||||
- **Database credentials** : Host, port, user, password (généré aléatoire 32 caractères)
|
||||
- **Mangopay API** : Client ID, API key, webhook secret
|
||||
|
||||
**Récupération depuis Backend Go** :
|
||||
|
||||
- Utilisation SDK `hashicorp/vault/api`
|
||||
- Authentification via token Vault (variable env VAULT_TOKEN)
|
||||
- Récupération secrets via KVv2 engine
|
||||
|
||||
### Encryption PII (Field-level)
|
||||
|
||||
**Données chiffrées** (AES-256-GCM) :
|
||||
|
||||
- **GPS précis** : lat/lon conservés 24h puis réduits à geohash-5 (~5km²) ([Règle 02](../domains/_shared/rules/rgpd.md))
|
||||
- **Email** : chiffré en base, déchiffré uniquement à l'envoi
|
||||
- **Numéro téléphone** : si ajouté (Phase 2)
|
||||
|
||||
**Architecture encryption** :
|
||||
|
||||
- Utilisation bibliothèque standard Go `crypto/aes` avec mode GCM (AEAD)
|
||||
- Master key 256 bits (32 bytes) récupérée depuis Vault
|
||||
- Chiffrement : génération nonce aléatoire + seal GCM → encodage base64
|
||||
- Stockage : colonne `email_encrypted` en base PostgreSQL
|
||||
|
||||
**Contraintes** :
|
||||
|
||||
- Index direct sur champ chiffré impossible
|
||||
- Solution : index sur hash SHA-256 de l'email chiffré pour recherche
|
||||
|
||||
### HTTPS/TLS Configuration
|
||||
|
||||
**Let's Encrypt wildcard certificate** :
|
||||
|
||||
- Méthode : Certbot avec DNS-01 challenge (API OVH)
|
||||
- Domaines couverts : `roadwave.fr` + `*.roadwave.fr` (wildcard)
|
||||
- Renouvellement : automatique via cron quotidien (30j avant expiration)
|
||||
|
||||
**Nginx TLS configuration** :
|
||||
|
||||
- Protocol : TLS 1.3 uniquement (pas de TLS 1.2 ou inférieur)
|
||||
- Ciphers : TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384
|
||||
- HSTS : max-age 1 an, includeSubDomains
|
||||
- Security headers : X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Secrets Management
|
||||
|
||||
| Option | Coût | Hébergement | Rotation auto | Audit | Verdict |
|
||||
|--------|------|-------------|---------------|-------|---------|
|
||||
| **Vault (OSS)** | **0€** | Self-hosted | ✅ Oui | ✅ Oui | ✅ Choisi |
|
||||
| Vault Enterprise | 150$/mois | Self-hosted | ✅ Oui | ✅ Oui | ❌ Overkill MVP |
|
||||
| Kubernetes Secrets | 0€ | K8s only | ❌ Non | ⚠️ Limité | ⚠️ Phase 2 (K8s) |
|
||||
| Variables env (.env) | 0€ | VM/container | ❌ Non | ❌ Non | ❌ Insécure prod |
|
||||
| AWS Secrets Manager | 0.40$/secret/mois | Cloud AWS | ✅ Oui | ✅ Oui | ❌ Souveraineté |
|
||||
|
||||
### Encryption Library
|
||||
|
||||
| Option | Performance | AEAD | FIPS 140-2 | Verdict |
|
||||
|--------|-------------|------|------------|---------|
|
||||
| **crypto/aes (Go std)** | ⭐⭐⭐ Rapide | ✅ GCM | ✅ Approuvé | ✅ Choisi |
|
||||
| age (filippo.io/age) | ⭐⭐ Moyen | ✅ ChaCha20 | ❌ Non | ⚠️ Moins standard |
|
||||
| NaCl/libsodium | ⭐⭐⭐ Rapide | ✅ Poly1305 | ❌ Non | ⚠️ CGO dependency |
|
||||
|
||||
### TLS Certificate
|
||||
|
||||
| Option | Coût | Renouvellement | Wildcard | Verdict |
|
||||
|--------|------|----------------|----------|---------|
|
||||
| **Let's Encrypt** | **0€** | Auto (90j) | ✅ Oui (DNS-01) | ✅ Choisi |
|
||||
| OVH SSL | 5€/an | Manuel | ✅ Oui | ❌ Coût inutile |
|
||||
| Cloudflare SSL | 0€ | Auto | ✅ Oui | ⚠️ Proxy Cloudflare |
|
||||
|
||||
## Justification
|
||||
|
||||
### HashiCorp Vault
|
||||
|
||||
- **Standard industrie** : utilisé par 80% Fortune 500
|
||||
- **Rotation automatique** : credentials DB renouvelés toutes les 90j
|
||||
- **Audit logs** : qui a accédé à quel secret, quand
|
||||
- **Unseal ceremony** : sécurité maximale (3/5 clés requises)
|
||||
- **Coût 0€** : version open source MPL-2.0
|
||||
|
||||
### AES-256-GCM
|
||||
|
||||
- **NIST approuvé** : standard gouvernement US (FIPS 140-2)
|
||||
- **AEAD** : Authenticated Encryption with Associated Data (pas de tampering)
|
||||
- **Performance** : hardware acceleration (AES-NI CPU)
|
||||
- **Bibliothèque std Go** : pas de dépendance externe
|
||||
|
||||
### Let's Encrypt
|
||||
|
||||
- **Gratuit** : économie 50-200€/an vs certificat commercial
|
||||
- **Automatique** : Certbot renouvelle 30j avant expiration
|
||||
- **Wildcard** : 1 certificat pour *.roadwave.fr (tous sous-domaines)
|
||||
- **Adopté massivement** : 300M+ sites web
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **Conformité RGPD** : encryption at rest PII, minimisation données
|
||||
- ✅ **PCI-DSS** : secrets paiement isolés (Mangopay API key dans Vault)
|
||||
- ✅ **OWASP Top 10** : SQL injection (sqlc), XSS/CSRF (Fiber), rate limiting
|
||||
- ✅ **Coût 0€** : stack complète open source
|
||||
- ✅ **Audit trail** : logs Vault tracent tous accès secrets
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Vault unseal** : nécessite 3/5 clés au redémarrage serveur (procédure manuelle)
|
||||
- ⚠️ **Performance encryption** : +0.5-2ms latency par champ chiffré (acceptable)
|
||||
- ❌ **Complexité opérationnelle** : Vault à maintenir (backups, upgrades)
|
||||
- ❌ **Recherche email impossible** : chiffrement empêche `WHERE email = 'x'` (utiliser hash)
|
||||
|
||||
### OWASP Top 10 Mitigation
|
||||
|
||||
| Vulnérabilité | Mitigation RoadWave | Implémentation |
|
||||
|---------------|---------------------|----------------|
|
||||
| **A01: Broken Access Control** | JWT scopes + RBAC | Zitadel roles (ADR-008) |
|
||||
| **A02: Cryptographic Failures** | AES-256-GCM + TLS 1.3 | crypto/aes + Let's Encrypt |
|
||||
| **A03: Injection** | Prepared statements (sqlc) | ADR-011 |
|
||||
| **A04: Insecure Design** | Threat modeling + ADR reviews | Process architecture |
|
||||
| **A05: Security Misconfiguration** | Vault secrets + hardened config | ADR-025 |
|
||||
| **A06: Vulnerable Components** | Dependabot + go mod tidy | GitHub Actions |
|
||||
| **A07: Auth Failures** | Zitadel + rate limiting | ADR-008 + Fiber middleware |
|
||||
| **A08: Software Integrity** | Code signing + SBOM | Phase 2 |
|
||||
| **A09: Logging Failures** | Loki centralisé + audit Vault | ADR-024 |
|
||||
| **A10: SSRF** | Whitelist URLs + network policies | Fiber middleware |
|
||||
|
||||
### Rate Limiting (Protection DDoS/Brute-force)
|
||||
|
||||
**Configuration** :
|
||||
|
||||
- Middleware Fiber `limiter` avec backend Redis
|
||||
- Limite : 100 requêtes par minute par IP (global)
|
||||
- Clé de limitation : adresse IP client
|
||||
- Réponse limitation : HTTP 429 "Too many requests"
|
||||
|
||||
**Rate limits par endpoint** :
|
||||
|
||||
- `/auth/login` : 5 req/min/IP (protection brute-force)
|
||||
- `/moderation/report` : 10 req/24h/user (anti-spam)
|
||||
- API générale : 100 req/min/IP
|
||||
|
||||
## Rotation des Secrets
|
||||
|
||||
**Politique de rotation** :
|
||||
|
||||
| Secret | Rotation | Justification |
|
||||
|--------|----------|---------------|
|
||||
| **JWT signing key** | 1 an | Compromission = invalidation tous tokens |
|
||||
| **DB credentials** | 90 jours | Best practice NIST |
|
||||
| **Mangopay API key** | À la demande | Rotation manuelle si compromission |
|
||||
| **Encryption master key** | Jamais (re-encryption massive) | Backup sécurisé uniquement |
|
||||
|
||||
**Process rotation DB credentials** :
|
||||
|
||||
- Vault génère automatiquement nouveau password
|
||||
- Vault met à jour PostgreSQL avec nouveau password
|
||||
- Application récupère nouveau password au prochain accès Vault
|
||||
- Ancien password invalide après grace period de 1h
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- 0 fuite secrets en production (audit logs Vault)
|
||||
- 100% traffic HTTPS (HTTP → HTTPS redirect)
|
||||
- Rate limiting < 0.1% false positives
|
||||
- Encryption overhead < 2ms p95
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 2-3)
|
||||
1. Deploy Vault (Docker single-node)
|
||||
2. Migrer secrets .env → Vault
|
||||
3. Encryption emails (AES-256-GCM)
|
||||
4. HTTPS Let's Encrypt (api.roadwave.fr)
|
||||
5. Rate limiting Fiber (100 req/min global)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 6-8)
|
||||
1. Vault HA (3 nodes Raft)
|
||||
2. Rotation automatique credentials
|
||||
3. Field-level encryption GPS (après 24h)
|
||||
4. WAF (Web Application Firewall) : ModSecurity
|
||||
5. Penetration testing externe (Bug Bounty)
|
||||
|
||||
## Références
|
||||
|
||||
- [ADR-008 : Authentification](008-authentification.md) (Zitadel, JWT)
|
||||
- [ADR-011 : Accès données](011-orm-acces-donnees.md) (sqlc, prepared statements)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md) (OVH France, souveraineté)
|
||||
- [ADR-024 : Monitoring](024-monitoring-observabilite.md) (Audit logs)
|
||||
- [Règle 02 : Conformité RGPD](../domains/_shared/rules/rgpd.md)
|
||||
- [HashiCorp Vault Documentation](https://www.vaultproject.io/docs)
|
||||
- [OWASP Top 10 2021](https://owasp.org/Top10/)
|
||||
- [NIST SP 800-175B (Cryptography)](https://csrc.nist.gov/publications/detail/sp/800-175b/final)
|
||||
126
docs/adr/README.md
Normal file
126
docs/adr/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Architecture Decision Records (ADR)
|
||||
|
||||
> Documentation des décisions architecturales importantes du projet RoadWave
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Les Architecture Decision Records (ADR) documentent les décisions techniques importantes prises au cours du développement de RoadWave. Chaque ADR suit un format standardisé : contexte, décision, alternatives considérées et conséquences.
|
||||
|
||||
## Index des ADR
|
||||
|
||||
### Core Architecture
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-001](001-langage-backend.md) | Langage Backend | ✅ Accepté | 2025-01-17 |
|
||||
| [ADR-010](010-architecture-backend.md) | Architecture Backend | ✅ Accepté | 2025-01-25 |
|
||||
| [ADR-011](011-orm-acces-donnees.md) | ORM et Accès Données | ✅ Accepté | 2025-01-26 |
|
||||
| [ADR-012](012-frontend-mobile.md) | Frontend Mobile | ✅ Accepté | 2025-01-26 |
|
||||
| [ADR-014](014-organisation-monorepo.md) | Organisation en Monorepo | ✅ Accepté | 2025-01-28 |
|
||||
|
||||
### Data & Infrastructure
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-005](005-base-de-donnees.md) | Base de données | ✅ Accepté | 2025-01-20 |
|
||||
| [ADR-021](021-solution-cache.md) | Solution de Cache | ✅ Accepté | 2025-02-01 |
|
||||
| [ADR-015](015-hebergement.md) | Hébergement | ✅ Accepté | 2025-01-28 |
|
||||
| [ADR-019](019-geolocalisation-ip.md) | Géolocalisation par IP | ✅ Accepté | 2025-01-30 |
|
||||
|
||||
### Streaming & Content
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-002](002-protocole-streaming.md) | Protocole Streaming | ✅ Accepté | 2025-01-18 |
|
||||
| [ADR-003](003-codec-audio.md) | Codec Audio | ✅ Accepté | 2025-01-19 |
|
||||
| [ADR-004](004-cdn.md) | CDN | ✅ Accepté | 2025-01-20 |
|
||||
|
||||
### Security & Auth
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-006](006-chiffrement.md) | Chiffrement | ✅ Accepté | 2025-01-21 |
|
||||
| [ADR-008](008-authentification.md) | Authentification | ✅ Accepté | 2025-01-24 |
|
||||
| [ADR-025](025-securite-secrets.md) | Sécurité & Secrets | ✅ Accepté | 2025-02-04 |
|
||||
|
||||
### Testing & Quality
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-007](007-tests-bdd.md) | Tests BDD | ✅ Accepté | 2025-01-22 |
|
||||
| [ADR-013](013-strategie-tests.md) | Stratégie Tests | ✅ Accepté | 2025-01-27 |
|
||||
| [ADR-022](022-strategie-cicd-monorepo.md) | CI/CD Monorepo | ✅ Accepté | 2025-02-02 |
|
||||
|
||||
### Features & Operations
|
||||
|
||||
| ADR | Titre | Statut | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-009](009-solution-paiement.md) | Solution Paiement | ✅ Accepté | 2025-01-24 |
|
||||
| [ADR-016](016-service-emailing.md) | Service Emailing | ✅ Accepté | 2025-01-29 |
|
||||
| [ADR-017](017-notifications-geolocalisees.md) | Notifications Géolocalisées | ✅ Accepté | 2025-01-30 |
|
||||
| [ADR-018](018-librairies-go.md) | Librairies Go | ✅ Accepté | 2025-01-30 |
|
||||
| [ADR-020](020-librairies-flutter.md) | Librairies Flutter | ✅ Accepté | 2025-01-31 |
|
||||
| [ADR-023](023-architecture-moderation.md) | Architecture Modération | ✅ Accepté | 2025-02-03 |
|
||||
| [ADR-024](024-monitoring-observabilite.md) | Monitoring & Observabilité | ✅ Accepté | 2025-02-03 |
|
||||
|
||||
## Vue d'ensemble technique consolidée
|
||||
|
||||
Pour une vue d'ensemble de l'architecture et de la stack technique, consultez [docs/TECHNICAL.md](../TECHNICAL.md).
|
||||
|
||||
## Légende des statuts
|
||||
|
||||
- ✅ **Accepté** : Décision validée et appliquée
|
||||
- 🟡 **Proposé** : En discussion
|
||||
- ⏸️ **Suspendu** : Temporairement en attente
|
||||
- ❌ **Rejeté** : Décision rejetée (conservée pour historique)
|
||||
- 🔄 **Révisé** : Décision modifiée, voir ADR plus récent
|
||||
|
||||
## Créer un nouvel ADR
|
||||
|
||||
Pour documenter une nouvelle décision architecturale :
|
||||
|
||||
1. Créer un fichier `XXX-titre-court.md` (numérotation séquentielle)
|
||||
2. Utiliser le template suivant :
|
||||
|
||||
```markdown
|
||||
# ADR-XXX : Titre de la décision
|
||||
|
||||
**Statut** : Proposé/Accepté/Rejeté
|
||||
**Date** : YYYY-MM-DD
|
||||
|
||||
## Contexte
|
||||
|
||||
Pourquoi cette décision est nécessaire ? Quel problème résout-elle ?
|
||||
|
||||
## Décision
|
||||
|
||||
Quelle solution avons-nous choisie ?
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
Quelles autres options ont été évaluées ?
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- Avantage 1
|
||||
- Avantage 2
|
||||
|
||||
### Négatives
|
||||
|
||||
- Limitation 1
|
||||
- Compromis accepté
|
||||
|
||||
## Références
|
||||
|
||||
- Liens vers documentation externe
|
||||
- Benchmarks
|
||||
- Articles de référence
|
||||
```
|
||||
|
||||
3. Ajouter l'ADR dans ce fichier README.md et dans `mkdocs.yml`
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-02-07
|
||||
@@ -1,319 +0,0 @@
|
||||
# Diagramme de Séquence : Cache Géospatial Redis
|
||||
|
||||
> Architecture du cache Redis Geospatial pour l'optimisation des requêtes de découverte de contenu géolocalisé.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le cache Redis Geospatial permet d'accélérer la recherche de contenus audio à proximité d'une position GPS en évitant des calculs PostGIS coûteux sur PostgreSQL à chaque requête.
|
||||
|
||||
**Performance** :
|
||||
- Sans cache : ~200-500ms (calcul PostGIS sur 100K points)
|
||||
- Avec cache : ~5-10ms (filtrage Redis en mémoire)
|
||||
|
||||
---
|
||||
|
||||
## Flux complet : Cold Start → Warm Cache
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 📱 Utilisateur<br/>(Paris)
|
||||
participant Backend as 🔧 Backend Go
|
||||
participant Redis as 🔴 Redis Geospatial<br/>(Cache)
|
||||
participant PostgreSQL as 🗄️ PostgreSQL<br/>+ PostGIS
|
||||
participant CDN as 🌐 NGINX Cache (OVH VPS)
|
||||
|
||||
Note over User,CDN: 🥶 Cold Start - Cache vide
|
||||
|
||||
User->>Backend: GET /contents?lat=48.8566&lon=2.3522&radius=50km
|
||||
Backend->>Redis: EXISTS geo:catalog
|
||||
Redis-->>Backend: false (cache vide)
|
||||
|
||||
Backend->>PostgreSQL: SELECT id, lat, lon, title, geo_level<br/>FROM contents WHERE active=true
|
||||
Note over PostgreSQL: Tous les contenus actifs<br/>de la plateforme (métadonnées)
|
||||
PostgreSQL-->>Backend: 100K contenus (métadonnées)
|
||||
|
||||
Backend->>Redis: GEOADD geo:catalog<br/>lon1 lat1 "content:1"<br/>lon2 lat2 "content:2"<br/>... (100K entrées)
|
||||
Note over Redis: Stockage index spatial<br/>en mémoire (~20 MB)
|
||||
Redis-->>Backend: OK (100000)
|
||||
|
||||
Backend->>Redis: EXPIRE geo:catalog 300
|
||||
Note over Redis: TTL 5 minutes
|
||||
|
||||
Backend->>Redis: GEORADIUS geo:catalog<br/>2.3522 48.8566 50 km
|
||||
Note over Redis: Filtrage spatial instantané<br/>(index geohash)
|
||||
Redis-->>Backend: [content:123, content:456, ...]<br/>(~500 IDs dans rayon)
|
||||
|
||||
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE id IN (123, 456, ...)<br/>AND geo_level = 'gps_precise'
|
||||
Note over PostgreSQL: Récupération détails complets<br/>uniquement contenus proches
|
||||
PostgreSQL-->>Backend: Détails complets (500 contenus GPS)
|
||||
|
||||
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE city='Paris' OR dept='75'<br/>OR region='IDF' OR geo_level='national'
|
||||
Note over PostgreSQL: Contenus par niveau géographique
|
||||
PostgreSQL-->>Backend: Contenus ville/région/national
|
||||
|
||||
Backend->>Backend: Scoring & mixage :<br/>- GPS proche : 70%<br/>- Ville : 20%<br/>- Région : 8%<br/>- National : 2%
|
||||
|
||||
Backend-->>User: JSON: [{id, title, creator, audioUrl, score}, ...]<br/>(playlist mixée et scorée)
|
||||
|
||||
Note over User,CDN: 🎵 Lecture audio (requêtes séparées)
|
||||
|
||||
User->>CDN: GET /audio/content-123.m3u8
|
||||
CDN-->>User: Playlist HLS
|
||||
|
||||
User->>CDN: GET /audio/content-123-segment-001.ts
|
||||
CDN-->>User: Segment audio Opus
|
||||
|
||||
Note over User,CDN: 🔥 Warm Cache - Utilisateur 2 à Lyon (45km+)
|
||||
|
||||
participant User2 as 📱 Utilisateur 2<br/>(Lyon)
|
||||
|
||||
User2->>Backend: GET /contents?lat=45.7640&lon=4.8357&radius=50km
|
||||
Backend->>Redis: EXISTS geo:catalog
|
||||
Redis-->>Backend: true ✅ (cache chaud)
|
||||
|
||||
Backend->>Redis: GEORADIUS geo:catalog<br/>4.8357 45.7640 50 km
|
||||
Note over Redis: Filtrage instantané<br/>sur cache existant
|
||||
Redis-->>Backend: [content:789, content:012, ...]<br/>(~300 IDs différents)
|
||||
|
||||
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE id IN (789, 012, ...)<br/>AND geo_level = 'gps_precise'
|
||||
PostgreSQL-->>Backend: Détails complets
|
||||
|
||||
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE city='Lyon' OR dept='69'<br/>OR region='Auvergne-RA' OR geo_level='national'
|
||||
PostgreSQL-->>Backend: Contenus ville/région/national
|
||||
|
||||
Backend->>Backend: Scoring & mixage
|
||||
|
||||
Backend-->>User2: JSON: [{id, title, creator, audioUrl, score}, ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stratégie de cache
|
||||
|
||||
### Cache du catalogue complet (approche choisie)
|
||||
|
||||
**Principe** : Au premier cache miss, charger **TOUS** les contenus géolocalisés en une seule fois dans Redis.
|
||||
|
||||
**Avantages** :
|
||||
- ✅ 1 seul cache miss au démarrage de l'instance
|
||||
- ✅ Toutes les requêtes suivantes servies par Redis (n'importe quelle position GPS)
|
||||
- ✅ Simple à gérer (1 seule clé Redis : `geo:catalog`)
|
||||
- ✅ Pas de duplication de données
|
||||
|
||||
**Inconvénients** :
|
||||
- ⚠️ Premier utilisateur subit le cold start (~500ms-1s)
|
||||
- ⚠️ Nécessite charger toute la base (acceptable : ~20 MB pour 100K contenus)
|
||||
|
||||
**Alternatives non retenues** :
|
||||
- Cache par zone géographique → cache miss fréquents, complexité gestion chevauchements
|
||||
- Cache à la demande → trop de cache miss, pas d'optimisation réelle
|
||||
|
||||
---
|
||||
|
||||
## Détails techniques
|
||||
|
||||
### 1. Données stockées dans Redis
|
||||
|
||||
**Clé Redis** : `geo:catalog`
|
||||
|
||||
**Structure** :
|
||||
```
|
||||
GEOADD geo:catalog
|
||||
2.3522 48.8566 "content:12345" # lon, lat, member
|
||||
4.8357 45.7640 "content:67890"
|
||||
...
|
||||
```
|
||||
|
||||
**Taille mémoire** :
|
||||
- ~200 bytes par entrée (ID + coordonnées + index geohash)
|
||||
- 100K contenus = ~20 MB
|
||||
- Négligeable pour Redis (plusieurs GB RAM disponibles)
|
||||
|
||||
**TTL** : 5 minutes (300 secondes)
|
||||
- Le contenu géolocalisé est quasi-statique (change peu)
|
||||
- Rechargement automatique toutes les 5 minutes si cache expiré
|
||||
- Permet de propager les nouveaux contenus rapidement
|
||||
|
||||
### 2. Niveaux géographiques
|
||||
|
||||
Le cache Redis ne contient que les contenus avec **GPS précis** (`geo_level = 'gps_precise'`).
|
||||
|
||||
Les autres niveaux géographiques sont gérés par filtrage applicatif :
|
||||
|
||||
| Niveau | Stockage | Requête |
|
||||
|--------|----------|---------|
|
||||
| **GPS précis** | Redis + PostgreSQL | `GEORADIUS` puis `SELECT WHERE id IN (...)` |
|
||||
| **Ville** | PostgreSQL uniquement | `SELECT WHERE city = ?` |
|
||||
| **Département** | PostgreSQL uniquement | `SELECT WHERE department = ?` |
|
||||
| **Région** | PostgreSQL uniquement | `SELECT WHERE region = ?` |
|
||||
| **National** | PostgreSQL uniquement | `SELECT WHERE geo_level = 'national'` |
|
||||
|
||||
### 3. Commandes Redis utilisées
|
||||
|
||||
```bash
|
||||
# Vérifier existence du cache
|
||||
EXISTS geo:catalog
|
||||
# Retour : 0 (n'existe pas) ou 1 (existe)
|
||||
|
||||
# Charger le catalogue complet (cold start)
|
||||
GEOADD geo:catalog 2.3522 48.8566 "content:1" 4.8357 45.7640 "content:2" ...
|
||||
# Retour : nombre d'éléments ajoutés
|
||||
|
||||
# Définir TTL 5 minutes
|
||||
EXPIRE geo:catalog 300
|
||||
|
||||
# Rechercher contenus dans un rayon
|
||||
GEORADIUS geo:catalog 2.3522 48.8566 50 km
|
||||
# Retour : ["content:123", "content:456", ...]
|
||||
|
||||
# Optionnel : obtenir distance et coordonnées
|
||||
GEORADIUS geo:catalog 2.3522 48.8566 50 km WITHDIST WITHCOORD
|
||||
# Retour : [["content:123", "12.5", ["2.35", "48.85"]], ...]
|
||||
```
|
||||
|
||||
### 4. Algorithme de scoring
|
||||
|
||||
Le backend mixe les résultats selon une pondération :
|
||||
|
||||
```
|
||||
Score final =
|
||||
(Pertinence GPS × 0.70) +
|
||||
(Pertinence Ville × 0.20) +
|
||||
(Pertinence Région × 0.08) +
|
||||
(Pertinence National × 0.02)
|
||||
```
|
||||
|
||||
**Critères de pertinence** :
|
||||
- **GPS** : Plus proche = score élevé (distance inversée)
|
||||
- **Ville/Région** : Matching exact = score maximal
|
||||
- **National** : Score fixe faible (contenu générique)
|
||||
|
||||
**Mixage playlist** :
|
||||
1. Trier tous les contenus par score décroissant
|
||||
2. Appliquer diversité créateurs (pas 3 contenus du même créateur d'affilée)
|
||||
3. Injecter contenus sponsorisés/mis en avant (futurs)
|
||||
4. Retourner top 50 pour la session d'écoute
|
||||
|
||||
---
|
||||
|
||||
## Métriques de performance
|
||||
|
||||
### Temps de réponse typiques
|
||||
|
||||
| Scénario | Latence | Détail |
|
||||
|----------|---------|--------|
|
||||
| **Cold start** | 500-1000ms | Chargement 100K contenus dans Redis + requête |
|
||||
| **Warm cache** | 5-10ms | `GEORADIUS` + `SELECT WHERE id IN (...)` |
|
||||
| **TTL expiré** | 500-1000ms | Rechargement automatique |
|
||||
|
||||
### Charge serveurs
|
||||
|
||||
| Composant | Sans cache | Avec cache |
|
||||
|-----------|------------|------------|
|
||||
| **PostgreSQL CPU** | 60-80% | 10-20% |
|
||||
| **Redis CPU** | N/A | 5-15% |
|
||||
| **Throughput** | ~50 req/s | ~500 req/s |
|
||||
|
||||
---
|
||||
|
||||
## Cas limites et optimisations futures
|
||||
|
||||
### Cas limite 1 : Contenu très dense (Paris intra-muros)
|
||||
|
||||
**Problème** : 10K contenus dans rayon 5km → trop de résultats
|
||||
|
||||
**Solution actuelle** :
|
||||
- Limiter résultats Redis à 1000 premiers (tri par distance)
|
||||
- Scorer et filtrer côté application
|
||||
|
||||
**Optimisation future** :
|
||||
- Ajuster rayon dynamiquement selon densité
|
||||
- Utiliser `GEORADIUS ... COUNT 500` pour limiter côté Redis
|
||||
|
||||
### Cas limite 2 : Zones rurales (peu de contenu)
|
||||
|
||||
**Problème** : Rayon 50km retourne <10 contenus
|
||||
|
||||
**Solution actuelle** :
|
||||
- Augmenter poids contenus région/national dans le scoring
|
||||
- Suggérer contenus nationaux populaires
|
||||
|
||||
**Optimisation future** :
|
||||
- Augmenter rayon automatiquement jusqu'à obtenir min 20 contenus
|
||||
- `GEORADIUS ... 100 km` si rayon initial insuffisant
|
||||
|
||||
### Cas limite 3 : Nombreux créateurs actifs (évolutivité)
|
||||
|
||||
**Problème** : Cache 100K → 1M contenus (200 MB Redis)
|
||||
|
||||
**Solution actuelle** :
|
||||
- 200 MB reste acceptable pour Redis
|
||||
|
||||
**Optimisation future** :
|
||||
- Sharding géographique : cache Europe, cache USA, etc.
|
||||
- Limiter cache aux contenus actifs 90 derniers jours
|
||||
|
||||
---
|
||||
|
||||
## Invalidation du cache
|
||||
|
||||
### Stratégies d'invalidation
|
||||
|
||||
| Événement | Action cache | Détail |
|
||||
|-----------|--------------|--------|
|
||||
| **Nouveau contenu publié** | Lazy (TTL) | Visible sous 5 minutes max |
|
||||
| **Contenu supprimé/modéré** | Lazy (TTL) | Disparaît sous 5 minutes max |
|
||||
| **Mise à jour GPS contenu** | Lazy (TTL) | Nouvelle position sous 5 minutes |
|
||||
| **Déploiement backend** | Flush volontaire | `DEL geo:catalog` si schema change |
|
||||
|
||||
**Pas d'invalidation immédiate** pour simplifier l'architecture (cohérence éventuelle acceptable).
|
||||
|
||||
**Alternative future** :
|
||||
- Pub/Sub Redis : notifier toutes les instances backend lors d'un changement
|
||||
- `GEOADD geo:catalog 2.35 48.85 "content:new"` pour ajout immédiat
|
||||
|
||||
---
|
||||
|
||||
## Schéma simplifié
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Utilisateur │
|
||||
│ (Position GPS actuelle) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
│ GET /contents?lat=X&lon=Y&radius=Z
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Backend Go (Fiber) │
|
||||
│ │
|
||||
│ 1. Vérifier cache Redis │
|
||||
│ ├─ Cache HIT → GEORADIUS rapide │
|
||||
│ └─ Cache MISS → Charger catalogue complet │
|
||||
│ │
|
||||
│ 2. Filtrer contenus GPS proches (Redis) │
|
||||
│ 3. Récupérer contenus ville/région/national (PG) │
|
||||
│ 4. Scorer et mixer selon pondération │
|
||||
│ 5. Retourner playlist │
|
||||
└────────────┬───────────────────────────┬────────────────┘
|
||||
│ │
|
||||
│ GEORADIUS │ SELECT détails
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌───────────────────────────┐
|
||||
│ Redis Geospatial │ │ PostgreSQL + PostGIS │
|
||||
│ (Index spatial) │ │ (Données complètes) │
|
||||
│ │ │ │
|
||||
│ • geo:catalog │ │ • contents (détails) │
|
||||
│ • TTL 5 min │ │ • users │
|
||||
│ • ~20 MB mémoire │ │ • playlists │
|
||||
└─────────────────────┘ └───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- [ADR-005 : Base de données](../../adr/005-base-de-donnees.md)
|
||||
- [Redis Geospatial Commands](https://redis.io/docs/data-types/geospatial/)
|
||||
- [PostGIS Documentation](https://postgis.net/documentation/)
|
||||
- [Règles métier : Découverte de contenu géolocalisé](../../regles-metier/03-decouverte-contenu.md)
|
||||
@@ -7,6 +7,7 @@
|
||||
## Contexte
|
||||
|
||||
RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android Auto) avec :
|
||||
|
||||
- Contenu généré par utilisateurs (UGC)
|
||||
- Monétisation : publicités géolocalisées + Premium (4.99€ web / 5.99€ IAP)
|
||||
- GPS en arrière-plan
|
||||
@@ -15,6 +16,7 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
## Stratégie de conformité
|
||||
|
||||
**Approche multi-plateforme** avec :
|
||||
|
||||
- Modération UGC robuste (IA + humain)
|
||||
- Prix différenciés selon région (US/EU/Monde)
|
||||
- GPS avec disclosure complète
|
||||
@@ -32,6 +34,7 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
## Conformité détaillée
|
||||
|
||||
### Android Auto / CarPlay ✅
|
||||
|
||||
- 100% audio (pas de vidéo)
|
||||
- Commandes standard au volant
|
||||
- Aucun achat in-car
|
||||
@@ -42,16 +45,19 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
### Google Play ⚠️
|
||||
|
||||
**UGC (critique)** :
|
||||
|
||||
- Modération hybride IA + humain ✅
|
||||
- 3 premiers contenus validés manuellement ✅
|
||||
- Système de strikes (4 = ban) ✅
|
||||
- Signalement + blocage utilisateurs ✅
|
||||
|
||||
**GPS Background (critique)** :
|
||||
|
||||
- Permission "Always Location" = **OPTIONNELLE**
|
||||
- Demandée uniquement pour mode piéton (notifications arrière-plan audio-guides)
|
||||
- Justification Play Console :
|
||||
> "RoadWave permet aux utilisateurs de recevoir des alertes audio-guides lorsqu'ils passent à pied près de monuments/musées, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée dans les paramètres."
|
||||
|
||||
- In-app disclosure obligatoire (écran dédié avant demande permission)
|
||||
- Si refusée : app fonctionne en mode voiture uniquement
|
||||
- **Action** : Remplir formulaire background location Play Console avec justification
|
||||
@@ -68,27 +74,32 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
### App Store ⚠️
|
||||
|
||||
**Prix différenciés (légaux depuis 2025-2026)** :
|
||||
|
||||
- 🇺🇸 US : Lien externe autorisé (0% commission)
|
||||
- 🇪🇺 EU : Paiement externe DMA (7-20% commission réduite)
|
||||
- 🌍 Monde : IAP obligatoire (30% commission)
|
||||
|
||||
**UGC** :
|
||||
|
||||
- Mode Kids obligatoire (filtrage selon âge) ✅
|
||||
- Système de modération + signalement ✅
|
||||
|
||||
**GPS Background (critique)** :
|
||||
|
||||
- Permission "Always Location" = **OPTIONNELLE**
|
||||
- Deux strings Info.plist requises :
|
||||
- `NSLocationWhenInUseUsageDescription` : explication mode voiture
|
||||
- `NSLocationAlwaysAndWhenInUseUsageDescription` : explication mode piéton (optionnel)
|
||||
|
||||
- In-app disclosure obligatoire avant demande "Always"
|
||||
- Flux two-step : When In Use → Always (si user active mode piéton)
|
||||
- Si refusée : app fonctionne en mode voiture uniquement
|
||||
- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
|
||||
### Revenus créateurs
|
||||
|
||||
**Position** : Paiements créateurs = "services" (comme YouTube/Uber), pas IAP
|
||||
|
||||
- Paiement via Mangopay Connect (externe)
|
||||
- Commission stores uniquement sur Premium (IAP)
|
||||
- Comparables : YouTube AdSense, TikTok Creator Fund, Uber
|
||||
@@ -110,12 +121,14 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
## Stratégie de lancement
|
||||
|
||||
**Phase 1 - MVP** :
|
||||
|
||||
- IAP uniquement (5.99€/mois mondial)
|
||||
- Modération UGC active
|
||||
- GPS avec disclosure
|
||||
- CarPlay/Android Auto basique
|
||||
|
||||
**Phase 2 - Post-validation** :
|
||||
|
||||
- Prix différenciés US (lien externe 4.99€)
|
||||
- Paiement externe EU (DMA)
|
||||
- Monétisation créateurs (Mangopay)
|
||||
|
||||
14
docs/docker/mkdocs.Dockerfile
Normal file
14
docs/docker/mkdocs.Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM squidfunk/mkdocs-material:latest
|
||||
|
||||
# Install mkdocs-kroki-plugin
|
||||
RUN pip install --no-cache-dir mkdocs-kroki-plugin
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /docs
|
||||
|
||||
# Expose MkDocs port
|
||||
EXPOSE 8000
|
||||
|
||||
# Default command
|
||||
ENTRYPOINT ["mkdocs"]
|
||||
CMD ["serve", "--dev-addr=0.0.0.0:8000"]
|
||||
@@ -18,4 +18,4 @@ RUN pip install --no-cache-dir \
|
||||
|
||||
WORKDIR /docs
|
||||
|
||||
ENTRYPOINT ["python3", "/docs/scripts/generate-pdf-docs.py"]
|
||||
ENTRYPOINT ["python3", "/docs/docs/scripts/generate-pdf-docs.py"]
|
||||
241
docs/domains/README.md
Normal file
241
docs/domains/README.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Context Map RoadWave
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
RoadWave est organisé selon les principes du **Domain-Driven Design (DDD)** avec **7 bounded contexts** clairs. Cette architecture modulaire permet une meilleure séparation des préoccupations, facilite la maintenance et l'évolution du système.
|
||||
|
||||
## Architecture des domaines
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Core Domain"
|
||||
SHARED[_shared<br/>Authentification, RGPD, Erreurs]
|
||||
end
|
||||
|
||||
subgraph "Supporting Subdomains"
|
||||
RECO[recommendation<br/>Jauges & Algorithme]
|
||||
CONTENT[content<br/>Audio-guides & Live]
|
||||
MODERATION[moderation<br/>Signalements & Sanctions]
|
||||
end
|
||||
|
||||
subgraph "Generic Subdomains"
|
||||
ADS[advertising<br/>Publicités]
|
||||
PREMIUM[premium<br/>Abonnements]
|
||||
MONETIZATION[monetization<br/>Monétisation créateurs]
|
||||
end
|
||||
|
||||
%% Dépendances principales
|
||||
RECO --> SHARED
|
||||
RECO --> CONTENT
|
||||
CONTENT --> SHARED
|
||||
ADS --> SHARED
|
||||
ADS --> RECO
|
||||
PREMIUM --> SHARED
|
||||
PREMIUM --> CONTENT
|
||||
MONETIZATION --> SHARED
|
||||
MONETIZATION --> CONTENT
|
||||
MONETIZATION --> ADS
|
||||
MONETIZATION --> PREMIUM
|
||||
MODERATION --> SHARED
|
||||
MODERATION --> CONTENT
|
||||
|
||||
%% Relations anti-corruption
|
||||
ADS -.-|bloqué par| PREMIUM
|
||||
MODERATION -.->|peut démonétiser| MONETIZATION
|
||||
```
|
||||
|
||||
## Bounded Contexts
|
||||
|
||||
### Core Domain
|
||||
|
||||
#### 🔐 [_shared](/_shared/)
|
||||
**Responsabilité** : Fonctionnalités transversales essentielles
|
||||
|
||||
- Authentification et inscription via Zitadel
|
||||
- Conformité RGPD (consentements, suppression données)
|
||||
- Gestion cohérente des erreurs
|
||||
- Entités centrales : `USERS`, `CONTENTS`, `SUBSCRIPTIONS`, `LISTENING_HISTORY`
|
||||
|
||||
**Utilisé par** : Tous les autres domaines
|
||||
|
||||
---
|
||||
|
||||
### Supporting Subdomains
|
||||
|
||||
#### 🎯 [recommendation](/recommendation/)
|
||||
**Responsabilité** : Recommandation géolocalisée de contenus
|
||||
|
||||
- Jauges de centres d'intérêt (scores dynamiques 0-100)
|
||||
- Algorithme de scoring (distance + affinité)
|
||||
- Adaptation selon interactions utilisateur
|
||||
- Entités : `USER_INTERESTS`, `INTEREST_CATEGORIES`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Ubiquitous Language** : Interest Gauge, Recommendation Score, Geographic Priority, Interest Decay
|
||||
|
||||
---
|
||||
|
||||
#### 🎙️ [content](/content/)
|
||||
**Responsabilité** : Création et diffusion de contenus audio
|
||||
|
||||
- Audio-guides multi-séquences géolocalisés
|
||||
- Radio live et enregistrements
|
||||
- Contenus géolocalisés pour voiture/piéton
|
||||
- Détection de contenu protégé (droits d'auteur)
|
||||
- Entités : `AUDIO_GUIDES`, `LIVE_STREAMS`, `GUIDE_SEQUENCES`, `LIVE_RECORDINGS`
|
||||
|
||||
**Dépend de** : `_shared`
|
||||
|
||||
**Interagit avec** : `moderation` (modération), `monetization` (revenus)
|
||||
|
||||
**Ubiquitous Language** : Audio Guide, Guide Sequence, Live Stream, Geofence, Content Fingerprint
|
||||
|
||||
---
|
||||
|
||||
#### 🛡️ [moderation](/moderation/)
|
||||
**Responsabilité** : Modération et sécurité de la plateforme
|
||||
|
||||
- Workflow de traitement des signalements
|
||||
- Système de strikes et sanctions
|
||||
- Processus d'appel
|
||||
- Badges de confiance créateurs
|
||||
- Modération communautaire
|
||||
- Entités : `REPORTS`, `SANCTIONS`, `APPEALS`, `STRIKES`, `BADGES`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Peut affecter** : `monetization` (démonétisation)
|
||||
|
||||
**Ubiquitous Language** : Report, Strike, Sanction, Appeal, Trust Badge, Community Moderation
|
||||
|
||||
---
|
||||
|
||||
### Generic Subdomains
|
||||
|
||||
#### 📢 [advertising](/advertising/)
|
||||
**Responsabilité** : Publicités audio géociblées
|
||||
|
||||
- Campagnes publicitaires
|
||||
- Ciblage géographique et par intérêts
|
||||
- Métriques (impressions, CPM)
|
||||
- Insertion dynamique dans flux audio
|
||||
- Entités : `AD_CAMPAIGNS`, `AD_METRICS`, `AD_IMPRESSIONS`
|
||||
|
||||
**Dépend de** : `_shared`, `recommendation` (ciblage)
|
||||
|
||||
**Bloqué par** : `premium` (pas de pub pour abonnés)
|
||||
|
||||
**Ubiquitous Language** : Ad Campaign, Ad Impression, CPM, Ad Targeting, Skip Rate
|
||||
|
||||
---
|
||||
|
||||
#### 💎 [premium](/premium/)
|
||||
**Responsabilité** : Abonnements et fonctionnalités premium
|
||||
|
||||
- Abonnements payants (mensuel/annuel)
|
||||
- Mode offline (téléchargement, synchro)
|
||||
- Notifications personnalisées
|
||||
- Avantages : sans pub, qualité audio supérieure
|
||||
- Entités : `PREMIUM_SUBSCRIPTIONS`, `ACTIVE_STREAMS`, `OFFLINE_DOWNLOADS`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Bloque** : `advertising` (désactivation pubs)
|
||||
|
||||
**Ubiquitous Language** : Premium Subscription, Offline Download, Sync Queue, Premium Tier, Auto-Renewal
|
||||
|
||||
---
|
||||
|
||||
#### 💰 [monetization](/monetization/)
|
||||
**Responsabilité** : Monétisation des créateurs
|
||||
|
||||
- KYC (vérification identité)
|
||||
- Calcul des revenus (pub + abonnements)
|
||||
- Versements mensuels via Mangopay
|
||||
- Tableaux de bord revenus
|
||||
- Entités : `CREATOR_MONETIZATION`, `REVENUES`, `PAYOUTS`
|
||||
|
||||
**Dépend de** : `_shared`, `content`, `advertising`, `premium`
|
||||
|
||||
**Affecté par** : `moderation` (démonétisation en cas de sanction)
|
||||
|
||||
**Ubiquitous Language** : Revenue Share, KYC Verification, Payout, Minimum Threshold
|
||||
|
||||
---
|
||||
|
||||
## Relations entre domaines
|
||||
|
||||
### Upstream/Downstream
|
||||
|
||||
| Upstream (Fournisseur) | Downstream (Consommateur) | Type de relation |
|
||||
|------------------------|---------------------------|------------------|
|
||||
| `_shared` | Tous | **Kernel partagé** |
|
||||
| `content` | `recommendation` | **Customer/Supplier** |
|
||||
| `recommendation` | `advertising` | **Customer/Supplier** |
|
||||
| `premium` | `advertising` | **Anti-Corruption Layer** |
|
||||
|
||||
### Événements de domaine
|
||||
|
||||
Les domaines communiquent via des événements métier :
|
||||
|
||||
- **UserRegistered** (`_shared` → tous) : Nouvel utilisateur
|
||||
- **ContentPublished** (`content` → `recommendation`, `moderation`) : Nouveau contenu
|
||||
- **InterestGaugeUpdated** (`recommendation` → `advertising`) : Mise à jour jauges
|
||||
- **UserBanned** (`moderation` → `monetization`) : Bannissement utilisateur
|
||||
- **SubscriptionActivated** (`premium` → `advertising`) : Activation premium
|
||||
|
||||
## Structure de la documentation
|
||||
|
||||
Chaque domaine suit cette organisation :
|
||||
|
||||
```
|
||||
domains/<domain>/
|
||||
├── README.md # Vue d'ensemble du domaine
|
||||
├── rules/ # Règles métier (*.md)
|
||||
├── entities/ # Diagrammes entités (*.md)
|
||||
├── sequences/ # Diagrammes séquences (*.md)
|
||||
├── states/ # Diagrammes états (*.md)
|
||||
├── flows/ # Diagrammes flux (*.md)
|
||||
└── features/ # Tests BDD Gherkin (*.feature)
|
||||
```
|
||||
|
||||
## Ubiquitous Language Global
|
||||
|
||||
**Termes transversaux utilisés dans tous les domaines** :
|
||||
|
||||
- **User** : Utilisateur (auditeur, créateur, ou les deux)
|
||||
- **Content** : Contenu audio diffusé sur la plateforme
|
||||
- **Creator** : Utilisateur créant du contenu
|
||||
- **Geolocation** : Position GPS de l'utilisateur
|
||||
- **Stream** : Flux de lecture audio
|
||||
- **Subscription** : Abonnement (à un créateur ou à premium)
|
||||
- **Interest** : Centre d'intérêt (automobile, voyage, musique, etc.)
|
||||
|
||||
## Principes d'architecture
|
||||
|
||||
1. **Bounded Contexts clairs** : Chaque domaine a des limites bien définies
|
||||
2. **Autonomie des domaines** : Chaque domaine peut évoluer indépendamment
|
||||
3. **Communication asynchrone** : Préférence pour les événements vs appels directs
|
||||
4. **Anti-Corruption Layer** : Protection contre les changements externes
|
||||
5. **Alignment code/docs** : Structure docs ↔ structure `backend/internal/`
|
||||
|
||||
## Alignement avec le code backend
|
||||
|
||||
```
|
||||
backend/internal/ docs/domains/
|
||||
├── auth/ ←→ _shared/
|
||||
├── user/ ←→ _shared/
|
||||
├── content/ ←→ content/
|
||||
├── geo/ ←→ recommendation/
|
||||
├── streaming/ ←→ content/
|
||||
├── moderation/ ←→ moderation/
|
||||
├── payment/ ←→ monetization/
|
||||
└── analytics/ ←→ recommendation/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-02-07
|
||||
**Statut** : ✅ Active
|
||||
**Auteur** : Documentation DDD initiative
|
||||
38
docs/domains/_shared/README.md
Normal file
38
docs/domains/_shared/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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/vue-ensemble.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)
|
||||
57
docs/domains/_shared/entities/account-deletions.md
Normal file
57
docs/domains/_shared/entities/account-deletions.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Account Deletions
|
||||
|
||||
📖 Suppressions de compte avec grace period 30 jours (Article 17 RGPD)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
email varchar(255)
|
||||
status varchar(20)
|
||||
}
|
||||
|
||||
Table account_deletions {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, unique, ref: - users.id, note: 'One-to-one: un user ne peut avoir qu une seule demande active']
|
||||
status deletion_status_enum [not null, default: 'pending']
|
||||
cancellation_token varchar(64) [unique, note: 'Token dans email pour annuler (expire après 30j)']
|
||||
requested_at timestamp [not null, default: `now()`]
|
||||
effective_at timestamp [not null, note: 'Auto-calculated: requested_at + 30 days']
|
||||
cancelled_at timestamp [note: 'Timestamp annulation via lien email (NULL si non annulé)']
|
||||
deleted_at timestamp [note: 'Timestamp suppression effective (NULL si pending/cancelled)']
|
||||
deletion_reason text [note: 'Raison optionnelle fournie par l utilisateur']
|
||||
deleted_data_summary jsonb [note: 'Résumé des données supprimées (audit trail)']
|
||||
|
||||
indexes {
|
||||
(user_id) [unique]
|
||||
(status, effective_at) [note: 'Daily cron job: WHERE status = pending AND effective_at < NOW()']
|
||||
(cancellation_token) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Enum deletion_status_enum {
|
||||
pending [note: 'Grace period actif (30j), compte désactivé, annulation possible']
|
||||
cancelled [note: 'Utilisateur a annulé via lien email']
|
||||
completed [note: 'Suppression effective réalisée après 30j']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Statuts** :
|
||||
|
||||
- `pending`: Grace period actif (30j), compte désactivé, annulation possible
|
||||
- `cancelled`: Utilisateur a annulé via lien email
|
||||
- `completed`: Suppression effective réalisée après 30j
|
||||
|
||||
**Processus** :
|
||||
1. Demande → compte désactivé, contenus cachés
|
||||
2. Email avec `cancellation_token` (valide 30j)
|
||||
3. Si annulation → `status = cancelled`, compte réactivé
|
||||
4. Si 30j écoulés → job cron supprime données, anonymise contenus
|
||||
|
||||
**Données supprimées** :
|
||||
|
||||
- Profil utilisateur, historique GPS/écoute, sessions
|
||||
- Contenus créés : anonymisés (`créateur = "Utilisateur supprimé"`)
|
||||
78
docs/domains/_shared/entities/breach-incidents.md
Normal file
78
docs/domains/_shared/entities/breach-incidents.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Breach Incidents
|
||||
|
||||
📖 Registre violations de données (Article 33 RGPD)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table breach_incidents {
|
||||
id uuid [primary key]
|
||||
severity breach_severity_enum [not null]
|
||||
description text [not null, note: 'Description détaillée de l incident']
|
||||
data_categories_affected jsonb [not null, note: 'Array: ["gps", "email", "listening_history"]']
|
||||
estimated_users_count int [not null, note: 'Estimation nombre users impactés']
|
||||
detected_at timestamp [not null, default: `now()`, note: 'H+0: Détection initiale']
|
||||
contained_at timestamp [note: 'Timestamp confinement de la faille']
|
||||
cnil_notified_at timestamp [note: 'H+48: Notification CNIL si requis']
|
||||
users_notified_at timestamp [note: 'H+72: Notification users si risque élevé']
|
||||
mitigation_actions text [note: 'Actions correctives mises en place']
|
||||
cnil_notification_required boolean [not null, default: false]
|
||||
user_notification_required boolean [not null, default: false]
|
||||
|
||||
indexes {
|
||||
(severity, detected_at) [note: 'Incidents par gravité et chronologie']
|
||||
(cnil_notification_required, cnil_notified_at) [note: 'Track CNIL notification compliance']
|
||||
}
|
||||
}
|
||||
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table breach_affected_users {
|
||||
id uuid [primary key]
|
||||
breach_id uuid [not null, ref: > breach_incidents.id]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
notified_at timestamp [note: 'Timestamp notification user (NULL si pas encore notifié)']
|
||||
notification_channel notification_channel_enum [note: 'Canal utilisé pour notifier']
|
||||
|
||||
indexes {
|
||||
(breach_id, user_id) [unique, note: 'Un user ne peut être listé qu une fois par incident']
|
||||
(breach_id, notified_at) [note: 'Track notification progress']
|
||||
(user_id) [note: 'Historique incidents pour un user']
|
||||
}
|
||||
}
|
||||
|
||||
Enum breach_severity_enum {
|
||||
low [note: 'Pas de notification requise (mesures techniques suffisantes)']
|
||||
medium [note: 'Notification CNIL uniquement']
|
||||
high [note: 'Notification CNIL + utilisateurs']
|
||||
critical [note: 'Notification immédiate tous canaux + SMS fondateur']
|
||||
}
|
||||
|
||||
Enum notification_channel_enum {
|
||||
email [note: 'Email notification']
|
||||
push [note: 'Push notification mobile']
|
||||
sms [note: 'SMS (critical only)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Sévérité** :
|
||||
|
||||
- `low`: Pas de notification requise (mesures techniques suffisantes)
|
||||
- `medium`: Notification CNIL uniquement
|
||||
- `high`: Notification CNIL + utilisateurs
|
||||
- `critical`: Notification immédiate tous canaux + SMS fondateur
|
||||
|
||||
**Timeline 72h** :
|
||||
|
||||
- H+0 : Détection, confinement
|
||||
- H+24 : Évaluation gravité
|
||||
- H+48 : Notification CNIL si requis
|
||||
- H+72 : Notification utilisateurs si risque élevé
|
||||
|
||||
**Catégories de données** :
|
||||
|
||||
- `data_categories_affected`: JSON `["gps", "email", "listening_history"]`
|
||||
56
docs/domains/_shared/entities/consents.md
Normal file
56
docs/domains/_shared/entities/consents.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# User Consents
|
||||
|
||||
📖 Consentements RGPD avec historique et versioning
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table user_consents {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
consent_type consent_type_enum [not null]
|
||||
consent_version varchar(10) [not null, note: 'Format: v1.0, v2.0, etc.']
|
||||
accepted boolean [not null, note: 'true = opted-in, false = opted-out']
|
||||
given_at timestamp [not null, default: `now()`]
|
||||
ip_address inet [not null, note: 'Proof of consent for CNIL audits']
|
||||
user_agent text [not null, note: 'Device/browser proof']
|
||||
|
||||
indexes {
|
||||
(user_id, consent_type, consent_version) [note: 'Latest consent per type']
|
||||
(user_id, given_at) [note: 'Consent history timeline']
|
||||
}
|
||||
}
|
||||
|
||||
Enum consent_type_enum {
|
||||
geolocation_precise [note: 'Géolocalisation GPS précise (obligatoire pour contenu hyperlocal)']
|
||||
analytics [note: 'Analytics Matomo (optionnel)']
|
||||
push_notifications [note: 'Notifications push (optionnel)']
|
||||
cookies_analytics [note: 'Cookies analytiques (optionnel)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Types de consentement** :
|
||||
|
||||
- `geolocation_precise` : Géolocalisation GPS précise (obligatoire pour contenu hyperlocal)
|
||||
- `analytics` : Analytics Matomo (optionnel)
|
||||
- `push_notifications` : Notifications push (optionnel)
|
||||
- `cookies_analytics` : Cookies analytiques (optionnel)
|
||||
|
||||
**Versioning** :
|
||||
|
||||
- Chaque changement de CGU/politique = nouvelle version
|
||||
- Historique complet conservé (preuve légale)
|
||||
- Format version : `v1.0`, `v2.0`, etc.
|
||||
|
||||
**Conformité RGPD** :
|
||||
|
||||
- Granularité : fonctionnel / analytique / marketing
|
||||
- Consentement libre et éclairé
|
||||
- Révocable à tout moment
|
||||
- Historique = preuve en cas de contrôle CNIL
|
||||
56
docs/domains/_shared/entities/data-retention-logs.md
Normal file
56
docs/domains/_shared/entities/data-retention-logs.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Data Retention Logs
|
||||
|
||||
📖 Logs purges automatiques inactivité (Article 5 RGPD - Minimisation)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table data_retention_logs {
|
||||
id uuid [primary key]
|
||||
action_type retention_action_enum [not null]
|
||||
users_processed int [not null, default: 0, note: 'Nombre total users analysés']
|
||||
users_warned int [not null, default: 0, note: 'Nombre users notifiés (90j/30j/7j)']
|
||||
users_deleted int [not null, default: 0, note: 'Nombre users supprimés effectivement']
|
||||
details jsonb [note: 'Détails: threshold_date, user_ids_deleted, notifications_sent']
|
||||
executed_at timestamp [not null, default: `now()`, note: 'Timestamp exécution du job cron']
|
||||
execution_duration_ms bigint [not null, note: 'Durée d exécution en millisecondes']
|
||||
|
||||
indexes {
|
||||
(action_type, executed_at) [note: 'Historique jobs par type']
|
||||
(executed_at) [note: 'Timeline complète des jobs']
|
||||
}
|
||||
}
|
||||
|
||||
Enum retention_action_enum {
|
||||
check_inactive [note: 'Vérification quotidienne comptes inactifs > 5 ans']
|
||||
send_warnings [note: 'Envoi notifications (90j/30j/7j avant suppression)']
|
||||
delete_accounts [note: 'Suppression effective comptes inactifs']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Action types** :
|
||||
|
||||
- `check_inactive`: Vérification quotidienne comptes inactifs > 5 ans
|
||||
- `send_warnings`: Envoi notifications (90j/30j/7j avant suppression)
|
||||
- `delete_accounts`: Suppression effective comptes inactifs
|
||||
|
||||
**Règles de conservation** :
|
||||
|
||||
- Auditeur : 5 ans inactivité → suppression
|
||||
- Créateur actif : jamais (tant que contenus écoutés)
|
||||
- Créateur inactif : 5 ans + 2 ans sans écoute → suppression
|
||||
|
||||
**Details JSON** :
|
||||
```json
|
||||
{
|
||||
"threshold_date": "2021-02-08",
|
||||
"user_ids_deleted": ["uuid1", "uuid2"],
|
||||
"notifications_sent": {
|
||||
"90_days": 15,
|
||||
"30_days": 8,
|
||||
"7_days": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
65
docs/domains/_shared/entities/devices.md
Normal file
65
docs/domains/_shared/entities/devices.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Devices
|
||||
|
||||
📖 Appareils de confiance et gestion multi-device
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table devices {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
device_name varchar(255) [note: 'User-defined device name']
|
||||
os varchar(50) [note: 'iOS, Android, Windows, macOS, Linux']
|
||||
browser varchar(50) [note: 'Safari, Chrome, Firefox, etc.']
|
||||
device_type device_type_enum [not null, note: 'mobile, tablet, desktop, car']
|
||||
is_trusted boolean [not null, default: false, note: 'Bypass 2FA for 30 days if true']
|
||||
trusted_until timestamp [note: 'NULL if not trusted, expires after 30 days']
|
||||
first_seen_at timestamp [not null, default: `now()`]
|
||||
last_seen_at timestamp [not null, default: `now()`]
|
||||
last_ip inet [not null]
|
||||
last_city varchar(100)
|
||||
last_country_code char(2)
|
||||
|
||||
indexes {
|
||||
(user_id, last_seen_at) [note: 'List user devices by recent activity']
|
||||
(user_id, is_trusted) [note: 'Find trusted devices for user']
|
||||
}
|
||||
}
|
||||
|
||||
Table sessions {
|
||||
id uuid [primary key]
|
||||
device_id uuid [ref: > devices.id]
|
||||
}
|
||||
|
||||
Enum device_type_enum {
|
||||
mobile [note: 'Smartphone Android/iOS']
|
||||
tablet [note: 'Tablette']
|
||||
desktop [note: 'Ordinateur']
|
||||
car [note: 'Système embarqué (CarPlay/Android Auto)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Types d'appareil** :
|
||||
|
||||
- `mobile` : Smartphone Android/iOS
|
||||
- `tablet` : Tablette
|
||||
- `desktop` : Ordinateur
|
||||
- `car` : Système embarqué (CarPlay/Android Auto)
|
||||
|
||||
**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
|
||||
|
||||
**Sécurité** :
|
||||
|
||||
- Détection automatique nouveau device → notification push + email
|
||||
- Localisation suspecte (pays différent) → alerte
|
||||
- Révocation individuelle ou globale possible
|
||||
77
docs/domains/_shared/entities/exports.md
Normal file
77
docs/domains/_shared/entities/exports.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Data Exports
|
||||
|
||||
📖 Exports de données utilisateur (portabilité RGPD Article 20)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table data_exports {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
status export_status_enum [not null, default: 'pending']
|
||||
export_url varchar(512) [note: 'S3/CDN signed URL (NULL until generated)']
|
||||
size_bytes bigint [note: 'File size in bytes (NULL until generated)']
|
||||
format export_format_enum [not null, default: 'json']
|
||||
requested_at timestamp [not null, default: `now()`]
|
||||
generated_at timestamp [note: 'When export file was created (NULL if pending/generating)']
|
||||
expires_at timestamp [note: 'Auto-calculated: generated_at + 7 days']
|
||||
downloaded_at timestamp [note: 'First download timestamp (NULL if not yet downloaded)']
|
||||
|
||||
indexes {
|
||||
(user_id, requested_at) [note: 'User export history']
|
||||
(status, requested_at) [note: 'Background worker queue (WHERE status = pending)']
|
||||
(expires_at) [note: 'Daily cleanup job (DELETE WHERE expires_at < NOW())']
|
||||
}
|
||||
}
|
||||
|
||||
Enum export_status_enum {
|
||||
pending [note: 'Demande en file d attente']
|
||||
generating [note: 'Génération en cours (worker background)']
|
||||
ready [note: 'Export disponible au téléchargement']
|
||||
downloaded [note: 'Export téléchargé par l utilisateur']
|
||||
expired [note: 'Export expiré (supprimé automatiquement)']
|
||||
}
|
||||
|
||||
Enum export_format_enum {
|
||||
json [note: 'Machine-readable (données brutes)']
|
||||
html [note: 'Human-readable (page web stylée)']
|
||||
zip [note: 'Archive complète (JSON + HTML + audio files)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Formats d'export** :
|
||||
|
||||
- `json` : Machine-readable (données brutes)
|
||||
- `html` : Human-readable (page web stylée)
|
||||
- `zip` : Archive complète (JSON + HTML + audio files)
|
||||
|
||||
**Contenu de l'export** :
|
||||
|
||||
- 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 RGPD
|
||||
|
||||
**Statuts** :
|
||||
|
||||
- `pending` : Demande en file d'attente
|
||||
- `generating` : Génération en cours (worker background)
|
||||
- `ready` : Export disponible au téléchargement
|
||||
- `downloaded` : Export téléchargé par l'utilisateur
|
||||
- `expired` : Export expiré (supprimé automatiquement)
|
||||
|
||||
**Règles** :
|
||||
|
||||
- Génération asynchrone (worker background)
|
||||
- Délai max : **48h** (conformité RGPD)
|
||||
- Conservation : **7 jours** après génération
|
||||
- Limite : **1 export/mois** (anti-abus)
|
||||
- Notification par email avec lien de téléchargement
|
||||
67
docs/domains/_shared/entities/interest-gauges.md
Normal file
67
docs/domains/_shared/entities/interest-gauges.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Interest Gauges
|
||||
|
||||
📖 Jauges de centres d'intérêt dynamiques pour recommandation personnalisée
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table interest_gauges {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
category interest_category_enum [not null]
|
||||
score decimal(5,2) [not null, default: 0, note: 'Range: 0.00 to 100.00']
|
||||
last_updated timestamp [not null, default: `now()`]
|
||||
interactions_count int [not null, default: 0, note: 'Total interactions for this category']
|
||||
|
||||
indexes {
|
||||
(user_id, category) [unique, note: 'One gauge per user per category']
|
||||
(user_id, score) [note: 'Order categories by score for recommendations']
|
||||
}
|
||||
}
|
||||
|
||||
Enum interest_category_enum {
|
||||
automobile [note: 'Voitures, mécanique, course automobile']
|
||||
travel [note: 'Voyages, tourisme, découverte']
|
||||
music [note: 'Musique, concerts, artistes']
|
||||
news [note: 'Actualités, politique, économie']
|
||||
sport [note: 'Sports, événements sportifs']
|
||||
culture [note: 'Cinéma, livres, expositions']
|
||||
food [note: 'Gastronomie, restaurants, recettes']
|
||||
tech [note: 'Technologie, innovation, gadgets']
|
||||
history [note: 'Histoire, patrimoine, musées']
|
||||
nature [note: 'Nature, randonnée, écologie']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Catégories** :
|
||||
|
||||
- `automobile` : Voitures, mécanique, course automobile
|
||||
- `travel` : Voyages, tourisme, découverte
|
||||
- `music` : Musique, concerts, artistes
|
||||
- `news` : Actualités, politique, économie
|
||||
- `sport` : Sports, événements sportifs
|
||||
- `culture` : Cinéma, livres, expositions
|
||||
- `food` : Gastronomie, restaurants, recettes
|
||||
- `tech` : Technologie, innovation, gadgets
|
||||
- `history` : Histoire, patrimoine, musées
|
||||
- `nature` : Nature, randonnée, écologie
|
||||
|
||||
**Score** :
|
||||
|
||||
- Échelle : **0-100**
|
||||
- Augmentation : +2% par like, +5% par abonnement créateur
|
||||
- Diminution : -1% par skip rapide (<30s), -5% par signalement
|
||||
- Calcul combiné : Distance GPS + matching intérêts
|
||||
|
||||
**Algorithme recommandation** :
|
||||
|
||||
- **70% géolocalisation** : Proximité GPS
|
||||
- **30% centres d'intérêt** : Score jauges
|
||||
- Boost si créateur suivi : +0.3 au score final
|
||||
- Limite : 6 contenus/heure pour éviter spam
|
||||
66
docs/domains/_shared/entities/location-history.md
Normal file
66
docs/domains/_shared/entities/location-history.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Location History
|
||||
|
||||
📖 Historique de géolocalisation avec anonymisation automatique
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table location_history {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
location geography [note: 'PostGIS geography type: POINT with SRID 4326 (WGS84)']
|
||||
geohash varchar(12) [note: 'Precision 5 geohash (~5km²) after anonymization']
|
||||
anonymized boolean [not null, default: false, note: 'true after 24h auto-anonymization']
|
||||
context location_context_enum [not null]
|
||||
speed_kmh float [note: 'GPS speed in km/h (NULL if stationary)']
|
||||
accuracy_meters float [not null, note: 'GPS accuracy radius in meters']
|
||||
created_at timestamp [not null, default: `now()`]
|
||||
anonymized_at timestamp [note: 'When precise location was replaced by geohash']
|
||||
|
||||
indexes {
|
||||
(user_id, created_at) [note: 'User location timeline']
|
||||
(created_at, anonymized) [note: 'Daily anonymization job (WHERE anonymized = false AND created_at < NOW() - 24h)']
|
||||
(location) [type: gist, note: 'PostGIS spatial index for proximity queries']
|
||||
(geohash) [note: 'Analytics queries on anonymized data']
|
||||
}
|
||||
}
|
||||
|
||||
Enum location_context_enum {
|
||||
listening [note: 'Position pendant écoute de contenu']
|
||||
search [note: 'Position lors d une recherche']
|
||||
background [note: 'Tracking en arrière-plan']
|
||||
manual [note: 'Position partagée manuellement']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Anonymisation progressive** :
|
||||
|
||||
- Données précises conservées **24h** (recommandation personnalisée)
|
||||
- Après 24h : conversion en **geohash précision 5** (~5km²)
|
||||
- Coordonnées originales supprimées définitivement
|
||||
- Job quotidien PostGIS automatique
|
||||
|
||||
**Exceptions** :
|
||||
|
||||
- Historique personnel visible (liste trajets) : conservation intégrale tant que compte actif
|
||||
- Analytics globales : uniquement geohash anonyme
|
||||
- Suppression complète si suppression du compte
|
||||
|
||||
**Contexte** :
|
||||
|
||||
- `listening` : Position pendant écoute de contenu
|
||||
- `search` : Position lors d'une recherche
|
||||
- `background` : Tracking en arrière-plan
|
||||
- `manual` : Position partagée manuellement
|
||||
|
||||
**Conformité RGPD** :
|
||||
|
||||
- Vraie anonymisation (CNIL compliant)
|
||||
- Permet analytics agrégées (heatmaps trafic)
|
||||
- PostGIS natif, 0€
|
||||
61
docs/domains/_shared/entities/parental-consents.md
Normal file
61
docs/domains/_shared/entities/parental-consents.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Parental Consents
|
||||
|
||||
📖 Consentements parentaux pour utilisateurs 13-15 ans (Article 8 RGPD)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
birthdate date [not null]
|
||||
}
|
||||
|
||||
Table parental_consents {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, unique, ref: > users.id, note: 'Ado 13-15 ans (1 consent par user max)']
|
||||
parent_email varchar(255) [not null, note: 'Email du parent pour validation']
|
||||
validation_token varchar(64) [unique, note: 'Token de validation envoyé par email (expire 7j)']
|
||||
validated boolean [not null, default: false, note: 'true après clic parent sur lien email']
|
||||
token_expires_at timestamp [not null, note: 'validation_token expire après 7 jours']
|
||||
validated_at timestamp [note: 'Timestamp de validation parent (NULL si non validé)']
|
||||
parent_ip inet [note: 'IP du parent lors de la validation']
|
||||
parent_user_agent text [note: 'User agent parent (preuve validation)']
|
||||
revoked_at timestamp [note: 'Révocation du consentement parental']
|
||||
revocation_reason text [note: 'Raison de la révocation (optionnel)']
|
||||
|
||||
indexes {
|
||||
(user_id) [unique, note: 'Un seul consentement parental actif par user']
|
||||
(validation_token) [unique, note: 'Lookup rapide pour validation lien email']
|
||||
(validated, token_expires_at) [note: 'Cleanup des tokens expirés non validés']
|
||||
}
|
||||
}
|
||||
|
||||
Table parental_controls {
|
||||
id uuid [primary key]
|
||||
parental_consent_id uuid [not null, unique, ref: - parental_consents.id, note: 'One-to-one relationship']
|
||||
gps_enabled boolean [not null, default: false, note: 'Autoriser GPS précis (false = GeoIP uniquement)']
|
||||
messaging_enabled boolean [not null, default: false, note: 'Autoriser messagerie privée']
|
||||
content_16plus_enabled boolean [not null, default: false, note: 'Autoriser contenu 16+']
|
||||
weekly_digest_config jsonb [note: 'Config notifications hebdo parent (email, contenu, format)']
|
||||
updated_at timestamp [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(parental_consent_id) [unique]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Workflow** :
|
||||
1. Ado saisit email parent → `validation_token` généré (expire 7j)
|
||||
2. Parent clique lien → `validated = true`
|
||||
3. Parent configure `PARENTAL_CONTROLS`
|
||||
4. Révocation possible → `revoked_at` renseigné
|
||||
|
||||
**Restrictions par défaut (13-15 ans)** :
|
||||
|
||||
- `gps_enabled`: `false` (GeoIP uniquement)
|
||||
- `messaging_enabled`: `false`
|
||||
- `content_16plus_enabled`: `false`
|
||||
- Dashboard parent : notifications hebdomadaires activité
|
||||
54
docs/domains/_shared/entities/privacy-policy-versions.md
Normal file
54
docs/domains/_shared/entities/privacy-policy-versions.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Privacy Policy Versions
|
||||
|
||||
📖 Versioning politique de confidentialité (Articles 13-14 RGPD)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table privacy_policy_versions {
|
||||
id uuid [primary key]
|
||||
version varchar(10) [not null, unique, note: 'Format: v1.0, v2.0, etc.']
|
||||
content_markdown text [not null, note: 'Source: docs/legal/politique-confidentialite.md (versionné Git)']
|
||||
major_change boolean [not null, default: false, note: 'true = popup obligatoire pour tous les users']
|
||||
changelog text [not null, note: 'Résumé des changements pour communication']
|
||||
effective_date timestamp [not null, note: 'Date d entrée en vigueur de cette version']
|
||||
created_at timestamp [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(version) [unique]
|
||||
(effective_date) [note: 'Order versions chronologically']
|
||||
}
|
||||
}
|
||||
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
}
|
||||
|
||||
Table user_policy_acceptances {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
policy_version_id uuid [not null, ref: > privacy_policy_versions.id]
|
||||
accepted boolean [not null, note: 'true = accepté, false = refusé (compte gelé)']
|
||||
accepted_at timestamp [not null, default: `now()`]
|
||||
ip_address inet [not null, note: 'IP de l utilisateur lors de l acceptation (preuve CNIL)']
|
||||
|
||||
indexes {
|
||||
(user_id, policy_version_id) [unique, note: 'Un user ne peut accepter qu une fois une version']
|
||||
(user_id, accepted_at) [note: 'Historique acceptations user']
|
||||
(policy_version_id) [note: 'Count acceptances par version']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Versioning** :
|
||||
|
||||
- `major_change`: `true` → popup obligatoire pour tous les utilisateurs
|
||||
- `major_change`: `false` → notification simple
|
||||
- Fichier source : `docs/legal/politique-confidentialite.md` (versionné Git)
|
||||
|
||||
**Popup si changement majeur** :
|
||||
|
||||
- Utilisateur doit accepter nouvelle version pour continuer
|
||||
- Refus → compte gelé (lecture seule)
|
||||
105
docs/domains/_shared/entities/reports.md
Normal file
105
docs/domains/_shared/entities/reports.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Reports
|
||||
|
||||
📖 Signalements de contenu et workflow de modération
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
username varchar(50)
|
||||
}
|
||||
|
||||
Table contents {
|
||||
id uuid [primary key]
|
||||
title varchar(255)
|
||||
user_id uuid [not null]
|
||||
}
|
||||
|
||||
Table reports {
|
||||
id uuid [primary key]
|
||||
content_id uuid [not null, ref: > contents.id, note: 'Content being reported']
|
||||
reporter_id uuid [not null, ref: > users.id, note: 'User who filed the report']
|
||||
moderator_id uuid [ref: > users.id, note: 'Moderator assigned to review (NULL if pending)']
|
||||
category report_category_enum [not null]
|
||||
status report_status_enum [not null, default: 'pending']
|
||||
comment text [note: 'Reporter explanation (mandatory for "other" category)']
|
||||
evidence_url varchar(512) [note: 'Screenshot or additional proof URL']
|
||||
reported_at timestamp [not null, default: `now()`]
|
||||
reviewed_at timestamp [note: 'When moderator reviewed the report']
|
||||
moderator_notes text [note: 'Internal moderator notes']
|
||||
action_taken report_action_enum [note: 'Action decided by moderator']
|
||||
|
||||
indexes {
|
||||
(content_id, status) [note: 'Find all reports for a content']
|
||||
(status, reported_at) [note: 'Queue for moderators (pending first)']
|
||||
(reporter_id) [note: 'User report history (detect abuse)']
|
||||
(moderator_id) [note: 'Reports assigned to moderator']
|
||||
}
|
||||
}
|
||||
|
||||
Enum report_category_enum {
|
||||
spam [note: 'Contenu publicitaire non sollicité']
|
||||
hate_speech [note: 'Discours haineux, discrimination']
|
||||
violence [note: 'Violence explicite']
|
||||
sexual_content [note: 'Contenu sexuel inapproprié']
|
||||
misinformation [note: 'Désinformation, fake news']
|
||||
copyright [note: 'Violation de droits d auteur']
|
||||
wrong_age_rating [note: 'Classification d âge incorrecte']
|
||||
other [note: 'Autre raison (commentaire obligatoire)']
|
||||
}
|
||||
|
||||
Enum report_status_enum {
|
||||
pending [note: 'En attente de revue']
|
||||
under_review [note: 'En cours d examen par modérateur']
|
||||
actioned [note: 'Action prise (contenu retiré/édité)']
|
||||
dismissed [note: 'Signalement rejeté (contenu valide)']
|
||||
duplicate [note: 'Doublon d un signalement existant']
|
||||
}
|
||||
|
||||
Enum report_action_enum {
|
||||
content_removed [note: 'Contenu supprimé']
|
||||
content_edited [note: 'Métadonnées modifiées (âge, tags)']
|
||||
warning_sent [note: 'Avertissement au créateur']
|
||||
strike_issued [note: 'Strike ajouté au créateur']
|
||||
account_suspended [note: 'Compte créateur suspendu']
|
||||
no_action [note: 'Aucune action (signalement infondé)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Catégories de signalement** :
|
||||
|
||||
- `spam` : Contenu publicitaire non sollicité
|
||||
- `hate_speech` : Discours haineux, discrimination
|
||||
- `violence` : Violence explicite
|
||||
- `sexual_content` : Contenu sexuel inapproprié
|
||||
- `misinformation` : Désinformation, fake news
|
||||
- `copyright` : Violation de droits d'auteur
|
||||
- `wrong_age_rating` : Classification d'âge incorrecte
|
||||
- `other` : Autre raison (commentaire obligatoire)
|
||||
|
||||
**Statuts** :
|
||||
|
||||
- `pending` : En attente de revue
|
||||
- `under_review` : En cours d'examen par modérateur
|
||||
- `actioned` : Action prise (contenu retiré/édité)
|
||||
- `dismissed` : Signalement rejeté (contenu valide)
|
||||
- `duplicate` : Doublon d'un signalement existant
|
||||
|
||||
**Actions possibles** :
|
||||
|
||||
- `content_removed` : Contenu supprimé
|
||||
- `content_edited` : Métadonnées modifiées (âge, tags)
|
||||
- `warning_sent` : Avertissement au créateur
|
||||
- `strike_issued` : Strike ajouté au créateur
|
||||
- `account_suspended` : Compte créateur suspendu
|
||||
- `no_action` : Aucune action (signalement infondé)
|
||||
|
||||
**Workflow modération** :
|
||||
|
||||
- **3 premiers contenus** : Modération préalable obligatoire
|
||||
- **Après validation** : Modération a posteriori (signalements)
|
||||
- **Priorisation** : Nombre de signalements (>3 = urgent)
|
||||
- **Délai de traitement** : <48h pour signalements critiques
|
||||
64
docs/domains/_shared/entities/sessions.md
Normal file
64
docs/domains/_shared/entities/sessions.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Sessions
|
||||
|
||||
📖 Gestion des sessions utilisateur et tokens d'authentification OAuth2/OIDC
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
email varchar(255) [not null, unique]
|
||||
username varchar(50) [not null, unique]
|
||||
}
|
||||
|
||||
Table devices {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
device_name varchar(255)
|
||||
device_type varchar(50)
|
||||
}
|
||||
|
||||
Table sessions {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
device_id uuid [ref: > devices.id]
|
||||
access_token_hash varchar(64) [not null, note: 'SHA256 hash, never stored in clear']
|
||||
refresh_token_hash varchar(64) [not null, note: 'SHA256 hash, auto-rotated']
|
||||
access_token_expires_at timestamp [not null, note: 'Lifetime: 15 minutes']
|
||||
refresh_token_expires_at timestamp [not null, note: 'Lifetime: 30 days (rolling)']
|
||||
ip_address inet [not null]
|
||||
user_agent text [not null]
|
||||
city varchar(100)
|
||||
country_code char(2)
|
||||
created_at timestamp [not null, default: `now()`]
|
||||
last_activity_at timestamp [not null, default: `now()`]
|
||||
revoked_at timestamp [note: 'NULL if active, timestamp if manually revoked']
|
||||
|
||||
indexes {
|
||||
(user_id, revoked_at) [note: 'Find active sessions for user']
|
||||
(device_id)
|
||||
(refresh_token_hash) [unique, note: 'Detect replay attacks']
|
||||
(last_activity_at) [note: 'Auto-cleanup inactive sessions']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Durées de vie** :
|
||||
|
||||
- Access token : **15 minutes**
|
||||
- Refresh token : **30 jours** (rotatif)
|
||||
- Inactivité : Déconnexion automatique après **30 jours**
|
||||
|
||||
**Sécurité** :
|
||||
|
||||
- Tokens stockés en **SHA256** (jamais en clair)
|
||||
- Rotation automatique des refresh tokens
|
||||
- Détection replay attack
|
||||
|
||||
**Multi-device** :
|
||||
|
||||
- Sessions simultanées **illimitées**
|
||||
- Révocation individuelle ou globale possible
|
||||
- Alertes si connexion depuis nouveau device ou pays différent
|
||||
65
docs/domains/_shared/entities/user-profile-history.md
Normal file
65
docs/domains/_shared/entities/user-profile-history.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# User Profile History
|
||||
|
||||
📖 Audit trail modifications profil (Article 16 RGPD - Droit de rectification)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```kroki-dbml
|
||||
Table users {
|
||||
id uuid [primary key]
|
||||
email varchar(255)
|
||||
username varchar(50)
|
||||
bio text
|
||||
}
|
||||
|
||||
Table user_profile_history {
|
||||
id uuid [primary key]
|
||||
user_id uuid [not null, ref: > users.id]
|
||||
field_name profile_field_enum [not null, note: 'Champ modifié (email, username, bio, etc.)']
|
||||
old_value text [note: 'Valeur avant modification (NULL si création)']
|
||||
new_value text [not null, note: 'Nouvelle valeur']
|
||||
change_reason change_reason_enum [not null]
|
||||
ip_address inet [not null, note: 'IP de l origine du changement']
|
||||
changed_at timestamp [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(user_id, changed_at) [note: 'Timeline modifications user (ordre chronologique)']
|
||||
(field_name, changed_at) [note: 'Track modifications par type de champ']
|
||||
(user_id, field_name) [note: 'Historique d un champ spécifique']
|
||||
}
|
||||
}
|
||||
|
||||
Enum profile_field_enum {
|
||||
email [note: 'Re-vérification requise après changement']
|
||||
username [note: 'Limite: 1 changement/30j']
|
||||
bio [note: 'Biographie utilisateur']
|
||||
avatar_url [note: 'URL de l avatar']
|
||||
date_of_birth [note: 'Date de naissance']
|
||||
}
|
||||
|
||||
Enum change_reason_enum {
|
||||
user_edit [note: 'Modification self-service utilisateur']
|
||||
admin_correction [note: 'Correction par admin (via backoffice)']
|
||||
gdpr_request [note: 'Suite demande RGPD formelle (droit de rectification)']
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Champs trackés** :
|
||||
|
||||
- `email`: Re-vérification requise
|
||||
- `username`: Limite 1 changement/30j
|
||||
- `bio`, `avatar_url`, `date_of_birth`
|
||||
|
||||
**Change reasons** :
|
||||
|
||||
- `user_edit`: Modification self-service utilisateur
|
||||
- `admin_correction`: Correction par admin
|
||||
- `gdpr_request`: Suite demande RGPD formelle
|
||||
|
||||
**Audit** :
|
||||
|
||||
- Historique complet conservé (preuve légale)
|
||||
- Accessible utilisateur : "Historique de mes modifications"
|
||||
- Accessible DPO : investigations
|
||||
320
docs/domains/_shared/entities/vue-ensemble.md
Normal file
320
docs/domains/_shared/entities/vue-ensemble.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# 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"
|
||||
USERS ||--o{ SESSIONS : "possède"
|
||||
USERS ||--o{ DEVICES : "possède"
|
||||
USERS ||--o{ USER_CONSENTS : "donne"
|
||||
USERS ||--o{ LOCATION_HISTORY : "génère"
|
||||
USERS ||--o{ INTEREST_GAUGES : "possède"
|
||||
USERS ||--o{ REPORTS : "signale"
|
||||
USERS ||--o{ DATA_EXPORTS : "demande"
|
||||
USERS ||--o{ PARENTAL_CONSENTS : "a"
|
||||
USERS ||--o{ ACCOUNT_DELETIONS : "demande"
|
||||
USERS ||--o{ USER_PROFILE_HISTORY : "modifie"
|
||||
|
||||
PARENTAL_CONSENTS ||--|| PARENTAL_CONTROLS : "configure"
|
||||
PRIVACY_POLICY_VERSIONS ||--o{ USER_POLICY_ACCEPTANCES : "acceptée par"
|
||||
USERS ||--o{ USER_POLICY_ACCEPTANCES : "accepte"
|
||||
BREACH_INCIDENTS ||--o{ BREACH_AFFECTED_USERS : "impacte"
|
||||
USERS ||--o{ BREACH_AFFECTED_USERS : "est impacté"
|
||||
|
||||
CONTENTS ||--o{ LISTENING_HISTORY : "écouté"
|
||||
CONTENTS }o--|| USERS : "créé par"
|
||||
CONTENTS ||--o{ REPORTS : "reçoit"
|
||||
|
||||
DEVICES ||--o{ SESSIONS : "a"
|
||||
|
||||
USERS {
|
||||
uuid id PK
|
||||
string email UK
|
||||
string pseudo UK
|
||||
date birthdate
|
||||
string role
|
||||
string account_status
|
||||
timestamp created_at
|
||||
timestamp last_login_at
|
||||
boolean email_verified
|
||||
boolean kyc_verified
|
||||
string phone_number
|
||||
int trust_score
|
||||
timestamp deletion_requested_at
|
||||
timestamp inactivity_notified_at
|
||||
}
|
||||
|
||||
CONTENTS {
|
||||
uuid id PK
|
||||
uuid creator_id FK
|
||||
string title
|
||||
string audio_url
|
||||
string status
|
||||
string moderation_status
|
||||
string age_rating
|
||||
string geo_type
|
||||
point geo_location
|
||||
string[] tags
|
||||
int duration_seconds
|
||||
timestamp published_at
|
||||
int reports_count
|
||||
text moderation_notes
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
SESSIONS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid device_id FK
|
||||
string access_token_hash
|
||||
string refresh_token_hash
|
||||
timestamp access_token_expires_at
|
||||
timestamp refresh_token_expires_at
|
||||
inet ip_address
|
||||
string user_agent
|
||||
string city
|
||||
string country_code
|
||||
timestamp created_at
|
||||
timestamp last_activity_at
|
||||
timestamp revoked_at
|
||||
}
|
||||
|
||||
DEVICES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string device_name
|
||||
string os
|
||||
string browser
|
||||
string device_type
|
||||
boolean is_trusted
|
||||
timestamp trusted_until
|
||||
timestamp first_seen_at
|
||||
timestamp last_seen_at
|
||||
}
|
||||
|
||||
USER_CONSENTS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string consent_type
|
||||
string consent_version
|
||||
boolean accepted
|
||||
timestamp given_at
|
||||
}
|
||||
|
||||
LOCATION_HISTORY {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
geography location
|
||||
string geohash
|
||||
boolean anonymized
|
||||
string context
|
||||
timestamp created_at
|
||||
timestamp anonymized_at
|
||||
}
|
||||
|
||||
INTEREST_GAUGES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string category
|
||||
decimal score
|
||||
timestamp last_updated
|
||||
int interactions_count
|
||||
}
|
||||
|
||||
REPORTS {
|
||||
uuid id PK
|
||||
uuid content_id FK
|
||||
uuid reporter_id FK
|
||||
uuid moderator_id FK
|
||||
string category
|
||||
string status
|
||||
text comment
|
||||
timestamp reported_at
|
||||
timestamp reviewed_at
|
||||
text moderator_notes
|
||||
string action_taken
|
||||
}
|
||||
|
||||
DATA_EXPORTS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string status
|
||||
string export_url
|
||||
bigint size_bytes
|
||||
string format
|
||||
timestamp requested_at
|
||||
timestamp generated_at
|
||||
timestamp expires_at
|
||||
}
|
||||
|
||||
PARENTAL_CONSENTS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string parent_email
|
||||
boolean validated
|
||||
timestamp validated_at
|
||||
timestamp revoked_at
|
||||
}
|
||||
|
||||
PARENTAL_CONTROLS {
|
||||
uuid id PK
|
||||
uuid parental_consent_id FK
|
||||
boolean gps_enabled
|
||||
boolean messaging_enabled
|
||||
boolean content_16plus_enabled
|
||||
}
|
||||
|
||||
PRIVACY_POLICY_VERSIONS {
|
||||
uuid id PK
|
||||
string version
|
||||
boolean major_change
|
||||
timestamp effective_date
|
||||
}
|
||||
|
||||
USER_POLICY_ACCEPTANCES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid policy_version_id FK
|
||||
boolean accepted
|
||||
timestamp accepted_at
|
||||
}
|
||||
|
||||
ACCOUNT_DELETIONS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string status
|
||||
timestamp requested_at
|
||||
timestamp effective_at
|
||||
timestamp deleted_at
|
||||
}
|
||||
|
||||
BREACH_INCIDENTS {
|
||||
uuid id PK
|
||||
string severity
|
||||
int estimated_users_count
|
||||
timestamp detected_at
|
||||
timestamp cnil_notified_at
|
||||
boolean user_notification_required
|
||||
}
|
||||
|
||||
BREACH_AFFECTED_USERS {
|
||||
uuid id PK
|
||||
uuid breach_id FK
|
||||
uuid user_id FK
|
||||
timestamp notified_at
|
||||
}
|
||||
|
||||
USER_PROFILE_HISTORY {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
string field_name
|
||||
text old_value
|
||||
text new_value
|
||||
timestamp changed_at
|
||||
}
|
||||
|
||||
DATA_RETENTION_LOGS {
|
||||
uuid id PK
|
||||
string action_type
|
||||
int users_processed
|
||||
int users_deleted
|
||||
timestamp executed_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités de base** :
|
||||
|
||||
- **USERS** : Utilisateurs plateforme
|
||||
- Rôles : `listener`, `creator`, `moderator`, `admin`
|
||||
- Account status : `active`, `suspended`, `grace_period`, `deleted`
|
||||
- Trust score : 0-100 (anti-spam, accès fonctionnalités avancées)
|
||||
|
||||
- **CONTENTS** : Contenus audio
|
||||
- Status : `draft`, `pending_review`, `published`, `moderated`, `deleted`
|
||||
- Moderation status : `pending_review`, `approved`, `rejected`
|
||||
- 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
|
||||
|
||||
**Entités authentification & sécurité** :
|
||||
|
||||
- **SESSIONS** : Sessions utilisateur OAuth2/OIDC ([détails](sessions.md))
|
||||
- Access token : 15 min, Refresh token : 30 jours (rotatif)
|
||||
|
||||
- **DEVICES** : Appareils de confiance ([détails](devices.md))
|
||||
- Types : `mobile`, `tablet`, `desktop`, `car`
|
||||
- Appareils de confiance : bypass 2FA pendant 30 jours
|
||||
|
||||
**Entités RGPD & conformité** :
|
||||
|
||||
- **USER_CONSENTS** : Consentements RGPD avec versioning ([détails](consents.md))
|
||||
- Types : `geolocation_precise`, `analytics`, `push_notifications`
|
||||
|
||||
- **LOCATION_HISTORY** : Historique GPS avec anonymisation 24h ([détails](location-history.md))
|
||||
- Geohash précision 5 (~5km²) après 24h
|
||||
|
||||
- **DATA_EXPORTS** : Exports de données utilisateur ([détails](exports.md))
|
||||
- Portabilité RGPD Article 20, délai 48h max
|
||||
|
||||
- **PARENTAL_CONSENTS** : Consentements parentaux 13-15 ans ([détails](parental-consents.md))
|
||||
- Workflow validation email parent, token expire 7j
|
||||
|
||||
- **PARENTAL_CONTROLS** : Paramètres contrôle parental ([détails](parental-consents.md))
|
||||
- GPS, messagerie, contenus +16 configurables par parent
|
||||
|
||||
- **PRIVACY_POLICY_VERSIONS** : Versioning politique confidentialité ([détails](privacy-policy-versions.md))
|
||||
- Popup si changement majeur, historique acceptations
|
||||
|
||||
- **ACCOUNT_DELETIONS** : Suppressions avec grace period 30j ([détails](account-deletions.md))
|
||||
- Annulation possible, suppression effective automatique
|
||||
|
||||
- **BREACH_INCIDENTS** : Registre violations de données ([détails](breach-incidents.md))
|
||||
- Procédure 72h CNIL, notification utilisateurs si risque élevé
|
||||
|
||||
- **USER_PROFILE_HISTORY** : Audit trail modifications profil ([détails](user-profile-history.md))
|
||||
- Droit rectification Article 16, preuve légale
|
||||
|
||||
- **DATA_RETENTION_LOGS** : Logs purges automatiques ([détails](data-retention-logs.md))
|
||||
- Inactivité 5 ans, notifications 90j/30j/7j
|
||||
|
||||
**Entités recommandation & modération** :
|
||||
|
||||
- **INTEREST_GAUGES** : Jauges de centres d'intérêt ([détails](interest-gauges.md))
|
||||
- Score 0-100 par catégorie (automobile, travel, music, etc.)
|
||||
- Algorithme : 70% géo + 30% intérêts
|
||||
|
||||
- **REPORTS** : Signalements de contenu ([détails](reports.md))
|
||||
- Catégories : `spam`, `hate_speech`, `violence`, `sexual_content`, `misinformation`, etc.
|
||||
- Workflow modération avec priorisation
|
||||
@@ -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
|
||||
@@ -23,7 +23,7 @@ Fonctionnalité: Classification des contenus par âge
|
||||
Alors la publication échoue
|
||||
Et je vois le message "Vous devez sélectionner une classification d'âge"
|
||||
|
||||
Scénario: Utilisateur 13-15 ans voit uniquement du contenu "Tout public"
|
||||
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 |
|
||||
@@ -32,10 +32,10 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| 16+ | 10 |
|
||||
| 18+ | 5 |
|
||||
Quand je demande des recommandations
|
||||
Alors je vois uniquement les 20 contenus "Tout public"
|
||||
Et les autres contenus ne sont jamais proposés
|
||||
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" et "13+"
|
||||
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 |
|
||||
@@ -44,8 +44,8 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| 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 pas proposés
|
||||
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
|
||||
@@ -54,11 +54,11 @@ Fonctionnalité: Classification des contenus par âge
|
||||
Alors je vois tous les contenus sans restriction
|
||||
Et aucun filtre d'âge n'est appliqué
|
||||
|
||||
Scénario: Mode Kids activé automatiquement pour les moins de 13 ans
|
||||
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 le mode Kids est activé automatiquement
|
||||
Et je vois uniquement du contenu "Tout public"
|
||||
Et des protections supplémentaires sont appliquées
|
||||
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"
|
||||
@@ -93,10 +93,11 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| classification |
|
||||
| Tout public |
|
||||
| 13+ |
|
||||
Et je ne vois pas les contenus 16+ et 18+ dans les résultats
|
||||
| 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 14 ans
|
||||
É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é
|
||||
@@ -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 |
|
||||
@@ -73,7 +73,7 @@ Fonctionnalité: Inscription utilisateur
|
||||
É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 le mode Kids est activé automatiquement
|
||||
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"
|
||||
@@ -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,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,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,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
|
||||
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
|
||||
@@ -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,110 @@
|
||||
# language: fr
|
||||
|
||||
@privacy @rgpd @security
|
||||
Fonctionnalité: Sécurité des données (Article 32 RGPD)
|
||||
En tant que responsable de traitement
|
||||
Je veux garantir la sécurité des données personnelles
|
||||
Afin de prévenir les violations et fuites
|
||||
|
||||
# Chiffrement transport
|
||||
@encryption @tls
|
||||
Scénario: TLS 1.3 obligatoire pour toutes les communications
|
||||
Quand un client tente de se connecter à l'API
|
||||
Alors la connexion utilise TLS 1.3 uniquement
|
||||
Et les protocoles TLS 1.0, 1.1, 1.2 sont refusés
|
||||
Et le certificat est valide et à jour
|
||||
|
||||
# Chiffrement stockage
|
||||
@encryption @database
|
||||
Scénario: Encryption at rest pour la base de données
|
||||
Étant donné que PostgreSQL stocke des données utilisateurs
|
||||
Alors le chiffrement AES-256 at rest est activé
|
||||
Et les backups sont également chiffrés AES-256
|
||||
Et les backups sont stockés offsite (règle 3-2-1)
|
||||
|
||||
# Tokens JWT sécurisés
|
||||
@encryption @jwt
|
||||
Scénario: Rotation des clés JWT tous les 90 jours
|
||||
Étant donné que l'API utilise des tokens JWT RS256
|
||||
Quand 90 jours se sont écoulés depuis la dernière rotation
|
||||
Alors une nouvelle paire de clés RSA est générée
|
||||
Et l'ancienne clé reste valide 7 jours (overlap)
|
||||
Et tous les tokens sont progressivement re-signés
|
||||
|
||||
# CDN avec signed URLs
|
||||
@cdn @signed-urls
|
||||
Scénario: URLs signées expirables pour les fichiers audio
|
||||
Quand un utilisateur demande à écouter un contenu
|
||||
Alors l'API génère une signed URL valide 1 heure
|
||||
Et l'URL contient un token HMAC
|
||||
Et après expiration, l'URL retourne 403 Forbidden
|
||||
|
||||
# Détection breach
|
||||
@breach @monitoring
|
||||
Scénario: Alerte immédiate si erreurs critiques backend
|
||||
Quand une erreur critique survient dans l'API (500, crash)
|
||||
Alors Sentry déclenche une alerte Discord/Slack immédiate
|
||||
Et l'équipe est notifiée en temps réel
|
||||
Et les logs sont consultables dans Grafana
|
||||
|
||||
@breach @monitoring
|
||||
Scénario: Alerte si pic de requêtes anormal (potentiel DDoS)
|
||||
Étant donné que le trafic habituel est ~1000 req/min
|
||||
Quand le trafic atteint 10000 req/min
|
||||
Alors Grafana déclenche une alerte email
|
||||
Et l'équipe vérifie s'il s'agit d'une attaque
|
||||
|
||||
@breach @monitoring
|
||||
Scénario: Alerte si accès DB non autorisé
|
||||
Quand une connexion PostgreSQL provient d'une IP non whitelistée
|
||||
Alors PostgreSQL bloque la connexion
|
||||
Et un SMS est envoyé au fondateur
|
||||
Et l'IP est loggée pour investigation
|
||||
|
||||
# Procédure breach 72h CNIL
|
||||
@breach @procedure
|
||||
Scénario: Notification CNIL si violation de données sous 72h
|
||||
Étant donné qu'une violation de données est détectée
|
||||
Et que des données GPS utilisateurs ont fuité
|
||||
Quand l'équipe évalue la gravité
|
||||
Alors le runbook "docs/rgpd/procedure-breach.md" est suivi:
|
||||
| Étape | Délai | Action |
|
||||
| 1 | H+0 | Détection et confinement |
|
||||
| 2 | H+24 | Évaluation gravité et impact |
|
||||
| 3 | H+48 | Notification CNIL si risque |
|
||||
| 4 | H+72 | Notification utilisateurs si élevé|
|
||||
Et un email pré-rédigé est envoyé à la CNIL
|
||||
Et le formulaire en ligne CNIL est rempli
|
||||
|
||||
# Mesures organisationnelles
|
||||
@security @access-control
|
||||
Scénario: Accès DB uniquement via VPN et whitelist IP
|
||||
Étant donné que je suis un développeur
|
||||
Quand je tente d'accéder à la DB de production
|
||||
Alors je dois être connecté au VPN OVH
|
||||
Et mon IP doit être dans la whitelist
|
||||
Sinon la connexion est refusée
|
||||
|
||||
@security @2fa
|
||||
Scénario: 2FA obligatoire pour les comptes administrateurs
|
||||
Étant donné que je suis un administrateur
|
||||
Quand je me connecte à Zitadel admin
|
||||
Alors le 2FA (TOTP) est obligatoire
|
||||
Et je ne peux pas désactiver le 2FA
|
||||
Et chaque connexion est loggée avec IP et timestamp
|
||||
|
||||
@security @logs
|
||||
Scénario: Anonymisation des IP dans les logs (rotation 90j)
|
||||
Quand un utilisateur fait une requête API
|
||||
Alors son IP est loggée pour debug
|
||||
Mais les 2 derniers octets sont masqués (ex: 192.168.x.x)
|
||||
Et les logs sont automatiquement supprimés après 90 jours
|
||||
|
||||
# Tests de sécurité
|
||||
@security @pentest
|
||||
Scénario: Pentest annuel obligatoire
|
||||
Étant donné que l'application est en production
|
||||
Quand une année s'est écoulée depuis le dernier pentest
|
||||
Alors un pentest externe est planifié
|
||||
Et les vulnérabilités découvertes sont corrigées sous 30 jours
|
||||
Et un rapport est produit pour audit
|
||||
@@ -0,0 +1,112 @@
|
||||
# language: fr
|
||||
|
||||
@privacy @rgpd @minors @parental-consent
|
||||
Fonctionnalité: Protection des mineurs et consentement parental (Article 8 RGPD)
|
||||
En tant que plateforme responsable
|
||||
Je veux protéger les mineurs avec consentement parental
|
||||
Afin de respecter l'Article 8 RGPD (13-15 ans)
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis sur la page d'inscription
|
||||
|
||||
Scénario: Blocage inscription si moins de 13 ans
|
||||
Quand je saisis ma date de naissance "15/03/2014"
|
||||
Et que je valide le formulaire
|
||||
Alors je vois le message "RoadWave est réservé aux personnes de 13 ans et plus"
|
||||
Et je vois un lien vers "RoadWave Kids"
|
||||
Et mon inscription est bloquée
|
||||
|
||||
Scénario: Inscription directe si 16 ans ou plus
|
||||
Quand je saisis ma date de naissance "10/01/2008"
|
||||
Et que je valide le formulaire
|
||||
Alors mon compte est créé immédiatement
|
||||
Et aucun consentement parental n'est requis
|
||||
Et j'ai accès à toutes les fonctionnalités
|
||||
|
||||
Plan du Scénario: Workflow consentement parental pour 13-15 ans
|
||||
Quand je saisis ma date de naissance "<date_naissance>"
|
||||
Et que je saisis l'email de mon parent "parent@example.com"
|
||||
Et que je valide le formulaire
|
||||
Alors un email est envoyé à "parent@example.com"
|
||||
Et le lien de validation expire dans 7 jours
|
||||
Et mon compte est créé mais inactif
|
||||
Et je vois "En attente validation parentale"
|
||||
|
||||
Exemples:
|
||||
| date_naissance | âge |
|
||||
| 15/03/2011 | 13 |
|
||||
| 20/06/2010 | 14 |
|
||||
| 01/12/2009 | 15 |
|
||||
|
||||
Scénario: Validation du consentement parental
|
||||
Étant donné que je suis un mineur de 14 ans avec compte inactif
|
||||
Et que mon parent a reçu l'email de validation
|
||||
Quand mon parent clique sur le lien de validation
|
||||
Alors il voit une page avec:
|
||||
| Section |
|
||||
| Résumé données collectées |
|
||||
| Paramètres contrôle parental|
|
||||
| Checkbox consentement |
|
||||
Et quand il coche "J'autorise mon enfant" et valide
|
||||
Alors mon compte est activé
|
||||
Et je reçois un email "Compte activé par ton parent"
|
||||
Et les restrictions 13-15 ans sont appliquées
|
||||
|
||||
Scénario: Restrictions pour comptes 13-15 ans
|
||||
Étant donné que je suis un utilisateur de 14 ans avec compte validé
|
||||
Alors je peux écouter des contenus autorisés
|
||||
Mais je NE peux PAS:
|
||||
| Restriction |
|
||||
| Activer le GPS précis sans accord parent |
|
||||
| Utiliser la messagerie privée |
|
||||
| Voir les contenus marqués +16 |
|
||||
| Afficher ma ville précise sur le profil |
|
||||
|
||||
Scénario: Dashboard parent - Visualisation activité enfant
|
||||
Étant donné que je suis un parent avec enfant de 14 ans
|
||||
Quand je me connecte à "roadwave.fr/parent/[child_id]"
|
||||
Alors je vois:
|
||||
| Information |
|
||||
| Historique d'écoute |
|
||||
| Temps d'écoute hebdomadaire |
|
||||
| Dernière connexion |
|
||||
Et je peux:
|
||||
| Action |
|
||||
| Activer/désactiver GPS précis |
|
||||
| Activer/désactiver messagerie |
|
||||
| Révoquer le consentement |
|
||||
|
||||
Scénario: Révocation du consentement parental
|
||||
Étant donné que je suis un parent
|
||||
Et que mon enfant de 14 ans a un compte actif
|
||||
Quand je clique sur "Révoquer le consentement"
|
||||
Alors le compte de mon enfant est immédiatement désactivé
|
||||
Et il ne peut plus se connecter
|
||||
Et je reçois un email de confirmation
|
||||
|
||||
@roadwave-kids
|
||||
Scénario: RoadWave Kids - Inscription via compte parent
|
||||
Étant donné que je suis un parent avec compte RoadWave actif
|
||||
Quand je vais dans "Mon compte > Ajouter un profil enfant"
|
||||
Et que je saisis:
|
||||
| Champ | Valeur |
|
||||
| Pseudo enfant | "Emma" |
|
||||
| Date naissance | "12/08/2016" |
|
||||
Alors un profil Kids est créé
|
||||
Et je reçois un QR code pour l'app RoadWave Kids
|
||||
Et l'enfant ne peut écouter que les contenus whitelist
|
||||
|
||||
@roadwave-kids
|
||||
Scénario: RoadWave Kids - Restrictions strictes
|
||||
Étant donné que je suis connecté sur l'app RoadWave Kids
|
||||
Alors je peux uniquement:
|
||||
| Fonctionnalité |
|
||||
| Écouter contenus présélectionnés |
|
||||
| Voir ma position ville (GeoIP) |
|
||||
Mais je NE peux PAS:
|
||||
| Restriction |
|
||||
| Activer le GPS précis |
|
||||
| Créer du contenu |
|
||||
| Avoir un profil public |
|
||||
| Utiliser la messagerie |
|
||||
| Voir du contenu UGC |
|
||||
@@ -0,0 +1,99 @@
|
||||
# language: fr
|
||||
|
||||
@privacy @rgpd @transparency
|
||||
Fonctionnalité: Politique de confidentialité et transparence (Articles 13-14 RGPD)
|
||||
En tant qu'utilisateur
|
||||
Je veux comprendre comment mes données sont utilisées
|
||||
Afin de donner un consentement éclairé
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est disponible
|
||||
|
||||
# Popup première connexion
|
||||
@first-launch
|
||||
Scénario: Affichage de la politique de confidentialité à l'inscription
|
||||
Quand je m'inscris pour la première fois
|
||||
Alors une popup s'affiche avec la politique de confidentialité
|
||||
Et je dois scroller jusqu'en bas pour activer le bouton "J'accepte"
|
||||
Et je dois cocher "J'ai lu et j'accepte la politique de confidentialité"
|
||||
Et je ne peux pas créer de compte sans accepter
|
||||
|
||||
# Contenu obligatoire
|
||||
@content
|
||||
Scénario: Vérification du contenu de la politique de confidentialité
|
||||
Quand je consulte la page "roadwave.fr/confidentialite"
|
||||
Alors je vois les informations suivantes:
|
||||
| Section |
|
||||
| Identité responsable traitement + DPO |
|
||||
| Finalités détaillées par traitement |
|
||||
| Base légale (consentement/contrat/intérêt)|
|
||||
| Destinataires données (CDN, Matomo, etc.) |
|
||||
| Durées de conservation |
|
||||
| Droits utilisateurs (accès, rectif, etc.) |
|
||||
| Droit réclamation CNIL |
|
||||
| Transferts hors UE (aucun) |
|
||||
|
||||
# Versioning
|
||||
@versioning
|
||||
Scénario: Versioning de la politique avec Git et DB
|
||||
Étant donné que la politique de confidentialité est modifiée
|
||||
Quand l'équipe commit les changements
|
||||
Alors le fichier "docs/legal/politique-confidentialite.md" est versionné Git
|
||||
Et une entrée est créée dans "privacy_policy_versions"
|
||||
Avec les champs:
|
||||
| Champ | Valeur |
|
||||
| version | "2.0" |
|
||||
| effective_date| "2026-03-01" |
|
||||
| major_change | true |
|
||||
| changelog | "Ajout tracking..." |
|
||||
|
||||
@versioning
|
||||
Scénario: Notification utilisateurs si changement majeur
|
||||
Étant donné qu'une nouvelle version majeure de la politique est publiée
|
||||
Quand un utilisateur se connecte
|
||||
Alors une popup s'affiche "Politique de confidentialité mise à jour"
|
||||
Et il doit l'accepter à nouveau pour continuer
|
||||
Et s'il refuse, son compte est gelé (accès lecture seule)
|
||||
|
||||
# Transparence algorithme
|
||||
@algorithm-transparency
|
||||
Scénario: Explication simplifiée de l'algorithme de recommandation
|
||||
Quand je vais sur "roadwave.fr/comment-ca-marche"
|
||||
Alors je vois une page "Comment fonctionne la recommandation ?"
|
||||
Avec les explications suivantes:
|
||||
| Critère | Explication |
|
||||
| Distance géographique | Contenus près de vous en priorité |
|
||||
| Centres d'intérêt | Jauges automatiques selon écoutes |
|
||||
| Popularité | Contenus les plus écoutés |
|
||||
Et je peux désactiver la personnalisation (mode anonyme)
|
||||
|
||||
# Contact DPO
|
||||
@dpo-contact
|
||||
Scénario: Accès facile au contact DPO
|
||||
Quand je vais dans "Paramètres > Confidentialité et données"
|
||||
Alors je vois un bouton "Contacter le DPO"
|
||||
Et le lien email "dpo@roadwave.fr" est cliquable
|
||||
Et je vois "Délai de réponse : 1 mois maximum"
|
||||
|
||||
# Profilage et décisions automatisées
|
||||
@profiling
|
||||
Scénario: Information sur le profilage et opposition possible
|
||||
Quand je consulte la politique de confidentialité
|
||||
Alors je vois une section "Profilage et décisions automatisées"
|
||||
Qui explique:
|
||||
| Type décision | Impact | Opposition possible |
|
||||
| Recommandations | Faible | Oui (mode anonyme) |
|
||||
| Modération automatique| Élevé | Oui (contestation) |
|
||||
Et je peux activer le mode anonyme à tout moment
|
||||
|
||||
# Sous-traitants
|
||||
@subprocessors
|
||||
Scénario: Liste transparente des sous-traitants
|
||||
Quand je consulte "roadwave.fr/confidentialite#sous-traitants"
|
||||
Alors je vois la liste complète:
|
||||
| Service | Finalité | Localisation | DPA |
|
||||
| OVH | Hébergement | France | ✅ |
|
||||
| Bunny.net | CDN audio | UE | ✅ |
|
||||
| Brevo | Emails | France | ✅ |
|
||||
| Mangopay | Paiements | Luxembourg | ✅ |
|
||||
Et je vois "Aucun transfert hors UE"
|
||||
@@ -0,0 +1,77 @@
|
||||
# language: fr
|
||||
|
||||
@privacy @rgpd @user-rights
|
||||
Fonctionnalité: Exercice des droits utilisateurs RGPD
|
||||
En tant qu'utilisateur
|
||||
Je veux exercer mes droits RGPD facilement
|
||||
Afin de contrôler mes données personnelles
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à mon compte
|
||||
|
||||
# Article 16 - Droit de rectification
|
||||
@rectification
|
||||
Scénario: Modification du pseudo (limite 1/30j)
|
||||
Quand je vais dans "Paramètres > Mon profil"
|
||||
Et que je modifie mon pseudo de "Alice123" à "AliceM"
|
||||
Alors le changement est immédiat
|
||||
Et je ne peux plus modifier mon pseudo pendant 30 jours
|
||||
Et l'historique est enregistré dans "user_profile_history"
|
||||
|
||||
@rectification
|
||||
Scénario: Modification de l'email avec re-vérification
|
||||
Quand je modifie mon email de "old@example.com" à "new@example.com"
|
||||
Alors je reçois un email de vérification sur "new@example.com"
|
||||
Et je dois cliquer sur le lien dans les 24h
|
||||
Et mon ancien email reste actif jusqu'à validation
|
||||
|
||||
# Article 21 - Droit d'opposition
|
||||
@opposition
|
||||
Scénario: Opposition au marketing par email
|
||||
Quand je vais dans "Paramètres > Notifications"
|
||||
Et que je décoche "Recevoir les emails marketing"
|
||||
Alors je ne reçois plus d'emails promotionnels
|
||||
Mais je reçois toujours les emails transactionnels
|
||||
|
||||
@opposition @anonymous-mode
|
||||
Scénario: Activation du mode anonyme (recommandations génériques)
|
||||
Quand je vais dans "Paramètres > Confidentialité"
|
||||
Et que j'active "Mode anonyme"
|
||||
Alors mes jauges d'intérêt sont ignorées
|
||||
Et je reçois uniquement les top contenus de ma zone géographique
|
||||
Et mon historique d'écoute n'est pas utilisé pour les recommandations
|
||||
Et je vois un badge "Mode anonyme actif"
|
||||
|
||||
# Article 18 - Droit à la limitation du traitement
|
||||
@limitation
|
||||
Scénario: Gel temporaire du compte
|
||||
Quand je vais dans "Paramètres > Gestion du compte"
|
||||
Et que je clique sur "Mettre en pause mon compte"
|
||||
Alors mon compte est gelé immédiatement
|
||||
Et mes contenus sont cachés (non diffusés)
|
||||
Et mon profil est invisible
|
||||
Mais je peux toujours me connecter en lecture seule
|
||||
Et je peux réactiver à tout moment
|
||||
|
||||
@limitation
|
||||
Scénario: Réactivation compte gelé
|
||||
Étant donné que mon compte est gelé depuis 15 jours
|
||||
Quand je me connecte
|
||||
Et que je clique sur "Réactiver mon compte"
|
||||
Alors mon compte redevient actif immédiatement
|
||||
Et mes contenus sont à nouveau visibles
|
||||
|
||||
# Article 12 - Délai de réponse 1 mois max
|
||||
@response-time
|
||||
Scénario: Demande d'accès aux données (délai 48h)
|
||||
Quand je demande un export de mes données
|
||||
Alors je reçois un email sous 48h maximum
|
||||
Avec un lien de téléchargement valide 7 jours
|
||||
|
||||
@response-time
|
||||
Scénario: Contestation d'une décision automatisée (délai 24h)
|
||||
Étant donné que mon contenu a été bloqué par la modération automatique
|
||||
Quand je clique sur "Contester cette décision"
|
||||
Alors un humain examine mon cas sous 24h
|
||||
Et je reçois une réponse motivée
|
||||
Et mon contenu est rétabli si la décision était erronée
|
||||
@@ -19,6 +19,7 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Coût modération** : Classification manuelle humaine très coûteuse (~2000€/mois pour 1-2 modérateurs senior full-time)
|
||||
- **Risque juridique** : Accusations de biais éditorial, contentieux DSA
|
||||
- **Complexité technique** : Dashboard audit, logs 3 ans, alertes déséquilibre
|
||||
@@ -26,6 +27,7 @@
|
||||
- **Pas essentiel MVP** : L'application fonctionne sans ce système
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Tag "Politique" simple (comme "Économie", "Sport")
|
||||
- Pas de classification gauche/droite
|
||||
- Pas d'équilibrage imposé
|
||||
@@ -36,6 +38,7 @@
|
||||
### Spécifications complètes (future implémentation)
|
||||
|
||||
**Échelle de classification** (5 niveaux) :
|
||||
|
||||
- 🔴 **Extrême gauche** (anticapitalisme radical, révolution)
|
||||
- 🟠 **Gauche** (écologie, social, critique capitalisme modérée)
|
||||
- ⚪ **Centre/Neutre** (pas de positionnement politique clair)
|
||||
@@ -44,11 +47,13 @@
|
||||
- 🟢 **Non politique** (enfants, musique, fiction, culture générale)
|
||||
|
||||
**Qui classifie** :
|
||||
|
||||
- ❌ Pas de classification automatique IA (outil informatif uniquement, jamais décisionnaire)
|
||||
- ✅ Modérateurs senior après transcription
|
||||
- ✅ Créateur peut contester via processus d'appel
|
||||
|
||||
**Affichage** :
|
||||
|
||||
- Badge politique visible : **au choix de l'utilisateur** (paramètre "Afficher orientation politique")
|
||||
- Par défaut : badges masqués (UX neutre)
|
||||
|
||||
@@ -62,17 +67,20 @@
|
||||
| **Masquer politique** | 0% gauche / 0% droite / 100% centre-neutre + non politique | Option apolitique |
|
||||
|
||||
**Audit et conformité DSA** :
|
||||
|
||||
- Rapport hebdomadaire automatique : % gauche/droite/centre diffusé par utilisateur
|
||||
- Alerte si déséquilibre global plateforme (>55% d'un bord)
|
||||
- Logs conservés **3 ans** (exigence Digital Services Act EU)
|
||||
- Dashboard admin : visualisation répartition temps réel
|
||||
|
||||
**Sanctions mauvaise classification** :
|
||||
|
||||
- Classification volontairement incorrecte = Strike 1
|
||||
- Récidive = Strike 2 (suspension 7j)
|
||||
- Détection via signalements utilisateurs + audit modération
|
||||
|
||||
**Justification** :
|
||||
|
||||
- **Conformité juridique DSA** (obligation neutralité plateforme EU)
|
||||
- Protection contre accusations de biais éditorial
|
||||
- Transparence auditable
|
||||
@@ -90,6 +98,7 @@
|
||||
5. Validation juridique du processus de classification
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+3 mois) : Validation demande utilisateurs via sondages
|
||||
- Phase 2 (Post-MVP+6 mois) : Recrutement modérateurs + développement dashboard
|
||||
- Phase 3 (Post-MVP+9 mois) : Tests bêta avec utilisateurs volontaires
|
||||
@@ -104,12 +113,14 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Complexité technique** : Intégration Lightning Network, gestion wallets crypto
|
||||
- **Réglementation** : Incertitude juridique crypto en EU (MiCA 2025)
|
||||
- **Focus MVP** : Priorité sur monétisation via abonnements Premium et publicités
|
||||
- **Adoption utilisateurs** : Nécessite éducation et adoption crypto préalables
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Monétisation créateurs via :
|
||||
- Partage revenus publicités (3€ CPM)
|
||||
- 70% revenus abonnements Premium
|
||||
@@ -128,6 +139,7 @@
|
||||
5. Conversion EUR/BTC automatique (optionnelle)
|
||||
|
||||
**Avantages Lightning Network** :
|
||||
|
||||
- ✅ Frais quasi-nuls (<1%) vs 1.8% Mangopay
|
||||
- ✅ Transactions instantanées (<1 seconde)
|
||||
- ✅ Micropaiements possibles (dès 0.01€)
|
||||
@@ -135,12 +147,14 @@
|
||||
- ✅ Pas d'intermédiaire (peer-to-peer)
|
||||
|
||||
**Contraintes** :
|
||||
|
||||
- ❌ Adoption crypto limitée (2-5% population EU en 2026)
|
||||
- ❌ Volatilité BTC (nécessite conversion EUR immédiate)
|
||||
- ❌ UX complexe pour utilisateurs non-crypto
|
||||
- ❌ Réglementation MiCA en évolution
|
||||
|
||||
**Alternatives étudiées** :
|
||||
|
||||
- Ko-fi / Buy Me a Coffee : simple mais frais 5%
|
||||
- PayPal/Stripe : frais 2.9% + 0.30€ (non viable pour micropaiements)
|
||||
- Mangopay : déjà utilisé, mais frais élevés pour petits montants
|
||||
@@ -157,6 +171,7 @@
|
||||
5. Demande créateurs confirmée via sondages
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+6 mois) : Étude de marché et demande utilisateurs
|
||||
- Phase 2 (Post-MVP+12 mois) : Développement intégration Lightning
|
||||
- Phase 3 (Post-MVP+15 mois) : Tests bêta avec créateurs volontaires
|
||||
@@ -171,6 +186,7 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Masse critique requise** : Nécessite pool suffisant d'utilisateurs simultanés (>500) pour matching rapide (<30s)
|
||||
- **Infrastructure WebRTC** : Coût serveurs TURN/STUN supplémentaire (~500€/mois pour 1000 utilisateurs actifs)
|
||||
- **Complexité modération** : Contenu live non enregistré = risques abus, nécessite système de confiance et signalement robuste
|
||||
@@ -178,6 +194,7 @@
|
||||
- **UX conducteur** : Commandes vocales avancées nécessaires pour sécurité routière
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Radio live créateurs uniquement (1 vers N)
|
||||
- Pas de connexion P2P entre auditeurs
|
||||
- Chat textuel limité aux POIs et commentaires
|
||||
@@ -220,12 +237,14 @@
|
||||
- Badge "En roulette" visible sur profil (transparence)
|
||||
|
||||
**Avantages** :
|
||||
|
||||
- ✅ Sérendipité et découverte (esprit "Chatroulette audio")
|
||||
- ✅ Complémentarité conducteur/piéton (récit route vs récit urbain)
|
||||
- ✅ Fidèle concept RoadWave (usagers de la route connectés)
|
||||
- ✅ Réutilisation infrastructure WebRTC existante (radio live)
|
||||
|
||||
**Contraintes** :
|
||||
|
||||
- ❌ Nécessite pool minimum 500 utilisateurs actifs simultanés
|
||||
- ❌ Modération temps réel complexe (contenu éphémère)
|
||||
- ❌ Coût infrastructure TURN/STUN significatif
|
||||
@@ -233,10 +252,12 @@
|
||||
- ❌ Commandes vocales avancées requises pour conducteurs
|
||||
|
||||
**Monétisation** :
|
||||
|
||||
- Gratuit avec limitation : 3 sessions/jour de 5 min
|
||||
- Premium : sessions illimitées + matching prioritaire (moins d'attente)
|
||||
|
||||
**Aspects légaux** :
|
||||
|
||||
- Âge minimum : 18 ans pour accès roulette
|
||||
- Charte d'utilisation spécifique (respect, pas de contenu sexuel/violent, pas de sollicitation commerciale)
|
||||
- Anonymat relatif : pseudo + ville visible, pas de photo
|
||||
@@ -254,12 +275,14 @@
|
||||
6. Commandes vocales avancées implémentées pour conducteurs
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+3 mois) : Validation demande utilisateurs via sondages, analyse concurrence (Clubhouse, Twitter Spaces)
|
||||
- Phase 2 (Post-MVP+6 mois) : Développement matchmaking + WebRTC P2P renforcé
|
||||
- Phase 3 (Post-MVP+9 mois) : Tests bêta avec 100 utilisateurs volontaires
|
||||
- Phase 4 (Post-MVP+12 mois) : Déploiement progressif si KPI positifs (>70% satisfaction, <5% signalements)
|
||||
|
||||
**KPI de succès** :
|
||||
|
||||
- Temps moyen d'attente matching : <30 secondes
|
||||
- Taux satisfaction post-session : >70%
|
||||
- Taux signalement : <5%
|
||||
@@ -275,12 +298,14 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Coût SMS** : ~0.04€/SMS en France via Brevo (400€/mois pour 10K inscriptions)
|
||||
- **Complexité UX** : Étape supplémentaire à l'inscription (friction)
|
||||
- **Focus MVP** : Priorité sur l'expérience utilisateur fluide
|
||||
- **Modération suffisante** : Système de strikes et signalements couvre les cas d'abus initiaux
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Inscription par email uniquement (via Zitadel)
|
||||
- Confirmation email obligatoire
|
||||
- Détection basique emails jetables (liste noire publique)
|
||||
@@ -321,28 +346,33 @@
|
||||
- Au-delà → signalement automatique modération
|
||||
|
||||
**Affichage** :
|
||||
|
||||
- Badge "Vérifié ✓" visible sur profil créateur
|
||||
- Non obligatoire pour auditeurs simples (seulement créateurs)
|
||||
- Option "Vérifier mon compte" dans paramètres
|
||||
|
||||
**Règles de diffusion** :
|
||||
|
||||
- Contenus créateurs non-vérifiés : portée limitée à 10 km pendant 30 premiers jours
|
||||
- Après 30 jours sans signalement : levée restriction
|
||||
- Créateurs vérifiés : aucune restriction
|
||||
|
||||
**Avantages** :
|
||||
|
||||
- ✅ Réduction spam et comptes multiples
|
||||
- ✅ Amélioration confiance plateforme
|
||||
- ✅ Conformité anti-fraude (KYC léger)
|
||||
- ✅ Réutilisation infrastructure Brevo (emails + SMS)
|
||||
|
||||
**Contraintes** :
|
||||
|
||||
- ❌ Coût SMS : ~400€/mois pour 10K inscriptions/mois
|
||||
- ❌ Friction UX (étape supplémentaire)
|
||||
- ❌ Numéros virtuels (Twilio, etc.) contournent vérification
|
||||
- ❌ Certains utilisateurs réticents (vie privée)
|
||||
|
||||
**Alternatives étudiées** :
|
||||
|
||||
- **Captcha reCAPTCHA v3** : efficace mais contournable, pas de coût
|
||||
- **Email reputation API** : ~0.01€/vérification (kickbox.io)
|
||||
- **Vérification bancaire** : trop contraignant pour MVP
|
||||
@@ -359,12 +389,14 @@
|
||||
5. Conformité RGPD : consentement stockage numéro mobile
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+3 mois) : Analyse taux spam/abus, validation besoin
|
||||
- Phase 2 (Post-MVP+4 mois) : Développement détection emails temporaires + API Brevo SMS
|
||||
- Phase 3 (Post-MVP+5 mois) : Tests bêta avec créateurs volontaires
|
||||
- Phase 4 (Post-MVP+6 mois) : Déploiement progressif selon catégorie utilisateur (créateurs en priorité)
|
||||
|
||||
**KPI de succès** :
|
||||
|
||||
- Réduction comptes spam : >50%
|
||||
- Taux vérification volontaire (créateurs) : >70%
|
||||
- Friction UX acceptable : taux abandon inscription <10%
|
||||
@@ -379,6 +411,7 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Complexité technique** : Intégration API TTS (Text-to-Speech), OCR pour PDF scannés, parsing multi-formats
|
||||
- **Coût infrastructure** : ~0.016€/1000 caractères (Google Cloud TTS) = ~1.60€ par livre moyen (100K caractères)
|
||||
- **Conformité droits d'auteur** : Risque juridique si conversion de contenus protégés sans licence
|
||||
@@ -386,6 +419,7 @@
|
||||
- **Usage limité** : Cas d'usage minoritaire vs contenu audio créé par la communauté
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Contenu audio uniquement créé par les créateurs
|
||||
- Pas de conversion automatique document → audio
|
||||
- Utilisateurs doivent uploader directement fichiers audio
|
||||
@@ -452,12 +486,14 @@
|
||||
| **Allemand** | Anna, Max | Google Cloud TTS WaveNet |
|
||||
|
||||
**Avantages** :
|
||||
|
||||
- ✅ Différenciation Premium forte (feature exclusive)
|
||||
- ✅ Fidélisation utilisateurs (consommation contenu personnel)
|
||||
- ✅ Réutilisation infrastructure audio existante (HLS, NGINX Cache)
|
||||
- ✅ Cas d'usage trajets longs (livres, articles longs)
|
||||
|
||||
**Contraintes** :
|
||||
|
||||
- ❌ Coût TTS : ~1.60€/livre moyen (Google Cloud TTS WaveNet)
|
||||
- ❌ Coût stockage : ~0.01€/GB/mois (temporaire 90 jours)
|
||||
- ❌ Risque juridique : conversion contenus protégés (livres, articles premium)
|
||||
@@ -502,12 +538,14 @@
|
||||
5. Infrastructure existante stable (HLS, CDN, backend Go)
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+6 mois) : Étude de marché, sondage utilisateurs Premium, validation juridique
|
||||
- Phase 2 (Post-MVP+9 mois) : Développement MVP TTS (PDF texte uniquement, FR/EN)
|
||||
- Phase 3 (Post-MVP+10 mois) : Tests bêta avec 100 utilisateurs Premium volontaires
|
||||
- Phase 4 (Post-MVP+12 mois) : Déploiement progressif si KPI positifs + ajout formats (EPUB, OCR)
|
||||
|
||||
**KPI de succès** :
|
||||
|
||||
- Adoption feature : >30% utilisateurs Premium l'utilisent au moins 1 fois/mois
|
||||
- Satisfaction : >75% note positive (4-5/5)
|
||||
- Rétention Premium : augmentation >10% grâce à cette feature
|
||||
@@ -525,6 +563,7 @@
|
||||
| **Total** | **~650€/mois** |
|
||||
|
||||
**Rentabilité** :
|
||||
|
||||
- Revenus Premium 1000 users : 4990€/mois (4.99€/mois × 1000)
|
||||
- Coût TTS : 650€/mois (13% revenus)
|
||||
- Marge après TTS : 4340€/mois (87%)
|
||||
@@ -539,6 +578,7 @@
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Couverture limitée** : ~30-40% du parc automobile EU en 2026 (CarPlay/Android Auto)
|
||||
- **Complexité technique** : Intégration Siri Intents (iOS) + Google Actions (Android)
|
||||
- **Modération vocale** : Signalements vocaux nécessitent enregistrement + transcription audio
|
||||
@@ -546,6 +586,7 @@
|
||||
- **Accessibilité secondaire** : Like automatique couvre déjà engagement conducteurs
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- ❌ Pas de commandes vocales
|
||||
- ✅ Like automatique basé sur temps d'écoute (en voiture)
|
||||
- ✅ Actions manuelles disponibles seulement en mode piéton
|
||||
@@ -591,6 +632,7 @@
|
||||
**Implémentation technique** :
|
||||
|
||||
**iOS** :
|
||||
|
||||
- Siri Intents (framework iOS 12+)
|
||||
- Clés Intent à ajouter dans `Info.plist` :
|
||||
```xml
|
||||
@@ -602,25 +644,30 @@
|
||||
</dict>
|
||||
</array>
|
||||
```
|
||||
|
||||
- Paramètres vocaux : détection "like", "abonne", "signale"
|
||||
|
||||
**Android** :
|
||||
|
||||
- Google Actions on Google Assistant (via assistant voice queries)
|
||||
- Intégration avec Android App Actions
|
||||
- Paremeters: Intent extras pour passer contenu actuel
|
||||
- Fallback : repérer contenu par titre + créateur
|
||||
|
||||
**Limitation importante** :
|
||||
|
||||
- ⚠️ **CarPlay/Android Auto requis** : Fonctionalité non disponible sur interface mobile
|
||||
- ⚠️ **Reconnaissance vocale réseau** : Nécessite connexion data
|
||||
- ⚠️ **Latence acceptable** : <2 secondes entre commande et confirmation
|
||||
|
||||
**UX - Feedback utilisateur** :
|
||||
|
||||
- Siri : "✓ J'ai ajouté ce contenu à vos favoris"
|
||||
- Google Assistant : "✓ Vous êtes maintenant abonné à [Créateur]"
|
||||
- Confirmation audio pour signalement : "Signalement envoyé. Catégorie : Spam"
|
||||
|
||||
**Signalements vocaux** :
|
||||
|
||||
- Enregistrement automatique de la voix (tampon 30 secondes)
|
||||
- Transcription audio → texte (via Google Cloud Speech ou similaire)
|
||||
- Catégorie pré-remplie selon réponse vocale ("Spam" → catégorie Spam)
|
||||
@@ -638,12 +685,14 @@
|
||||
5. Système de confiance utilisateur en place (éviter abus signalements)
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+2 mois) : Validation demande utilisateurs (CarPlay/Android Auto)
|
||||
- Phase 2 (Post-MVP+4 mois) : Développement Siri Intents + Google Actions
|
||||
- Phase 3 (Post-MVP+6 mois) : Tests bêta avec conducteurs volontaires
|
||||
- Phase 4 (Post-MVP+8 mois) : Déploiement progressif si KPI positifs
|
||||
|
||||
**KPI de succès** :
|
||||
|
||||
- Adoption commandes vocales : >30% utilisateurs CarPlay/Android Auto
|
||||
- Taux erreur reconnaissance vocale : <10%
|
||||
- Satisfaction utilisateurs : >75% (4-5/5)
|
||||
@@ -660,6 +709,333 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. Contenus prioritaires et comptes officiels
|
||||
|
||||
> ⚠️ **Reporté post-MVP** - Système d'alertes critiques et intégration sources officielles (gestionnaires autoroutes, Météo France, préfectures).
|
||||
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- **Masse critique requise** : Partenariats avec organismes officiels nécessitent base utilisateurs solide (>50K MAU)
|
||||
- **Complexité technique** : Intégration APIs externes, système de priorités, TTS automatisé
|
||||
- **Responsabilité légale** : Diffusion alertes sécurité = engagement fort, nécessite infrastructure stable
|
||||
- **Focus MVP** : Priorité sur contenu créateurs communautaires
|
||||
- **ROI incertain** : Valeur ajoutée forte mais sans revenus directs (service public)
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
|
||||
- Tous contenus = créateurs classiques
|
||||
- Pas de système de priorité
|
||||
- Pas de comptes officiels vérifiés
|
||||
- Pas d'interruption de contenu en cours
|
||||
|
||||
---
|
||||
|
||||
### Spécifications complètes (future implémentation)
|
||||
|
||||
**Problématique** : Certaines informations (obstacle sur autoroute, alerte météo dangereuse) doivent être diffusées en **priorité absolue**, indépendamment de l'algorithme de recommandation.
|
||||
|
||||
**Solution** : Système de contenus prioritaires avec comptes officiels vérifiés et interruption conditionnelle du flux audio.
|
||||
|
||||
#### A) Nouveau type de compte : Compte Officiel
|
||||
|
||||
| Type compte | Validation | Badge | Priorité | Modération |
|
||||
|-------------|-----------|-------|----------|------------|
|
||||
| **Créateur classique** | Email + KYC (si monétisation) | - | Normale | 3 premiers contenus |
|
||||
| **Créateur vérifié** | KYC validé OU >10K abonnés | ✓ | Normale | A posteriori |
|
||||
| **Compte Officiel** | Validation RoadWave manuelle + contrat partenariat | 🏛️ | **Configurable (0-3)** | **Aucune** |
|
||||
|
||||
**Exemples comptes officiels** :
|
||||
|
||||
- **Gestionnaires autoroutes** : SANEF, Vinci Autoroutes, APRR, ASF
|
||||
- **Services météo** : Météo France, vigilance.gouv.fr
|
||||
- **Sécurité civile** : Préfectures, Plan alerte enlèvement
|
||||
- **Services publics** : Bison Futé, Sécurité Routière
|
||||
- **Médias publics** : France Info, France Inter (déjà créateurs, passage en Officiel)
|
||||
|
||||
**Processus de validation** :
|
||||
1. Demande partenariat → contact commercial RoadWave
|
||||
2. Vérification identité organisme (SIRET, documents officiels)
|
||||
3. Signature convention partenariat (gratuit, service d'intérêt public)
|
||||
4. Création compte Officiel avec badge 🏛️
|
||||
5. Configuration API Webhook pour contenus automatisés
|
||||
|
||||
---
|
||||
|
||||
#### B) Système de priorité des contenus
|
||||
|
||||
**Nouveau champ DB** : `priority_level`
|
||||
|
||||
```sql
|
||||
ALTER TABLE contents ADD COLUMN priority_level INT DEFAULT 0 CHECK (priority_level BETWEEN 0 AND 3);
|
||||
|
||||
-- 0 = Normal (créateurs classiques, algo standard)
|
||||
-- 1 = Élevé (infos trafic importantes, boost algo)
|
||||
-- 2 = Urgent (obstacle imminent, injection forcée)
|
||||
-- 3 = Critique (danger immédiat, interruption autorisée)
|
||||
```
|
||||
|
||||
**Comportement selon priorité** :
|
||||
|
||||
| Priorité | Nom | Comportement | Bypass quota 6/h | Interruption contenu en cours |
|
||||
|----------|-----|--------------|------------------|-------------------------------|
|
||||
| **0** | Normal | Algo standard (score géo + intérêts + engagement) | Non | Non |
|
||||
| **1** | Élevé | Boost score final +0.3 (favorisé mais pas forcé) | Non | Non |
|
||||
| **2** | Urgent | Injection forcée en **prochaine position** file d'attente | Oui | Non (attend fin contenu actuel) |
|
||||
| **3** | Critique | **Interruption immédiate avec countdown 5s** | Oui | **Oui** (pause contenu, overlay, lecture alerte) |
|
||||
|
||||
**Cas d'usage par priorité** :
|
||||
|
||||
```
|
||||
🟢 Priorité 0 - Normal
|
||||
├─ Tous contenus créateurs classiques
|
||||
└─ Algorithme de recommandation standard
|
||||
|
||||
🟡 Priorité 1 - Élevé
|
||||
├─ Info trafic général (bouchon prévu, travaux)
|
||||
├─ Événement local impactant circulation (match, concert)
|
||||
└─ Météo défavorable non dangereuse (pluie modérée)
|
||||
|
||||
🟠 Priorité 2 - Urgent
|
||||
├─ Accident récent avec impact circulation
|
||||
├─ Route coupée / déviation obligatoire
|
||||
├─ Péage fermé de façon imprévue
|
||||
└─ Alerte pollution temporaire
|
||||
|
||||
🔴 Priorité 3 - Critique
|
||||
├─ Obstacle sur voie (objet, véhicule arrêté)
|
||||
├─ Alerte météo orange/rouge (tempête, inondation, neige)
|
||||
├─ Alerte enlèvement (Plan alerte enlèvement)
|
||||
├─ Fermeture tunnel/pont pour sécurité
|
||||
└─ Contre-sens signalé
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### C) Flow interruption (priorité 3)
|
||||
|
||||
**Interface utilisateur** :
|
||||
|
||||
```
|
||||
User écoute podcast normal à 30 km/h sur A7
|
||||
↓
|
||||
Contenu priorité 3 détecté dans zone 500m devant
|
||||
↓
|
||||
Overlay rouge translucide apparaît sur écran :
|
||||
┌─────────────────────────────────────┐
|
||||
│ ⚠️ ALERTE SÉCURITÉ │
|
||||
│ │
|
||||
│ Obstacle signalé A7 voie gauche │
|
||||
│ km 125 │
|
||||
│ │
|
||||
│ Diffusion dans 5... 4... 3... │
|
||||
│ │
|
||||
│ [Ignorer l'alerte] │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
Countdown 5 secondes (annulable)
|
||||
↓
|
||||
Podcast actuel → PAUSE automatique
|
||||
↓
|
||||
Son d'alerte : Bip urgent (0.5s)
|
||||
↓
|
||||
Alerte TTS : "Attention, obstacle signalé sur voie de gauche, autoroute A7, kilomètre 125. Réduisez votre vitesse."
|
||||
↓
|
||||
Alerte se termine (15-30 secondes max)
|
||||
↓
|
||||
Podcast reprend automatiquement à position exacte
|
||||
```
|
||||
|
||||
**Paramètres techniques** :
|
||||
|
||||
- **Rayon déclenchement** : 500m-2km selon vitesse (calcul dynamique)
|
||||
- **Son d'alerte** : Bip distinctif (pas agressif, mais audible)
|
||||
- **Durée max alerte** : 30 secondes (format court, info essentielle)
|
||||
- **Cooldown** : même alerte pas reproposée avant 10 minutes
|
||||
- **Annulation** : bouton "Ignorer" disponible pendant countdown (mais déconseillé)
|
||||
|
||||
**Traçabilité** :
|
||||
|
||||
- Log : `user_id`, `alert_id`, `action` (played / ignored), `timestamp`
|
||||
- Statistiques : taux d'écoute alertes vs taux ignore (KPI efficacité)
|
||||
|
||||
---
|
||||
|
||||
#### D) Intégration APIs externes et TTS automatisé
|
||||
|
||||
**Partenariats cibles** :
|
||||
|
||||
| Partenaire | API | Type contenu | Priorité | Coût | Disponibilité |
|
||||
|-----------|-----|--------------|----------|------|---------------|
|
||||
| **Météo France** | API Vigilance | Alertes météo orange/rouge | 3 | Gratuit (service public) | ✅ API publique |
|
||||
| **Bison Futé** | API Trafic | Info trafic temps réel | 1-2 | Gratuit | ✅ API publique |
|
||||
| **Gestionnaires autoroutes** | APIs propriétaires | Obstacles, fermetures | 2-3 | Gratuit (partenariat) | ⚠️ Négociation |
|
||||
| **Sécurité Routière** | Données ouvertes | Zones accidentogènes, campagnes | 1 | Gratuit | ✅ Open Data |
|
||||
| **Waze / Coyote** | API (si accessible) | Dangers signalés users | 2 | Négociation | ❌ APIs fermées |
|
||||
|
||||
**Flow automatisé (exemple Météo France)** :
|
||||
|
||||
```
|
||||
1. API Météo France → Webhook RoadWave
|
||||
Données : {
|
||||
"departement": "83",
|
||||
"vigilance": "orange",
|
||||
"phenomene": "pluie-inondation",
|
||||
"debut": "2026-01-20T14:00:00Z",
|
||||
"fin": "2026-01-20T23:00:00Z"
|
||||
}
|
||||
|
||||
2. Backend RoadWave (worker Go) traite webhook :
|
||||
- Récupère polygon département 83 (PostGIS)
|
||||
- Génère texte alerte : "Alerte météo orange dans le Var : fortes pluies et risque d'inondations. Soyez prudents."
|
||||
- Appelle TTS (Google Cloud TTS ou AWS Polly)
|
||||
- Génère fichier audio MP3 + segments HLS
|
||||
|
||||
3. Création automatique contenu :
|
||||
├─ Titre : "⚠️ Alerte Météo Orange - Var"
|
||||
├─ Audio : Fichier TTS généré
|
||||
├─ Zone : Polygon département 83
|
||||
├─ Priority : 3 (critique)
|
||||
├─ Durée vie : 12h (expiration automatique)
|
||||
├─ Créateur : Compte "Météo France" (officiel)
|
||||
└─ Tags : ["Météo", "Sécurité"]
|
||||
|
||||
4. Diffusion immédiate :
|
||||
- Tous users dans département 83
|
||||
- Interruption flux audio (countdown 5s)
|
||||
- Diffusion alerte
|
||||
- Reprise contenu normal
|
||||
```
|
||||
|
||||
**TTS (Text-to-Speech)** :
|
||||
|
||||
- **Fournisseur** : Google Cloud TTS WaveNet (voix neurale professionnelle)
|
||||
- **Coût** : ~0.016€/1000 caractères
|
||||
- **Voix** : "Léa" (féminine, française, ton calme mais ferme pour alertes)
|
||||
- **Normalisation audio** : -14 LUFS (comme autres contenus)
|
||||
|
||||
**Expiration automatique** :
|
||||
|
||||
- Alertes météo : 12h après fin vigilance
|
||||
- Obstacles autoroute : 2h après signalement (si non mis à jour)
|
||||
- Alertes enlèvement : 48h ou jusqu'à résolution officielle
|
||||
|
||||
---
|
||||
|
||||
#### E) Dashboard admin (gestion alertes)
|
||||
|
||||
**Interface modérateur RoadWave** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ 🏛️ Gestion contenus officiels │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Alertes actives (3) │
|
||||
│ │
|
||||
│ 🔴 CRITIQUE - Obstacle A7 km 125 │
|
||||
│ Source : SANEF │
|
||||
│ Diffusions : 1,247 | Ignores : 23 (1.8%) │
|
||||
│ Expire : dans 1h32 │
|
||||
│ [Prolonger] [Arrêter maintenant] │
|
||||
│ │
|
||||
│ 🔴 CRITIQUE - Alerte météo orange Var │
|
||||
│ Source : Météo France │
|
||||
│ Diffusions : 8,921 | Ignores : 156 (1.7%) │
|
||||
│ Expire : dans 9h12 │
|
||||
│ [Modifier] [Arrêter] │
|
||||
│ │
|
||||
│ 🟠 URGENT - Bouchon A6 Lyon │
|
||||
│ Source : Bison Futé │
|
||||
│ Diffusions : 2,104 | Ignores : 312 (14.8%) │
|
||||
│ Expire : dans 3h05 │
|
||||
│ [Modifier] [Arrêter] │
|
||||
│ │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [+ Créer alerte manuelle] │
|
||||
│ │
|
||||
│ Historique (7 derniers jours) │
|
||||
│ · 127 alertes diffusées │
|
||||
│ · 98.2% taux écoute moyen │
|
||||
│ · 1.8% taux ignore moyen │
|
||||
│ │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Création alerte manuelle** :
|
||||
|
||||
- Use case : information non automatisée (événement exceptionnel)
|
||||
- Champs : Texte (TTS auto), Zone (carte), Priorité (1-3), Durée vie
|
||||
- Validation admin RoadWave requise (pas auto-publication)
|
||||
|
||||
---
|
||||
|
||||
### Avantages
|
||||
|
||||
- ✅ **Sécurité routière** : diffusion info critique temps réel
|
||||
- ✅ **Valeur ajoutée** : différenciation vs Waze/Coyote (audio automatique)
|
||||
- ✅ **Partenariats gagnant-gagnant** : visibilité organismes publics, service utilisateurs
|
||||
- ✅ **Coût maîtrisé** : APIs gratuites + TTS ponctuel (~50€/mois max)
|
||||
- ✅ **Réutilisation infra** : HLS, PostGIS, backend Go déjà en place
|
||||
|
||||
### Contraintes
|
||||
|
||||
- ❌ **Responsabilité légale** : diffusion alertes = engagement fort (info exacte, à jour)
|
||||
- ❌ **Partenariats longs** : négociations avec organismes publics (6-12 mois)
|
||||
- ❌ **Maintenance APIs** : dépendance externe, risque coupure service
|
||||
- ❌ **Modération réactive** : si alerte erronée, correction manuelle urgente
|
||||
- ❌ **Interruption UX** : priorité 3 peut frustrer si trop fréquent (nécessite calibration)
|
||||
|
||||
---
|
||||
|
||||
### Conditions de réintégration
|
||||
|
||||
**Prérequis** :
|
||||
1. Base utilisateurs stable >50K MAU (argumentaire crédible pour partenariats)
|
||||
2. Chiffre affaires positif (infrastructure fiable = confiance partenaires)
|
||||
3. Équipe support disponible 24/7 pour gestion alertes critiques
|
||||
4. Validation juridique responsabilité (assurance RC pro couvre diffusion alertes)
|
||||
5. Tests A/B réussis sur interruption priorité 3 (acceptabilité utilisateurs)
|
||||
|
||||
**Chronologie estimée** :
|
||||
|
||||
- Phase 1 (Post-MVP+6 mois) : Développement système priorités + dashboard admin + TTS
|
||||
- Phase 2 (Post-MVP+9 mois) : Premier partenariat (Météo France, API publique simple)
|
||||
- Phase 3 (Post-MVP+12 mois) : Tests bêta alertes météo avec utilisateurs volontaires
|
||||
- Phase 4 (Post-MVP+15 mois) : Extension autres partenaires (Bison Futé, gestionnaires autoroutes)
|
||||
- Phase 5 (Post-MVP+18 mois) : Déploiement complet si KPI positifs
|
||||
|
||||
**KPI de succès** :
|
||||
|
||||
- Taux écoute alertes priorité 3 : >95% (faible taux ignore)
|
||||
- Satisfaction utilisateurs : >80% jugent alertes utiles (sondage post-alerte)
|
||||
- Taux faux positifs : <2% (alerte diffusée à tort ou obsolète)
|
||||
- Réduction incidents : mesure impact (accidents évités, détours anticipés) → difficile mais qualitatif fort
|
||||
- Partenariats actifs : >3 organismes officiels connectés
|
||||
|
||||
**Budget estimé** (base 100K MAU) :
|
||||
|
||||
| Composant | Coût mensuel |
|
||||
|-----------|--------------|
|
||||
| **TTS alertes auto** | ~50€ (10-20 alertes/mois, textes courts) |
|
||||
| **Stockage audio alertes** | ~5€ (fichiers temporaires, expiration auto) |
|
||||
| **Modération alertes** | ~200€ (part-time, monitoring dashboard) |
|
||||
| **APIs externes** | 0€ (gratuites, services publics) |
|
||||
| **Bande passante** | Inclus infrastructure existante |
|
||||
| **Total** | **~255€/mois** |
|
||||
|
||||
**ROI** :
|
||||
|
||||
- Pas de revenus directs (service public)
|
||||
- Valeur indirecte : **différenciation produit majeure**
|
||||
- Argument commercial : "RoadWave vous protège en temps réel"
|
||||
- Rétention utilisateurs : +5-10% (feature killer)
|
||||
- Presse/médias : couverture positive (innovation sécurité routière)
|
||||
|
||||
---
|
||||
|
||||
## Autres fonctionnalités candidates Post-MVP
|
||||
|
||||
Liste non exhaustive de fonctionnalités évoquées mais non encore spécifiées :
|
||||
@@ -10,16 +10,18 @@
|
||||
- ✅ 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.
|
||||
> 📋 **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -35,11 +37,13 @@
|
||||
| **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)
|
||||
@@ -47,11 +51,13 @@
|
||||
#### 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
|
||||
@@ -61,22 +67,26 @@
|
||||
### 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)
|
||||
@@ -88,22 +98,26 @@
|
||||
**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 🟢 uniquement
|
||||
- Utilisateur 16-17 ans → contenus 🟢 🟡
|
||||
- Utilisateur 18+ → tous contenus
|
||||
|
||||
- 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
|
||||
@@ -113,17 +127,20 @@
|
||||
### 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)
|
||||
@@ -135,16 +152,19 @@
|
||||
**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€)
|
||||
@@ -154,16 +174,19 @@
|
||||
### 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€)
|
||||
@@ -173,19 +196,23 @@
|
||||
### 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
|
||||
@@ -197,15 +224,18 @@
|
||||
**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
|
||||
@@ -224,13 +254,16 @@
|
||||
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
|
||||
@@ -23,6 +23,7 @@
|
||||
| **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
|
||||
@@ -44,10 +45,12 @@
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -56,7 +59,7 @@
|
||||
|
||||
### 12.3 Perte de réseau
|
||||
|
||||
**Buffer adaptatif** (cf. TECHNICAL.md) :
|
||||
**Buffer adaptatif** (cf. [TECHNICAL.md](../../../TECHNICAL.md)) :
|
||||
|
||||
| Réseau | Buffer min | Buffer cible | Buffer max |
|
||||
|--------|------------|--------------|------------|
|
||||
@@ -67,30 +70,36 @@
|
||||
**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
|
||||
@@ -113,19 +122,23 @@
|
||||
| **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)
|
||||
619
docs/domains/_shared/rules/rgpd.md
Normal file
619
docs/domains/_shared/rules/rgpd.md
Normal file
@@ -0,0 +1,619 @@
|
||||
## 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
|
||||
4. Job quotidien automatique via cron
|
||||
|
||||
**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
|
||||
|
||||
**Format export** : Archive ZIP contenant JSON (machine-readable), HTML (human-readable), fichiers audio, README
|
||||
|
||||
**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é
|
||||
|
||||
---
|
||||
|
||||
### 13.11 Droit de rectification
|
||||
|
||||
**Décision** : Interface self-service + validation immédiate
|
||||
|
||||
**Données rectifiables** :
|
||||
|
||||
- Email (avec re-vérification)
|
||||
- Pseudo (unique, disponibilité vérifiée)
|
||||
- Bio / description
|
||||
- Centres d'intérêt (jauges)
|
||||
- Photo de profil
|
||||
|
||||
**Processus** :
|
||||
|
||||
- Changements immédiats (sauf email)
|
||||
- Email : lien vérification → validation sous 24h
|
||||
- Historique modifications conservé (audit trail)
|
||||
|
||||
**Limitations** :
|
||||
|
||||
- Pseudo : max 1 changement/30j (anti-squat)
|
||||
|
||||
**Justification** : Conformité Article 16 RGPD, self-service 0€
|
||||
|
||||
---
|
||||
|
||||
### 13.12 Droit d'opposition
|
||||
|
||||
**Décision** : Opt-out granulaire, effet immédiat
|
||||
|
||||
| Traitement | Toggle | Effet |
|
||||
|------------|--------|-------|
|
||||
| **Marketing email** | Paramètres | Stop emails promo |
|
||||
| **Notifications push** | Paramètres | Stop push marketing |
|
||||
| **Analytics** | Banner RGPD | Exclusion Matomo |
|
||||
| **Recommandations personnalisées** | "Mode anonyme" | Reco génériques uniquement |
|
||||
|
||||
**Mode anonyme** :
|
||||
|
||||
- Désactive algorithme (jauges ignorées)
|
||||
- Recommandations = top contenus zone géo uniquement
|
||||
- Historique non utilisé
|
||||
|
||||
**Justification** : Conformité Article 21 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.13 Droit à la limitation du traitement
|
||||
|
||||
**Décision** : "Geler mon compte" temporaire
|
||||
|
||||
**Effets** :
|
||||
|
||||
- Compte gelé, contenus cachés, profil invisible
|
||||
- Connexion lecture seule OK
|
||||
- Réactivation à tout moment
|
||||
|
||||
**Justification** : Conformité Article 18 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.14 Politique de confidentialité
|
||||
|
||||
**Décision** : Page web + popup in-app + versioning Git
|
||||
|
||||
**Emplacement** :
|
||||
|
||||
- Web : `roadwave.fr/confidentialite`
|
||||
- App : page dédiée paramètres
|
||||
- Popup première connexion (scroll requis)
|
||||
|
||||
**Contenu** : Identité responsable, finalités, base légale, destinataires, durées, droits, transferts UE
|
||||
|
||||
**Versioning** : Git + DB `privacy_policy_versions`, popup si changement majeur
|
||||
|
||||
**Justification** : Conformité Articles 13-14 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.15 Minimisation des données
|
||||
|
||||
**Décision** : Collecte strictement nécessaire
|
||||
|
||||
| Donnée | Finalité | Optionnel |
|
||||
|--------|----------|-----------|
|
||||
| Email | Authentification | ❌ |
|
||||
| Pseudo | Identité publique | ❌ |
|
||||
| GPS précis | Reco hyperlocales | ✅ (GeoIP fallback) |
|
||||
| Jauges intérêt | Reco thématiques | ✅ |
|
||||
| Date naissance | Vérifier âge minimum | ❌ (année seule) |
|
||||
|
||||
**Non collecté** : nom/prénom réels, adresse postale (sauf créateurs payés), téléphone (sauf 2FA optionnel)
|
||||
|
||||
**Justification** : Conformité Article 5.1.c RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.16 Sécurité des données
|
||||
|
||||
**Décision** : Chiffrement multi-niveaux
|
||||
|
||||
| Couche | Implémentation |
|
||||
|--------|----------------|
|
||||
| Transport | TLS 1.3 ([ADR-006](../../../adr/006-chiffrement.md)) |
|
||||
| DB | PostgreSQL encryption at rest AES-256 |
|
||||
| Tokens | JWT RS256 + rotation 90j |
|
||||
| CDN | Signed URLs expirables |
|
||||
| Backups | AES-256 + offsite |
|
||||
|
||||
**Mesures orga** : Whitelist IP, Vault secrets, logs anonymisés, 2FA admins, pentest annuel
|
||||
|
||||
**Justification** : Conformité Article 32 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.17 Transferts hors UE
|
||||
|
||||
**Décision** : Hébergement 100% France/UE
|
||||
|
||||
| Service | Localisation | Transfert UE |
|
||||
|---------|--------------|--------------|
|
||||
| Hébergement | OVH France | ❌ |
|
||||
| Database | OVH France | ❌ |
|
||||
| CDN | Bunny.net EU | ❌ |
|
||||
| Matomo | Self-hosted France | ❌ |
|
||||
|
||||
**Si CDN global futur** : Clauses Contractuelles Types (CCE) 2021
|
||||
|
||||
**Justification** : Conformité Articles 44-50 RGPD, souveraineté données
|
||||
|
||||
---
|
||||
|
||||
### 13.18 Profilage et décisions automatisées
|
||||
|
||||
**Décision** : Transparence + droit opposition
|
||||
|
||||
| Traitement | Impact | Intervention humaine | Opposition |
|
||||
|------------|--------|---------------------|------------|
|
||||
| Recommandations | Faible | ❌ | ✅ (mode anonyme) |
|
||||
| Modération auto | Élevé | ✅ (review 24h) | ✅ (appeal) |
|
||||
|
||||
**Transparence** : Page "Comment fonctionne l'algo ?", explications simplifiées
|
||||
|
||||
**Justification** : Conformité Article 22 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.19 Gestion des mineurs
|
||||
|
||||
**Décision** : 13 ans minimum + consentement parental 13-15 ans + RoadWave Kids
|
||||
|
||||
#### App principale (RoadWave)
|
||||
|
||||
**Âge minimum** : **13 ans** (alignement YouTube/TikTok)
|
||||
|
||||
**Processus inscription** :
|
||||
1. Saisie date naissance (JJ/MM/AAAA)
|
||||
2. **Si < 13 ans** : blocage + message redirection RoadWave Kids
|
||||
3. **Si 13-15 ans** : workflow consentement parental
|
||||
4. **Si ≥ 16 ans** : inscription directe
|
||||
|
||||
**Workflow consentement parental (13-15 ans)** :
|
||||
1. Ado saisit email parent
|
||||
2. Email automatique parent avec lien validation (expire 7j)
|
||||
3. Parent clique lien → page dédiée avec résumé données collectées, paramètres contrôle parental, checkbox consentement
|
||||
4. Validation parent → compte ado activé avec restrictions
|
||||
|
||||
**Restrictions 13-15 ans** :
|
||||
|
||||
- ✅ Écoute contenus autorisés
|
||||
- ✅ Création contenus (modération renforcée)
|
||||
- ⚠️ GPS précis : consentement parental explicite requis
|
||||
- ⚠️ Messagerie privée : désactivée par défaut
|
||||
- ⚠️ Contenus sensibles : filtrés (pas de contenu +16)
|
||||
- ⚠️ Profil public limité (pas d'affichage ville précise)
|
||||
|
||||
**Contrôles parentaux** :
|
||||
|
||||
- Dashboard parent : `roadwave.fr/parent/[child_id]`
|
||||
- Visualisation historique écoute
|
||||
- Activation/désactivation GPS précis
|
||||
- Activation/désactivation messagerie
|
||||
- Révocation consentement à tout moment
|
||||
- Notification hebdomadaire activité
|
||||
|
||||
**Vérification légère identité parent** :
|
||||
|
||||
- Email parent ≠ email ado (vérification domaine)
|
||||
- Lien expiration 7 jours
|
||||
- Pas de vérification identité forte (MVP)
|
||||
|
||||
#### RoadWave Kids (< 13 ans)
|
||||
|
||||
**App dédiée** : Version séparée avec contrôles renforcés
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- ❌ Pas de GPS précis (GeoIP ville uniquement)
|
||||
- ❌ Pas de création contenu
|
||||
- ❌ Pas de profil public
|
||||
- ❌ Pas de messagerie
|
||||
- ✅ Contenus présélectionnés (whitelist éditoriale)
|
||||
- ✅ Mode lecture seule
|
||||
- ✅ Contrôle parental obligatoire
|
||||
|
||||
**Contenus autorisés** :
|
||||
|
||||
- Contes audio enfants
|
||||
- Guides touristiques famille
|
||||
- Podcasts éducatifs labellisés
|
||||
- Histoires locales patrimoine
|
||||
|
||||
**Workflow inscription** :
|
||||
1. Création compte parent (RoadWave standard)
|
||||
2. Ajout profil enfant dans dashboard parent
|
||||
3. App Kids : login via QR code parent
|
||||
4. Pas de compte autonome enfant
|
||||
|
||||
**Modération** :
|
||||
|
||||
- 100% contenus présélectionnés par équipe éditoriale
|
||||
- Aucun UGC accessible
|
||||
- Whitelist créateurs vérifiés uniquement
|
||||
|
||||
**Justification** :
|
||||
|
||||
- Conformité Article 8 RGPD (13-16 ans selon pays)
|
||||
- 13 ans France = seuil légal avec consentement parental
|
||||
- App Kids = protection renforcée < 13 ans
|
||||
- Alignement marché (YouTube 13+, YouTube Kids)
|
||||
|
||||
**Roadmap** :
|
||||
|
||||
- **MVP** : App principale 16+ uniquement (simplicité)
|
||||
- **Phase 2** : Workflow 13-15 ans + consentement parental
|
||||
- **Phase 3** : RoadWave Kids (app séparée)
|
||||
|
||||
---
|
||||
|
||||
### 13.20 Sous-traitants et DPA
|
||||
|
||||
**Décision** : DPA systématique, audit annuel
|
||||
|
||||
| Service | Traitement | Localisation | DPA | Certifications |
|
||||
|---------|------------|--------------|-----|----------------|
|
||||
| OVH | Hébergement | France | ✅ | ISO 27001, HDS |
|
||||
| Bunny.net | CDN | UE | ✅ | ISO 27001 |
|
||||
| Brevo | Emailing | France | ✅ | RGPD certified |
|
||||
| Mangopay | Paiements | Luxembourg | ✅ | PCI-DSS, ACPR |
|
||||
|
||||
**Obligations DPA** : Traitement selon instructions, confidentialité, sécurité, assistance droits, suppression fin contrat
|
||||
|
||||
**Gestion** : `docs/rgpd/sous-traitants.md`, review annuelle
|
||||
|
||||
**Justification** : Conformité Article 28 RGPD
|
||||
|
||||
---
|
||||
|
||||
### 13.21 Analyse d'impact (DPIA)
|
||||
|
||||
**Décision** : DPIA obligatoire (GPS + profilage grande échelle)
|
||||
|
||||
**Raisons** :
|
||||
|
||||
- Traitement grande échelle données GPS sensibles
|
||||
- Profilage automatisé recommandations
|
||||
- Surveillance zones publiques
|
||||
|
||||
**Contenu** : Description traitement, finalités, nécessité, risques (tracking, profilage, fuite), mesures atténuation (anonymisation 24h, consentement, chiffrement, mode dégradé)
|
||||
|
||||
**Fichier** : `docs/rgpd/dpia-geolocalisation.md`, review annuelle
|
||||
|
||||
**Justification** : Conformité Article 35 RGPD (critères CNIL remplis)
|
||||
|
||||
---
|
||||
|
||||
### 13.22 Délai de réponse aux demandes
|
||||
|
||||
**Décision** : 1 mois max, automatisation maximale
|
||||
|
||||
**Canaux** : Email dpo@roadwave.fr, formulaire in-app, courrier postal
|
||||
|
||||
| Droit | Délai cible | Automatisation |
|
||||
|-------|-------------|----------------|
|
||||
| Accès (export) | 48h | ✅ Worker |
|
||||
| Rectification | Immédiat | ✅ Self-service |
|
||||
| Suppression | Immédiat | ✅ Self-service |
|
||||
| Opposition | Immédiat | ✅ Toggles |
|
||||
| Limitation | Immédiat | ✅ Gel compte |
|
||||
| Portabilité | 48h | ✅ Export |
|
||||
| Contestation décision | 24h | ⚠️ Manuel |
|
||||
|
||||
**Vérification identité** : Si email vérifié = aucune vérif supplémentaire
|
||||
|
||||
**Justification** : Conformité Article 12 RGPD
|
||||
56
docs/domains/_shared/sequences/anonymisation-gps.md
Normal file
56
docs/domains/_shared/sequences/anonymisation-gps.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Anonymisation automatique GPS après 24h
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Utilisateur
|
||||
participant App as Application
|
||||
participant DB as Base de données (PostGIS)
|
||||
participant Cron as Job Cron Quotidien
|
||||
|
||||
Note over User,App: Écoute de contenu avec GPS
|
||||
|
||||
User->>App: Écoute contenu (GPS activé)
|
||||
App->>App: Capturer position GPS précise
|
||||
App->>DB: INSERT position (lat, lon, anonymized: false)
|
||||
Note over DB: Position précise stockée<br/>Utilisée pour recommandations
|
||||
|
||||
Note over DB,Cron: Moins de 24h : position précise conservée
|
||||
|
||||
App->>DB: SELECT positions pour recommandations
|
||||
DB->>App: Positions GPS précises (< 24h)
|
||||
App->>User: Recommandations hyperlocales
|
||||
|
||||
Note over Cron: 24h+ plus tard
|
||||
|
||||
Cron->>DB: SELECT positions WHERE created_at < NOW() - 24h AND anonymized = false
|
||||
DB->>Cron: Liste positions à anonymiser
|
||||
|
||||
loop Pour chaque position
|
||||
Cron->>DB: Convertir (lat, lon) → geohash précision 5 (~5km²)
|
||||
Cron->>DB: UPDATE position avec geohash
|
||||
Cron->>DB: Supprimer coordonnées précises
|
||||
Cron->>DB: SET anonymized = true
|
||||
end
|
||||
|
||||
Cron->>DB: Log anonymisation (nombre positions traitées)
|
||||
|
||||
Note over DB: Positions anonymisées utilisées pour analytics
|
||||
|
||||
App->>DB: SELECT positions anonymisées (analytics globales)
|
||||
DB->>App: Positions geohash uniquement
|
||||
App->>App: Générer heatmap trafic (~5km² précision)
|
||||
|
||||
Note over User: Exception : historique personnel
|
||||
|
||||
User->>App: Consulter "Mon historique d'écoute"
|
||||
App->>DB: SELECT historique personnel utilisateur
|
||||
DB->>App: Positions précises conservées (tant que compte actif)
|
||||
App->>User: Trajets détaillés
|
||||
```
|
||||
|
||||
**Légende** :
|
||||
|
||||
- **< 24h** : GPS précis conservé (recommandations hyperlocales)
|
||||
- **> 24h** : Conversion automatique en geohash précision 5 (~5km²)
|
||||
- **Exception** : Historique personnel conservé intact tant que compte actif
|
||||
- **Analytics** : Uniquement positions anonymisées (geohash)
|
||||
45
docs/domains/_shared/sequences/authentification.md
Normal file
45
docs/domains/_shared/sequences/authentification.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Séquence - Authentification
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Utilisateur
|
||||
participant A as App Mobile
|
||||
participant Z as Zitadel
|
||||
participant API as Backend API
|
||||
participant DB as PostgreSQL
|
||||
|
||||
U->>A: Saisie email/password
|
||||
A->>Z: POST /oauth/token (email, password)
|
||||
Z->>Z: Validation credentials
|
||||
Z-->>A: access_token (15min) + refresh_token (30j)
|
||||
|
||||
A->>API: GET /api/user/profile (Bearer token)
|
||||
API->>Z: Validation JWT
|
||||
Z-->>API: Token valide + user_id
|
||||
API->>DB: SELECT user WHERE id = ?
|
||||
DB-->>API: Données utilisateur
|
||||
API->>DB: INSERT session (hash tokens, IP, device)
|
||||
DB-->>API: Session créée
|
||||
API-->>A: Profil utilisateur
|
||||
A->>U: Connexion réussie
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Acteurs** :
|
||||
|
||||
- Zitadel : Gère l'authentification OAuth2/OIDC
|
||||
- Backend API : Valide les tokens et accède aux données
|
||||
|
||||
**Tokens** :
|
||||
|
||||
- Access token : 15 min (JWT), utilisé pour chaque requête API
|
||||
- Refresh token : 30 jours, permet renouvellement access token
|
||||
|
||||
**Sécurité** :
|
||||
|
||||
- Tokens stockés hashés (SHA256) en DB
|
||||
- Device fingerprinting (OS, navigateur, IP)
|
||||
- Notification si nouveau device
|
||||
44
docs/domains/_shared/sequences/consentement-parental.md
Normal file
44
docs/domains/_shared/sequences/consentement-parental.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Consentement parental (13-15 ans)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Ado as Adolescent (13-15 ans)
|
||||
participant App as Application
|
||||
participant DB as Base de données
|
||||
participant Email as Service Email
|
||||
participant Parent as Parent
|
||||
|
||||
Ado->>App: Inscription (date naissance 13-15 ans)
|
||||
App->>Ado: Demande email parent
|
||||
Ado->>App: Saisit email parent
|
||||
|
||||
App->>DB: Créer compte (statut: pending_parental_consent)
|
||||
App->>Email: Envoyer email validation parent
|
||||
Email->>Parent: Email avec lien (expire 7j)
|
||||
App->>Ado: "En attente validation parentale"
|
||||
|
||||
Note over Parent: Parent clique lien validation
|
||||
|
||||
Parent->>App: Accès page consentement
|
||||
App->>Parent: Afficher résumé données + contrôles
|
||||
Parent->>App: Valider consentement + paramètres
|
||||
|
||||
App->>DB: Enregistrer consentement parental
|
||||
App->>DB: Activer compte (statut: active_minor)
|
||||
App->>DB: Appliquer restrictions 13-15 ans
|
||||
|
||||
App->>Email: Notification ado (compte activé)
|
||||
Email->>Ado: Email confirmation
|
||||
|
||||
App->>Email: Notification parent (récapitulatif)
|
||||
Email->>Parent: Email + lien dashboard parental
|
||||
|
||||
Ado->>App: Connexion
|
||||
App->>Ado: Accès restreint (GPS/messagerie selon config parent)
|
||||
```
|
||||
|
||||
**Légende** :
|
||||
|
||||
- Délai expiration lien : 7 jours
|
||||
- Restrictions 13-15 ans : GPS précis, messagerie, contenus +16 (configurables par parent)
|
||||
- Dashboard parent : `roadwave.fr/parent/[child_id]`
|
||||
66
docs/domains/_shared/sequences/export-donnees.md
Normal file
66
docs/domains/_shared/sequences/export-donnees.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Export de données (portabilité)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Utilisateur
|
||||
participant App as Application
|
||||
participant DB as Base de données
|
||||
participant Queue as File d'attente
|
||||
participant Worker as Worker Background
|
||||
participant CDN as CDN (fichiers audio)
|
||||
participant Storage as Stockage temporaire
|
||||
participant Email as Service Email
|
||||
|
||||
User->>App: Demande export données
|
||||
|
||||
App->>DB: Vérifier dernière demande
|
||||
alt Dernière demande < 30 jours
|
||||
DB->>App: Demande refusée
|
||||
App->>User: "Prochain export disponible dans X jours"
|
||||
else Demande autorisée
|
||||
App->>DB: Créer demande export (statut: pending)
|
||||
App->>Queue: Ajouter job export
|
||||
App->>User: "Export en cours, email sous 48h"
|
||||
|
||||
Queue->>Worker: Job export disponible
|
||||
|
||||
Note over Worker: Génération asynchrone
|
||||
|
||||
Worker->>DB: Récupérer profil utilisateur
|
||||
Worker->>DB: Récupérer historique d'écoute
|
||||
Worker->>DB: Récupérer contenus créés (métadonnées)
|
||||
Worker->>DB: Récupérer centres d'intérêt
|
||||
Worker->>DB: Récupérer historique consentements
|
||||
|
||||
Worker->>CDN: Télécharger fichiers audio utilisateur
|
||||
CDN->>Worker: Fichiers audio (.opus)
|
||||
|
||||
Worker->>Worker: Générer export.json (machine-readable)
|
||||
Worker->>Worker: Générer index.html (human-readable)
|
||||
Worker->>Worker: Générer README.txt
|
||||
Worker->>Worker: Créer archive ZIP
|
||||
|
||||
Worker->>Storage: Stocker ZIP (expire 7j)
|
||||
Storage->>Worker: URL signée (expire 7j)
|
||||
|
||||
Worker->>DB: Mettre à jour demande (statut: completed)
|
||||
Worker->>DB: Enregistrer URL + date expiration
|
||||
|
||||
Worker->>Email: Envoyer email avec lien
|
||||
Email->>User: Email + lien téléchargement (valide 7j)
|
||||
|
||||
User->>Storage: Clic lien téléchargement
|
||||
Storage->>User: Téléchargement ZIP
|
||||
|
||||
Note over Storage: Après 7 jours
|
||||
|
||||
Storage->>Storage: Suppression automatique ZIP
|
||||
end
|
||||
```
|
||||
|
||||
**Légende** :
|
||||
|
||||
- Limite : 1 export / 30 jours (anti-abus)
|
||||
- Délai génération : 48h maximum (conformité RGPD Article 20)
|
||||
- Expiration lien : 7 jours
|
||||
- Format : ZIP contenant JSON, HTML, audio, README
|
||||
44
docs/domains/_shared/sequences/moderation-contenu.md
Normal file
44
docs/domains/_shared/sequences/moderation-contenu.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Séquence - Modération de contenu
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Créateur
|
||||
participant API as Backend API
|
||||
participant DB as PostgreSQL
|
||||
participant Q as Queue
|
||||
participant M as Modérateur
|
||||
participant N as Notification
|
||||
|
||||
C->>API: POST /contents (3 premiers contenus)
|
||||
API->>DB: INSERT content (status=pending_review)
|
||||
API->>Q: Ajout file modération
|
||||
API-->>C: Contenu soumis
|
||||
|
||||
Q->>M: Notification nouveau contenu
|
||||
M->>API: GET /moderation/contents/pending
|
||||
API-->>M: Liste contenus à modérer
|
||||
|
||||
M->>M: Écoute + vérification
|
||||
|
||||
alt Validation
|
||||
M->>API: POST /moderation/approve/{id}
|
||||
API->>DB: UPDATE status=published
|
||||
API->>N: Email + push créateur
|
||||
N-->>C: Contenu publié ✓
|
||||
else Rejet
|
||||
M->>API: POST /moderation/reject/{id} (motif)
|
||||
API->>DB: UPDATE status=rejected
|
||||
API->>DB: INSERT strike (si grave)
|
||||
API->>N: Email créateur (motif)
|
||||
N-->>C: Contenu rejeté + motif
|
||||
end
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Modération préalable** : 3 premiers contenus uniquement
|
||||
**Après validation** : Publication directe (modération a posteriori via signalements)
|
||||
**Délai** : 48h maximum
|
||||
**Strike** : +1 si violation grave
|
||||
64
docs/domains/_shared/sequences/notification-breach.md
Normal file
64
docs/domains/_shared/sequences/notification-breach.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Notification violation de données (breach) - Procédure 72h CNIL
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Monitoring as Monitoring (Sentry/Grafana)
|
||||
participant Equipe as Équipe Technique
|
||||
participant DPO as DPO
|
||||
participant DB as Base de données
|
||||
participant CNIL as CNIL
|
||||
participant Users as Utilisateurs impactés
|
||||
|
||||
Note over Monitoring: H+0 - Détection
|
||||
|
||||
Monitoring->>Equipe: Alerte breach détecté
|
||||
Equipe->>Equipe: Confinement immédiat
|
||||
Equipe->>DB: Bloquer accès compromis
|
||||
Equipe->>DPO: Notification breach
|
||||
|
||||
Note over DPO,Equipe: H+0 à H+24 - Évaluation
|
||||
|
||||
DPO->>DB: Investigation périmètre
|
||||
DB->>DPO: Données compromises
|
||||
DPO->>DB: Liste utilisateurs impactés
|
||||
DB->>DPO: X utilisateurs
|
||||
|
||||
DPO->>DPO: Évaluation gravité
|
||||
Note over DPO: - Type données (GPS, email, etc.)<br/>- Nombre utilisateurs<br/>- Risque pour droits/libertés
|
||||
|
||||
alt Risque pour utilisateurs
|
||||
Note over DPO: H+24 à H+48 - Préparation notification CNIL
|
||||
|
||||
DPO->>DPO: Rédaction rapport breach
|
||||
Note over DPO: - Nature violation<br/>- Catégories/nb données<br/>- Conséquences probables<br/>- Mesures prises/envisagées
|
||||
|
||||
DPO->>CNIL: Notification sous 72h (email + formulaire en ligne)
|
||||
CNIL->>DPO: Accusé réception
|
||||
|
||||
alt Risque élevé pour utilisateurs
|
||||
Note over DPO: H+48 à H+72 - Notification utilisateurs
|
||||
|
||||
DPO->>Users: Email notification breach
|
||||
Note over Users: - Nature violation<br/>- Coordonnées DPO<br/>- Mesures prises<br/>- Recommandations (changer mdp, etc.)
|
||||
|
||||
DPO->>Users: Notification in-app
|
||||
end
|
||||
else Risque faible (mesures techniques suffisantes)
|
||||
DPO->>DPO: Documentation interne uniquement
|
||||
Note over DPO: Pas de notification CNIL requise
|
||||
end
|
||||
|
||||
Note over DPO: Post-incident
|
||||
|
||||
DPO->>Equipe: Audit sécurité complet
|
||||
DPO->>DB: Enregistrement incident (registre violations)
|
||||
DPO->>DPO: Plan correctif
|
||||
```
|
||||
|
||||
**Légende** :
|
||||
|
||||
- **H+0 à H+24** : Détection, confinement, évaluation périmètre
|
||||
- **H+24 à H+48** : Évaluation gravité, préparation rapport
|
||||
- **H+48 à H+72** : Notification CNIL (si risque) + utilisateurs (si risque élevé)
|
||||
- Délai CNIL : **72h maximum** (Article 33 RGPD)
|
||||
- Notification utilisateurs obligatoire si **risque élevé** pour droits/libertés (Article 34 RGPD)
|
||||
40
docs/domains/_shared/sequences/refresh-token.md
Normal file
40
docs/domains/_shared/sequences/refresh-token.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Séquence - Refresh Token
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as App Mobile
|
||||
participant Z as Zitadel
|
||||
participant DB as PostgreSQL
|
||||
|
||||
Note over A: Access token expiré (15min)
|
||||
|
||||
A->>Z: POST /oauth/token (refresh_token)
|
||||
Z->>DB: Vérification hash refresh_token
|
||||
|
||||
alt Token valide
|
||||
Z->>Z: Génération nouveaux tokens
|
||||
Z->>DB: Update session (nouveau hash)
|
||||
Z->>DB: Invalidation ancien refresh_token
|
||||
Z-->>A: Nouveaux tokens
|
||||
Note over DB: Rotation complète
|
||||
else Token invalide ou rejoué
|
||||
Z->>DB: Révocation toutes sessions user
|
||||
Z-->>A: 401 Unauthorized
|
||||
Note over A: Reconnexion requise
|
||||
end
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Rotation** :
|
||||
|
||||
- Nouveau refresh_token à chaque refresh
|
||||
- Ancien token invalidé immédiatement
|
||||
- Prévient replay attack
|
||||
|
||||
**Sécurité** :
|
||||
|
||||
- Si ancien token réutilisé → révocation globale
|
||||
- Logs sécurité + notification utilisateur
|
||||
51
docs/domains/_shared/sequences/signalement.md
Normal file
51
docs/domains/_shared/sequences/signalement.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Séquence - Signalement de contenu
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Utilisateur
|
||||
participant API as Backend API
|
||||
participant DB as PostgreSQL
|
||||
participant M as Modérateur
|
||||
participant C as Créateur
|
||||
participant N as Notification
|
||||
|
||||
U->>API: POST /reports (content_id, category, comment)
|
||||
API->>DB: INSERT report (status=pending)
|
||||
API->>DB: UPDATE contents.reports_count++
|
||||
|
||||
alt Priorité haute (3+ reports)
|
||||
API->>N: Alerte modérateurs
|
||||
end
|
||||
|
||||
API-->>U: Signalement enregistré
|
||||
|
||||
M->>API: GET /moderation/reports/pending
|
||||
API-->>M: Reports triés par priorité
|
||||
|
||||
M->>API: PUT /reports/{id} (status=under_review)
|
||||
M->>M: Écoute contenu + contexte
|
||||
|
||||
alt Violation confirmée
|
||||
M->>API: POST /moderation/action (action_taken)
|
||||
API->>DB: UPDATE content.status=moderated
|
||||
API->>DB: UPDATE report.status=actioned
|
||||
API->>DB: INSERT strike (créateur)
|
||||
API->>N: Notification créateur + signaleur
|
||||
N-->>C: Contenu retiré (motif + appel)
|
||||
N-->>U: Action prise
|
||||
else Infondé
|
||||
M->>API: PUT /reports/{id} (status=dismissed)
|
||||
API->>DB: UPDATE report
|
||||
API->>N: Notification signaleur
|
||||
N-->>U: Signalement rejeté
|
||||
end
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Priorité haute** : 3+ signalements ou catégories critiques (hate_speech, violence)
|
||||
**Délai** : < 24h priorité haute, < 48h normale
|
||||
**Anti-abus** : > 5 dismissed → warning signaleur
|
||||
**Appel** : Créateur peut contester 7j après moderation
|
||||
52
docs/domains/_shared/sequences/suppression-compte.md
Normal file
52
docs/domains/_shared/sequences/suppression-compte.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Suppression compte avec grace period
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Utilisateur
|
||||
participant App as Application
|
||||
participant DB as Base de données
|
||||
participant Email as Service Email
|
||||
participant Cron as Job Cron
|
||||
|
||||
User->>App: Demande suppression compte
|
||||
App->>User: Confirmation (êtes-vous sûr ?)
|
||||
User->>App: Confirmer
|
||||
|
||||
App->>DB: Désactiver compte (statut: pending_deletion)
|
||||
App->>DB: Cacher contenus (visible: false)
|
||||
App->>DB: Révoquer sessions/tokens
|
||||
App->>DB: Enregistrer date suppression effective (J+30)
|
||||
|
||||
App->>Email: Email confirmation + lien annulation
|
||||
Email->>User: Email (lien valide 30j)
|
||||
App->>User: "Compte désactivé. Suppression dans 30 jours."
|
||||
|
||||
Note over User,App: Grace period 30 jours
|
||||
|
||||
alt Utilisateur annule
|
||||
User->>App: Clic lien annulation
|
||||
App->>DB: Réactiver compte (statut: active)
|
||||
App->>DB: Restaurer visibilité contenus
|
||||
App->>Email: Email confirmation réactivation
|
||||
Email->>User: "Compte réactivé"
|
||||
else 30 jours sans annulation
|
||||
Cron->>DB: Job quotidien (vérif comptes pending_deletion)
|
||||
DB->>Cron: Liste comptes J+30 dépassé
|
||||
|
||||
loop Pour chaque compte
|
||||
Cron->>DB: Supprimer données personnelles
|
||||
Cron->>DB: Anonymiser contenus (créateur: "Utilisateur supprimé")
|
||||
Cron->>DB: Supprimer historique GPS/écoute
|
||||
Cron->>DB: Marquer statut: deleted
|
||||
end
|
||||
|
||||
Cron->>Email: Email confirmation suppression effective
|
||||
Email->>User: "Compte définitivement supprimé"
|
||||
end
|
||||
```
|
||||
|
||||
**Légende** :
|
||||
|
||||
- Grace period : 30 jours
|
||||
- Pendant grace period : compte inaccessible, contenus cachés
|
||||
- Après 30j : suppression définitive, contenus anonymisés conservés
|
||||
44
docs/domains/_shared/states/account-deletion-lifecycle.md
Normal file
44
docs/domains/_shared/states/account-deletion-lifecycle.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Cycle de vie - Suppression de compte
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Requested: Utilisateur demande suppression
|
||||
|
||||
Requested --> GracePeriod: Compte désactivé, email envoyé
|
||||
|
||||
GracePeriod --> Cancelled: Clic lien annulation (< 30j)
|
||||
GracePeriod --> PendingDeletion: Délai 30j écoulé
|
||||
|
||||
Cancelled --> [*]
|
||||
|
||||
PendingDeletion --> Deleted: Job cron suppression effective
|
||||
|
||||
Deleted --> [*]
|
||||
```
|
||||
|
||||
## Règles
|
||||
|
||||
| État | Valeur | Description |
|
||||
|------|--------|-------------|
|
||||
| Requested | `requested` | Demande initiée, validation requise |
|
||||
| Grace Period | `grace_period` | 30j annulation possible, compte inaccessible |
|
||||
| Cancelled | `cancelled` | Utilisateur a annulé, compte réactivé |
|
||||
| Pending Deletion | `pending_deletion` | File job cron (< 24h) |
|
||||
| Deleted | `deleted` | Données supprimées, contenus anonymisés |
|
||||
|
||||
**Grace period** : 30 jours
|
||||
**Pendant grace period** :
|
||||
|
||||
- Compte désactivé (login impossible)
|
||||
- Contenus cachés (non diffusés)
|
||||
- Sessions/tokens révoqués
|
||||
- Email avec token annulation (valide 30j)
|
||||
|
||||
**Après 30j** :
|
||||
|
||||
- Données personnelles supprimées
|
||||
- Contenus créés anonymisés (créateur = "Utilisateur supprimé")
|
||||
- Historique GPS/écoute supprimé
|
||||
- Irréversible
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user