Story 1.1: initialisation projet
This commit is contained in:
254
docs/stories/5.4.integration-recaptcha.md
Normal file
254
docs/stories/5.4.integration-recaptcha.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Story 5.4: Intégration reCAPTCHA v3
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** propriétaire du site,
|
||||
**I want** une protection anti-spam invisible,
|
||||
**so that** je ne reçois pas de spam sans pénaliser l'expérience utilisateur.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. reCAPTCHA v3 est intégré (invisible, pas de case à cocher)
|
||||
2. Le script reCAPTCHA est chargé depuis Google
|
||||
3. Un token est généré à la soumission du formulaire
|
||||
4. Le token est envoyé avec les données du formulaire au backend PHP
|
||||
5. Les clés API (site key) sont configurables (fichier de config ou .env)
|
||||
6. Si reCAPTCHA échoue à charger, le formulaire reste utilisable (dégradation gracieuse)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5)
|
||||
- [] Ajouter RECAPTCHA_SITE_KEY dans .env
|
||||
- [] Ajouter RECAPTCHA_SECRET_KEY dans .env
|
||||
- [] Créer includes/config.php pour charger .env et définir les constantes
|
||||
|
||||
- [] **Task 2 : Charger le script Google** (AC: 2)
|
||||
- [] Ajouter le script dans templates/footer.php
|
||||
- [] Charger de manière asynchrone (async defer)
|
||||
- [] Exposer la site key via window.RECAPTCHA_SITE_KEY
|
||||
|
||||
- [] **Task 3 : Générer le token** (AC: 3)
|
||||
- [] Créer RecaptchaService dans contact-form.js
|
||||
- [] Méthode getToken() avec grecaptcha.execute()
|
||||
- [] Retourne une Promise avec le token
|
||||
|
||||
- [] **Task 4 : Envoyer le token au backend** (AC: 4)
|
||||
- [] RecaptchaService.getToken() prêt à être utilisé
|
||||
- [] Intégration avec AJAX dans Story 5.5/5.6
|
||||
|
||||
- [] **Task 5 : Dégradation gracieuse** (AC: 6)
|
||||
- [] isAvailable() vérifie si grecaptcha est défini
|
||||
- [] Retourne chaîne vide si indisponible
|
||||
- [] console.warn si non disponible
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Configuration .env
|
||||
|
||||
```env
|
||||
# reCAPTCHA v3
|
||||
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
|
||||
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
|
||||
```
|
||||
|
||||
Note : Les clés ci-dessus sont les clés de test de Google (fonctionnent partout mais retournent toujours un score de 0.9).
|
||||
|
||||
### Exposer la Site Key (config.php ou header.php)
|
||||
|
||||
```php
|
||||
<!-- Dans templates/footer.php ou avant </body> -->
|
||||
|
||||
<?php if (defined('RECAPTCHA_SITE_KEY') && RECAPTCHA_SITE_KEY): ?>
|
||||
<script>
|
||||
window.RECAPTCHA_SITE_KEY = '<?= RECAPTCHA_SITE_KEY ?>';
|
||||
</script>
|
||||
<script src="https://www.google.com/recaptcha/api.js?render=<?= RECAPTCHA_SITE_KEY ?>" async defer></script>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Service JavaScript
|
||||
|
||||
```javascript
|
||||
// assets/js/contact-form.js (ajouter)
|
||||
|
||||
/**
|
||||
* Service reCAPTCHA v3
|
||||
*/
|
||||
const RecaptchaService = {
|
||||
siteKey: null,
|
||||
|
||||
init() {
|
||||
this.siteKey = window.RECAPTCHA_SITE_KEY || null;
|
||||
},
|
||||
|
||||
isAvailable() {
|
||||
return this.siteKey && typeof grecaptcha !== 'undefined';
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtient un token reCAPTCHA
|
||||
* @param {string} action - Action à valider (ex: 'contact')
|
||||
* @returns {Promise<string>} - Token ou chaîne vide si indisponible
|
||||
*/
|
||||
async getToken(action = 'contact') {
|
||||
// Dégradation gracieuse si reCAPTCHA non disponible
|
||||
if (!this.isAvailable()) {
|
||||
console.warn('reCAPTCHA non disponible, envoi sans protection');
|
||||
return '';
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
grecaptcha.ready(() => {
|
||||
grecaptcha.execute(this.siteKey, { action })
|
||||
.then(token => resolve(token))
|
||||
.catch(error => {
|
||||
console.error('Erreur reCAPTCHA:', error);
|
||||
resolve(''); // Permettre l'envoi quand même
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialiser au chargement
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
RecaptchaService.init();
|
||||
});
|
||||
```
|
||||
|
||||
### Intégration dans l'Envoi du Formulaire
|
||||
|
||||
```javascript
|
||||
// Dans contact-form.js
|
||||
|
||||
async function submitForm(formData) {
|
||||
// Obtenir le token reCAPTCHA
|
||||
const recaptchaToken = await RecaptchaService.getToken('contact');
|
||||
|
||||
// Ajouter aux données
|
||||
const payload = {
|
||||
...formData,
|
||||
recaptcha_token: recaptchaToken
|
||||
};
|
||||
|
||||
// Envoyer au backend
|
||||
const response = await fetch('/api/contact.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Vérification Côté Serveur (api/contact.php)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Vérifie le token reCAPTCHA v3 auprès de Google
|
||||
* @param string $token Token reçu du client
|
||||
* @return float Score (0.0 à 1.0), 0.0 si échec
|
||||
*/
|
||||
function verifyRecaptcha(string $token): float
|
||||
{
|
||||
// Si pas de token, retourner un score bas mais pas bloquant
|
||||
if (empty($token)) {
|
||||
error_log('reCAPTCHA: token vide');
|
||||
return 0.3; // Score bas mais pas 0
|
||||
}
|
||||
|
||||
$response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded',
|
||||
'content' => http_build_query([
|
||||
'secret' => RECAPTCHA_SECRET_KEY,
|
||||
'response' => $token,
|
||||
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? ''
|
||||
])
|
||||
]
|
||||
]));
|
||||
|
||||
if ($response === false) {
|
||||
error_log('reCAPTCHA: impossible de contacter Google');
|
||||
return 0.3; // Dégradation gracieuse
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
error_log('reCAPTCHA: échec - ' . json_encode($result['error-codes'] ?? []));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) ($result['score'] ?? 0.0);
|
||||
}
|
||||
```
|
||||
|
||||
### Seuil de Score
|
||||
|
||||
| Score | Interprétation | Action |
|
||||
|-------|----------------|--------|
|
||||
| 0.9 - 1.0 | Très probablement humain | Accepter |
|
||||
| 0.5 - 0.9 | Probablement humain | Accepter |
|
||||
| 0.3 - 0.5 | Douteux | Accepter avec vigilance |
|
||||
| 0.0 - 0.3 | Probablement bot | Rejeter |
|
||||
|
||||
Dans notre implémentation : seuil à 0.5
|
||||
|
||||
### Dégradation Gracieuse
|
||||
|
||||
Si reCAPTCHA échoue :
|
||||
1. Le formulaire reste fonctionnel
|
||||
2. Un avertissement est loggé
|
||||
3. Le backend peut décider d'accepter ou non
|
||||
4. Pas de message d'erreur visible pour l'utilisateur
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Le script reCAPTCHA se charge (vérifier Network)
|
||||
- [] Un token est généré à la soumission (RecaptchaService.getToken)
|
||||
- [] Le token est prêt à être envoyé au backend
|
||||
- [ ] Le backend vérifie le token avec Google (Story 5.5)
|
||||
- [] Si reCAPTCHA indisponible, le formulaire fonctionne quand même
|
||||
- [] Pas d'erreur visible si reCAPTCHA échoue (console.warn seulement)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `includes/config.php` | Created | Chargement .env et définition des constantes |
|
||||
| `.env` | Created | Variables d'environnement (clés test Google) |
|
||||
| `index.php` | Modified | Ajout require config.php |
|
||||
| `templates/footer.php` | Modified | Script reCAPTCHA + window.RECAPTCHA_SITE_KEY |
|
||||
| `assets/js/contact-form.js` | Modified | Ajout RecaptchaService |
|
||||
|
||||
### Completion Notes
|
||||
- Système de chargement .env avec loadEnv() dans config.php
|
||||
- Constantes PHP : RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY, APP_ENV, etc.
|
||||
- Script Google chargé en async/defer dans footer.php
|
||||
- RecaptchaService avec méthodes init(), isAvailable(), getToken()
|
||||
- Dégradation gracieuse : retourne '' si reCAPTCHA indisponible
|
||||
- Clés de test Google utilisées en développement (score toujours 0.9)
|
||||
- La vérification côté serveur sera implémentée dans Story 5.5
|
||||
|
||||
### Debug Log References
|
||||
Aucun problème rencontré.
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |
|
||||
Reference in New Issue
Block a user