feat(api): implement recaptcha v3 anti-spam protection
- Add RecaptchaValidatorInterface and RecaptchaResult entity in Domain - Create RecaptchaValidationService in Application layer - Implement GoogleRecaptchaValidator for API integration - Add recaptcha-settings schema and admin FormBuilder - Integrate reCAPTCHA validation in NewsletterAjaxHandler - Integrate reCAPTCHA validation in ContactFormAjaxHandler - Update FooterRenderer and ContactFormRenderer with reCAPTCHA scripts - Configure DIContainer with RecaptchaValidationService injection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
42
Shared/Domain/Contracts/RecaptchaValidatorInterface.php
Normal file
42
Shared/Domain/Contracts/RecaptchaValidatorInterface.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\RecaptchaResult;
|
||||
|
||||
/**
|
||||
* RecaptchaValidatorInterface - Contrato para validacion de reCAPTCHA
|
||||
*
|
||||
* RESPONSABILIDAD: Definir el contrato para validar tokens de reCAPTCHA v3
|
||||
* con la API de Google.
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - Interface Segregation: Una sola responsabilidad - validar tokens
|
||||
* - Dependency Inversion: Depender de abstraccion, no de implementacion
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* final class GoogleRecaptchaValidator implements RecaptchaValidatorInterface
|
||||
* {
|
||||
* public function validate(string $token, string $action, string $secretKey): RecaptchaResult
|
||||
* {
|
||||
* // Llamar a API de Google y retornar resultado
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface RecaptchaValidatorInterface
|
||||
{
|
||||
/**
|
||||
* Validar un token de reCAPTCHA v3 con la API de Google
|
||||
*
|
||||
* @param string $token Token generado por reCAPTCHA en frontend
|
||||
* @param string $action Nombre de la accion (ej: 'newsletter_submit', 'contact_submit')
|
||||
* @param string $secretKey Clave secreta de reCAPTCHA
|
||||
* @return RecaptchaResult Resultado de la validacion con score y estado
|
||||
*/
|
||||
public function validate(string $token, string $action, string $secretKey): RecaptchaResult;
|
||||
}
|
||||
134
Shared/Domain/Entities/RecaptchaResult.php
Normal file
134
Shared/Domain/Entities/RecaptchaResult.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Entities;
|
||||
|
||||
/**
|
||||
* RecaptchaResult - Entidad que representa el resultado de validacion reCAPTCHA
|
||||
*
|
||||
* RESPONSABILIDAD: Encapsular el resultado de la validacion de reCAPTCHA v3
|
||||
* incluyendo el score, estado de exito y posibles codigos de error.
|
||||
*
|
||||
* INMUTABILIDAD: Esta entidad es inmutable - una vez creada no puede modificarse.
|
||||
*
|
||||
* SCORE reCAPTCHA v3:
|
||||
* - 1.0: Muy probablemente humano
|
||||
* - 0.9: Probablemente humano
|
||||
* - 0.5: Indeterminado
|
||||
* - 0.1: Probablemente bot
|
||||
* - 0.0: Muy probablemente bot
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Entities
|
||||
*/
|
||||
final class RecaptchaResult
|
||||
{
|
||||
/**
|
||||
* @param bool $success Si la validacion fue exitosa (token valido)
|
||||
* @param float $score Score de 0.0 (bot) a 1.0 (humano)
|
||||
* @param string $action Accion verificada
|
||||
* @param array<string> $errorCodes Codigos de error de la API
|
||||
* @param string $hostname Hostname donde se genero el token
|
||||
* @param string $challengeTs Timestamp del challenge
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $success,
|
||||
private float $score,
|
||||
private string $action,
|
||||
private array $errorCodes = [],
|
||||
private string $hostname = '',
|
||||
private string $challengeTs = ''
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verificar si el resultado es valido segun un threshold
|
||||
*
|
||||
* @param float $threshold Score minimo requerido (ej: 0.5)
|
||||
* @return bool True si success=true Y score >= threshold
|
||||
*/
|
||||
public function isValid(float $threshold): bool
|
||||
{
|
||||
return $this->success && $this->score >= $threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si la accion coincide con la esperada
|
||||
*
|
||||
* @param string $expectedAction Accion esperada
|
||||
* @return bool True si la accion coincide
|
||||
*/
|
||||
public function hasValidAction(string $expectedAction): bool
|
||||
{
|
||||
return $this->action === $expectedAction;
|
||||
}
|
||||
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function getScore(): float
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getErrorCodes(): array
|
||||
{
|
||||
return $this->errorCodes;
|
||||
}
|
||||
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return !empty($this->errorCodes);
|
||||
}
|
||||
|
||||
public function getHostname(): string
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
public function getChallengeTs(): string
|
||||
{
|
||||
return $this->challengeTs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear resultado de fallo (para errores de red, timeout, etc.)
|
||||
*
|
||||
* @param array<string> $errorCodes Codigos de error
|
||||
* @return self
|
||||
*/
|
||||
public static function failure(array $errorCodes = ['unknown-error']): self
|
||||
{
|
||||
return new self(
|
||||
success: false,
|
||||
score: 0.0,
|
||||
action: '',
|
||||
errorCodes: $errorCodes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertir a array para logging
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
'score' => $this->score,
|
||||
'action' => $this->action,
|
||||
'error_codes' => $this->errorCodes,
|
||||
'hostname' => $this->hostname,
|
||||
'challenge_ts' => $this->challengeTs,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user