Files
fotospiel-app/app/Http/Controllers/RevenueCatWebhookController.php
Codex Agent fc3e6715db
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Add integrations health monitoring
2026-01-02 18:35:12 +01:00

107 lines
3.4 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessRevenueCatWebhook;
use App\Services\Integrations\IntegrationWebhookRecorder;
use App\Support\ApiError;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class RevenueCatWebhookController extends Controller
{
public function __construct(private readonly IntegrationWebhookRecorder $recorder) {}
public function handle(Request $request): JsonResponse
{
$secret = (string) config('services.revenuecat.webhook', '');
if ($secret === '') {
Log::error('RevenueCat webhook secret not configured');
return ApiError::response(
'webhook_not_configured',
'Webhook Not Configured',
'RevenueCat webhook secret is missing.',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
$signature = trim((string) $request->header('X-Signature', ''));
if ($signature === '') {
return ApiError::response(
'signature_missing',
'Signature Missing',
'The RevenueCat webhook request did not include a signature.',
Response::HTTP_BAD_REQUEST
);
}
$payload = $request->getContent();
if (! $this->signatureMatches($payload, $signature, $secret)) {
return ApiError::response(
'signature_invalid',
'Invalid Signature',
'The webhook signature could not be validated.',
Response::HTTP_BAD_REQUEST
);
}
$decoded = json_decode($payload, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) {
Log::warning('RevenueCat webhook received invalid JSON', [
'error' => json_last_error_msg(),
]);
return ApiError::response(
'payload_invalid',
'Invalid Payload',
'The webhook payload could not be decoded as JSON.',
Response::HTTP_BAD_REQUEST,
['json_error' => json_last_error_msg()]
);
}
$eventId = (string) $request->header('X-Event-Id', '');
$eventType = data_get($decoded, 'event.type');
$webhookEvent = $this->recorder->recordReceived(
'revenuecat',
$eventId !== '' ? $eventId : null,
is_string($eventType) && $eventType !== '' ? $eventType : null,
);
ProcessRevenueCatWebhook::dispatch(
$decoded,
$eventId,
$webhookEvent->id,
);
return response()->json(['status' => 'accepted'], 202);
}
private function signatureMatches(string $payload, string $providedSignature, string $secret): bool
{
$normalized = preg_replace('/\s+/', '', $providedSignature);
if (! is_string($normalized)) {
return false;
}
$candidates = [
base64_encode(hash_hmac('sha1', $payload, $secret, true)),
base64_encode(hash_hmac('sha256', $payload, $secret, true)),
hash_hmac('sha1', $payload, $secret),
hash_hmac('sha256', $payload, $secret),
];
foreach ($candidates as $expected) {
if (hash_equals($expected, $normalized)) {
return true;
}
}
return false;
}
}