diff --git a/.env b/.env index 3ae5a15..847f8cd 100644 --- a/.env +++ b/.env @@ -102,6 +102,10 @@ MAILCOW_URL=https://mail.esy-web.dev MAILCOW_API_KEY= ###< mailcow ### +###> webhooks ### +WEBHOOK_BASE_URL= +###< webhooks ### + ###> docuseal ### DOCUSEAL_URL=https://signature.esy-web.dev DOCUSEAL_API= diff --git a/ansible/env.local.j2 b/ansible/env.local.j2 index c3d628b..e02f1c7 100644 --- a/ansible/env.local.j2 +++ b/ansible/env.local.j2 @@ -33,6 +33,7 @@ AWS_REGION=eu-west-3 CLOUDFLARE_KEY={{ cloudflare_key }} MAILCOW_URL=https://mail.esy-web.dev MAILCOW_API_KEY={{ mailcow_api_key }} +WEBHOOK_BASE_URL=https://stripe.siteconseil.fr DOCUSEAL_URL=https://signature.esy-web.dev DOCUSEAL_API={{ docuseal_api }} DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign diff --git a/docker/ngrok/sync.sh b/docker/ngrok/sync.sh index 0378a40..d4f7dc3 100755 --- a/docker/ngrok/sync.sh +++ b/docker/ngrok/sync.sh @@ -23,7 +23,9 @@ fi touch /app/.env.local sed -i '/^OUTSIDE_URL=/d' /app/.env.local +sed -i '/^WEBHOOK_BASE_URL=/d' /app/.env.local echo "OUTSIDE_URL=$NGROK_URL" >> /app/.env.local +echo "WEBHOOK_BASE_URL=$NGROK_URL" >> /app/.env.local echo "Ngrok URL: $NGROK_URL" -echo "Written to .env.local" +echo "Written OUTSIDE_URL and WEBHOOK_BASE_URL to .env.local" diff --git a/migrations/Version20260402205935.php b/migrations/Version20260402205935.php new file mode 100644 index 0000000..bee0efa --- /dev/null +++ b/migrations/Version20260402205935.php @@ -0,0 +1,32 @@ +addSql('CREATE TABLE stripe_webhook_secret (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, type VARCHAR(30) NOT NULL, secret VARCHAR(255) NOT NULL, endpoint_id VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_D9BDCBA48CDE5729 ON stripe_webhook_secret (type)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE stripe_webhook_secret'); + } +} diff --git a/src/Controller/Admin/SyncController.php b/src/Controller/Admin/SyncController.php index db5698e..3066fc8 100644 --- a/src/Controller/Admin/SyncController.php +++ b/src/Controller/Admin/SyncController.php @@ -2,13 +2,17 @@ namespace App\Controller\Admin; +use App\Entity\StripeWebhookSecret; use App\Repository\CustomerRepository; use App\Repository\PriceAutomaticRepository; use App\Repository\RevendeurRepository; +use App\Repository\StripeWebhookSecretRepository; +use Doctrine\ORM\EntityManagerInterface; use App\Service\MeilisearchService; use App\Service\StripePriceService; use App\Service\StripeWebhookService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -80,17 +84,26 @@ class SyncController extends AbstractController } #[Route('/stripe/webhooks', name: 'stripe_webhooks', methods: ['POST'])] - public function syncStripeWebhooks(Request $request, StripeWebhookService $webhookService): Response - { - $baseUrl = trim($request->request->getString('base_url')); - - if ('' === $baseUrl) { - $this->addFlash('error', 'URL de base requise.'); + public function syncStripeWebhooks( + StripeWebhookService $webhookService, + StripeWebhookSecretRepository $secretRepository, + EntityManagerInterface $em, + #[Autowire(env: 'WEBHOOK_BASE_URL')] string $webhookBaseUrl, + ): Response { + if ('' === $webhookBaseUrl) { + $this->addFlash('error', 'WEBHOOK_BASE_URL non configuree dans .env.local'); return $this->redirectToRoute('app_admin_sync_index'); } - $result = $webhookService->createAllWebhooks(rtrim($baseUrl, '/')); + $result = $webhookService->createAllWebhooks(rtrim($webhookBaseUrl, '/')); + + $typeMap = [ + 'Main Light' => StripeWebhookSecret::TYPE_MAIN_LIGHT, + 'Main Instant' => StripeWebhookSecret::TYPE_MAIN_INSTANT, + 'Connect Light' => StripeWebhookSecret::TYPE_CONNECT_LIGHT, + 'Connect Instant' => StripeWebhookSecret::TYPE_CONNECT_INSTANT, + ]; foreach ($result['created'] as $wh) { $status = $wh['status'] ?? 'created'; @@ -98,9 +111,24 @@ class SyncController extends AbstractController $this->addFlash('success', $wh['type'].' : deja configure ('.$wh['id'].')'); } else { $this->addFlash('success', $wh['type'].' : cree ('.$wh['id'].')'); + + if (isset($wh['secret'], $typeMap[$wh['type']])) { + $dbType = $typeMap[$wh['type']]; + $existing = $secretRepository->findByType($dbType); + + if (null !== $existing) { + $existing->setSecret($wh['secret']); + $existing->setEndpointId($wh['id']); + } else { + $entity = new StripeWebhookSecret($dbType, $wh['secret'], $wh['id']); + $em->persist($entity); + } + } } } + $em->flush(); + foreach ($result['errors'] as $error) { $this->addFlash('error', 'Stripe Webhook : '.$error); } @@ -153,4 +181,5 @@ class SyncController extends AbstractController return $this->redirectToRoute('app_admin_sync_index'); } + } diff --git a/src/Controller/WebhookStripeController.php b/src/Controller/WebhookStripeController.php index 57bfad9..828a1fd 100644 --- a/src/Controller/WebhookStripeController.php +++ b/src/Controller/WebhookStripeController.php @@ -2,9 +2,10 @@ namespace App\Controller; +use App\Entity\StripeWebhookSecret; +use App\Repository\StripeWebhookSecretRepository; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -14,43 +15,44 @@ class WebhookStripeController extends AbstractController { public function __construct( private LoggerInterface $logger, + private StripeWebhookSecretRepository $secretRepository, ) { } #[Route('/webhooks/stripe/main/light', name: 'app_webhook_stripe_main_light', methods: ['POST'])] - public function mainLight( - Request $request, - #[Autowire(env: 'STRIPE_WEBHOOK_SECRET')] string $secret, - ): Response { - return $this->handleWebhook($request, $secret, 'main_light'); + public function mainLight(Request $request): Response + { + return $this->handleWebhook($request, StripeWebhookSecret::TYPE_MAIN_LIGHT); } #[Route('/webhooks/stripe/main/instant', name: 'app_webhook_stripe_main_instant', methods: ['POST'])] - public function mainInstant( - Request $request, - #[Autowire(env: 'STRIPE_WEBHOOK_SECRET')] string $secret, - ): Response { - return $this->handleWebhook($request, $secret, 'main_instant'); + public function mainInstant(Request $request): Response + { + return $this->handleWebhook($request, StripeWebhookSecret::TYPE_MAIN_INSTANT); } #[Route('/webhooks/stripe/connect/light', name: 'app_webhook_stripe_connect_light', methods: ['POST'])] - public function connectLight( - Request $request, - #[Autowire(env: 'STRIPE_WEBHOOK_SECRET_CONNECT')] string $secret, - ): Response { - return $this->handleWebhook($request, $secret, 'connect_light'); + public function connectLight(Request $request): Response + { + return $this->handleWebhook($request, StripeWebhookSecret::TYPE_CONNECT_LIGHT); } #[Route('/webhooks/stripe/connect/instant', name: 'app_webhook_stripe_connect_instant', methods: ['POST'])] - public function connectInstant( - Request $request, - #[Autowire(env: 'STRIPE_WEBHOOK_SECRET_CONNECT')] string $secret, - ): Response { - return $this->handleWebhook($request, $secret, 'connect_instant'); + public function connectInstant(Request $request): Response + { + return $this->handleWebhook($request, StripeWebhookSecret::TYPE_CONNECT_INSTANT); } - private function handleWebhook(Request $request, string $secret, string $channel): Response + private function handleWebhook(Request $request, string $channel): Response { + $secret = $this->secretRepository->getSecret($channel); + + if (null === $secret) { + $this->logger->warning('Stripe webhook ['.$channel.']: secret non configure'); + + return new JsonResponse(['error' => 'Webhook not configured'], Response::HTTP_SERVICE_UNAVAILABLE); + } + $payload = $request->getContent(); $sigHeader = $request->headers->get('Stripe-Signature', ''); @@ -69,7 +71,6 @@ class WebhookStripeController extends AbstractController ]); // TODO: dispatcher les evenements vers les handlers specifiques - // Ex: match ($event->type) { 'invoice.paid' => ..., } return new JsonResponse(['status' => 'ok', 'channel' => $channel, 'event' => $event->type]); } diff --git a/src/Entity/StripeWebhookSecret.php b/src/Entity/StripeWebhookSecret.php new file mode 100644 index 0000000..3cbf2f9 --- /dev/null +++ b/src/Entity/StripeWebhookSecret.php @@ -0,0 +1,75 @@ +type = $type; + $this->secret = $secret; + $this->endpointId = $endpointId; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getType(): string + { + return $this->type; + } + + public function getSecret(): string + { + return $this->secret; + } + + public function setSecret(string $secret): void + { + $this->secret = $secret; + } + + public function getEndpointId(): ?string + { + return $this->endpointId; + } + + public function setEndpointId(?string $endpointId): void + { + $this->endpointId = $endpointId; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } +} diff --git a/src/Repository/StripeWebhookSecretRepository.php b/src/Repository/StripeWebhookSecretRepository.php new file mode 100644 index 0000000..bb9fe20 --- /dev/null +++ b/src/Repository/StripeWebhookSecretRepository.php @@ -0,0 +1,30 @@ + + */ +class StripeWebhookSecretRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, StripeWebhookSecret::class); + } + + public function findByType(string $type): ?StripeWebhookSecret + { + return $this->findOneBy(['type' => $type]); + } + + public function getSecret(string $type): ?string + { + $entity = $this->findByType($type); + + return $entity?->getSecret(); + } +} diff --git a/templates/admin/sync/index.html.twig b/templates/admin/sync/index.html.twig index bdd378a..f7bcfd3 100644 --- a/templates/admin/sync/index.html.twig +++ b/templates/admin/sync/index.html.twig @@ -131,11 +131,7 @@
Cree les 4 endpoints : main light/instant + connect light/instant
-