# Rapport de phase — Phase 7 : Cookies & consentement (RGPD)

> Migration HR CONSULTING & CO (Next.js → Laravel 12).
> Branche : `phase-7` · Base : `phase-6` · Date : 2026-06-07.

---

## 1. Résumé

| | |
|---|---|
| **Objectif (PLAN.md §113-121)** | Bandeau de consentement RGPD (Accepter/Refuser/Personnaliser) avec catégories (Nécessaires/Analytics/Marketing), mémorisation du choix (cookie + journal en base), pages Politique de cookies & de confidentialité éditables en backoffice, et conditionnement du futur tracking analytics (Phase 8) au consentement. |
| **Livrable atteint** | ✅ Oui — bandeau Alpine fonctionnel et réouvrable, choix journalisé (`cookie_consents`, IP pseudonymisée) + cookie `cookie_consent` (6 mois), pages légales rendues depuis des textes éditables (HTML purifié), backoffice de consultation/export, helper `cookie_consent('analytics')` prêt pour la Phase 8. |
| **Écarts / périmètre** | Le **gating effectif** du tracking analytics sera branché en Phase 8 (le helper de lecture du consentement est livré ici). Purge/rétention du journal de consentements : reportée (TODO Phase 8/9). |

---

## 2. Travaux réalisés

### Consentement (enregistrement)
- `app/Http/Controllers/CookieConsentController.php` — `POST /cookie-consent` : valide les catégories (boolean), réutilise/génère le `consent_id`, journalise dans `cookie_consents` (IP **pseudonymisée HMAC-SHA256**, user-agent tronqué), pose le cookie `cookie_consent` (6 mois, `HttpOnly`, `SameSite=Lax`, `Secure` hors local).
- `app/Support/helpers.php` — helper `cookie_consent($category)` : lit le choix côté serveur (catégorie → bool, ou tableau complet). Base du conditionnement analytics (Phase 8).
- `bootstrap/app.php` — `cookie_consent` exclu du chiffrement des cookies (contenu non sensible : booléens + identifiant aléatoire ; relu côté serveur).
- `routes/web.php` — routes `/politique-cookies`, `/politique-confidentialite`, et `POST /cookie-consent` (`throttle:30,1`).

### Bandeau (front)
- `resources/views/components/cookie-consent.blade.php` — bandeau Blade + Alpine : **toujours présent** mais affiché seulement sans choix enregistré (état serveur), **réouvrable** via l'événement `open-cookie-settings` ; trois actions (Tout accepter / Tout refuser / Personnaliser avec cases Nécessaires [verrouillée] / Analytics / Marketing) ; envoi `fetch` POST avec `X-CSRF-TOKEN` ; transitions, `x-cloak`, a11y (`role="dialog"`).
- `resources/views/layouts/app.blade.php` — injection du bandeau.
- `resources/views/components/footer.blade.php` — liens « Politique de confidentialité », « Politique de cookies » et bouton « Gérer les cookies ».

### Pages légales
- `resources/views/legal/cookies.blade.php` & `privacy.blade.php` — rendu du contenu éditable (`setting('cookie_policy'/'privacy_policy')`) **purifié** via `clean()` (mews/purifier) ; la page cookies propose un bouton « Gérer mes préférences ».
- `app/Http/Controllers/PageController.php` — méthodes `cookiePolicy()` / `privacy()`.

### Backoffice
- `app/Filament/Pages/ManageSettings.php` — onglet **« Cookies & RGPD »** : texte du bandeau (`Textarea`) + politiques (`RichEditor`).
- `database/seeders/SettingSeeder.php` — textes par défaut (`cookie_banner_text`, `cookie_policy`, `privacy_policy`).
- `app/Filament/Resources/CookieConsentResource.php` (+ `Pages/ListCookieConsents.php`) — journal en lecture seule (filtres analytics/marketing, **export CSV** avec neutralisation d'injection de formule, IP pseudonymisée affichée tronquée), `canCreate()=false`.

### Décisions d'architecture (non tranchées par PLAN)
- **Bandeau rendu côté serveur** (état `open` selon présence du cookie) plutôt que 100 % JS : pas de flash, réouvrable, et lisible sans dépendre du déchiffrement.
- **Cookie non chiffré mais HttpOnly** : contenu non sensible, relu côté serveur ; cohérent avec les solutions de consentement et testable.
- **IP pseudonymisée (HMAC)** plutôt qu'en clair — preuve de consentement traçable sans exposer l'adresse.

---

## 3. Tests fonctionnels (parcours critiques)

| Scénario | Résultat |
|---|---|
| « Tout accepter » → journal (catégories actives) + cookie posé + IP pseudonymisée (jamais en clair) | ✅ |
| « Tout refuser » → journal (analytics/marketing à false) | ✅ |
| Validation serveur : catégories requises et booléennes (422 sinon) | ✅ |
| Réutilisation de l'`consent_id` existant (journal historisé) | ✅ |
| Bandeau affiché sans choix (`open: true`), masqué avec cookie (`open: false`) | ✅ |
| Pages légales rendues depuis les textes paramétrés (HTML purifié) | ✅ |
| Backoffice : invité redirigé, non-admin 403, admin liste le journal | ✅ |
| Journal non créable (`canCreate=false`) | ✅ |
| Export CSV : en-tête + lignes, IP pseudonymisée (jamais en clair) | ✅ |

---

## 4. Couverture de tests

- **Suite complète : 128 tests verts, 451 assertions** (`php artisan test`). Phase 7 : **+11 tests** (`CookieConsentTest` 6, `CookieConsentAdminTest` 5) + ajustements (`AdminPanelResourcesTest`, `PublicSiteTest`).
- Note test : `cookie_consent` étant exclu du chiffrement, les tests l'injectent via `withUnencryptedCookie` (et un POST formulaire — `postJson` ne transmet pas les cookies non chiffrés).
- % couverture ligne : non mesuré (ni Xdebug ni PCOV) ; couverture fonctionnelle des parcours critiques assurée.

---

## 5. Tests de sécurité (OWASP Top 10 + RGPD)

Revue par agent sécurité dédié (serveur MCP Aikido non configuré). `composer audit` (A06) : aucune vulnérabilité.

| Domaine | Verdict |
|---|---|
| **A03 XSS** | ✅ Pages légales `{!! clean(...) !!}` (profil purifier `default` : scripts/`on*`/`javascript:` bloqués) ; texte bandeau `{{ }}` échappé ; valeurs du script Alpine via `@js`/ternaires stricts. |
| **A01 Access Control** | ✅ Resource journal réservée aux admins (gate `is_admin`), `canCreate=false` ; route POST publique par nature (consentement anonyme). |
| **A04 / CSRF** | ✅ POST dans le groupe `web` (CSRF actif) + `throttle:30,1` ; le fetch envoie `X-CSRF-TOKEN`. |
| **A08 Validation** | ✅ Catégories `required|boolean` ; `necessary` forcé serveur (non pilotable client). |
| **RGPD** | ✅ IP **pseudonymisée** (HMAC-SHA256), `consent_id` aléatoire, cookie sans PII, `HttpOnly`+`SameSite=Lax`+`Secure` (prod). |
| **Injection CSV** | ✅ `neutralizeCsvFormula` sur les colonnes texte. |

### Correctifs appliqués pendant la phase
- **F2** : cookie explicitement `SameSite=Lax` + `Secure` hors local.
- **M1** : IP via `hash_hmac` (au lieu de `hash` + concaténation) ; terminologie corrigée en « pseudonymisation ».

### Risques résiduels acceptés / TODO
- **M2** : purge/rétention du journal `cookie_consents` (≈13 mois) non encore planifiée → à brancher avec la purge des `visits` (Phase 8/9).
- **F1** : `user_agent` conservé (preuve de consentement) — à mentionner dans la politique de confidentialité.
- Pseudonymisation IP non irréversible (limite intrinsèque du hachage d'IP) — acceptable pour une preuve de consentement.

**Verdict sécurité : OK pour merge**, aucun finding critique/élevé.

---

## 6. Performance / scalabilité

- Bandeau : un seul `fetch` au moment du choix ; pas de requête tant qu'aucune action.
- Lecture du consentement côté serveur via le cookie (pas de requête DB par page) — efficace pour le futur gating analytics.
- Export CSV par lots (`chunk(500)`).
- Le journal est indexé sur `consent_id` et `ip_hash`.

---

## 7. Dette technique / TODO reportés

- **Gating analytics** : conditionner réellement le tracking (Phase 8) à `cookie_consent('analytics')`.
- **Purge RGPD** du journal `cookie_consents` (tâche planifiée) → Phase 8/9.
- **Minimisation** : envisager de réduire/retirer `user_agent` du journal.
- **Anonymisation forte de l'IP** (troncature /24–/48) pour l'analytics Phase 8.

---

## 8. Lien PR & branche

- **Branche** : `phase-7` (poussée sur `origin`).
- **Pull Request** : `gh` indisponible — ouvrir via le diff Phase 7 (base `phase-6`) :
  https://github.com/HR-CONSULTING-CO/hrconsulting_website/compare/phase-6...phase-7?expand=1
  - Titre : **Phase 7 — Cookies & consentement (RGPD)**.
