From b0dead81209879bb467cd8a881b98ebff7f0ce3d Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Sat, 21 Mar 2026 16:43:59 +0100 Subject: [PATCH] Add security key to BilletOrder, QR code helper text - securityKey: HMAC-SHA256(reference, APP_SECRET) truncated to 16 hex chars - Generated automatically at ticket creation via BilletOrderService - Deterministic: same reference + secret = same key, verifiable server-side - Cannot be forged without knowing APP_SECRET - PDF: "Presentez ce QR code pour valider votre ticket" under QR code - PDF: "Cle de securite" displayed with letter-spacing - Tests: generateSecurityKey determinism, uniqueness, format Co-Authored-By: Claude Opus 4.6 (1M context) --- migrations/Version20260321230000.php | 26 ++++++++++++++++++++++++++ src/Entity/BilletOrder.php | 20 ++++++++++++++++++++ src/Service/BilletOrderService.php | 2 ++ templates/pdf/billet.html.twig | 4 ++++ tests/Entity/BilletOrderTest.php | 24 ++++++++++++++++++++++++ 5 files changed, 76 insertions(+) create mode 100644 migrations/Version20260321230000.php diff --git a/migrations/Version20260321230000.php b/migrations/Version20260321230000.php new file mode 100644 index 0000000..1ee917b --- /dev/null +++ b/migrations/Version20260321230000.php @@ -0,0 +1,26 @@ +addSql("ALTER TABLE billet_order ADD COLUMN IF NOT EXISTS security_key VARCHAR(16) DEFAULT '' NOT NULL"); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE billet_order DROP COLUMN IF EXISTS security_key'); + } +} diff --git a/src/Entity/BilletOrder.php b/src/Entity/BilletOrder.php index f28151a..ca27ef7 100644 --- a/src/Entity/BilletOrder.php +++ b/src/Entity/BilletOrder.php @@ -27,6 +27,9 @@ class BilletOrder #[ORM\Column(length: 255)] private ?string $billetName = null; + #[ORM\Column(length: 16)] + private string $securityKey = ''; + #[ORM\Column] private int $unitPriceHT = 0; @@ -98,6 +101,23 @@ class BilletOrder return $this; } + public function getSecurityKey(): string + { + return $this->securityKey; + } + + public function setSecurityKey(string $securityKey): static + { + $this->securityKey = $securityKey; + + return $this; + } + + public static function generateSecurityKey(string $reference, string $appSecret): string + { + return strtoupper(substr(hash_hmac('sha256', $reference, $appSecret), 0, 16)); + } + public function getUnitPriceHT(): int { return $this->unitPriceHT; diff --git a/src/Service/BilletOrderService.php b/src/Service/BilletOrderService.php index 3876d70..c367c44 100644 --- a/src/Service/BilletOrderService.php +++ b/src/Service/BilletOrderService.php @@ -25,6 +25,7 @@ class BilletOrderService private MailerService $mailer, private UrlGeneratorInterface $urlGenerator, #[Autowire('%kernel.project_dir%')] private string $projectDir, + #[Autowire('%kernel.secret%')] private string $appSecret, ) { } @@ -37,6 +38,7 @@ class BilletOrderService $ticket->setBillet($item->getBillet()); $ticket->setBilletName($item->getBilletName()); $ticket->setUnitPriceHT($item->getUnitPriceHT()); + $ticket->setSecurityKey(BilletOrder::generateSecurityKey($ticket->getReference(), $this->appSecret)); $this->em->persist($ticket); } diff --git a/templates/pdf/billet.html.twig b/templates/pdf/billet.html.twig index 845c0d5..948a70f 100644 --- a/templates/pdf/billet.html.twig +++ b/templates/pdf/billet.html.twig @@ -306,6 +306,7 @@ QR +
Presentez ce QR code
pour valider votre ticket
Reference billet
@@ -313,6 +314,9 @@
Commande
{{ order.orderNumber }}
+
+
Cle de securite
+
{{ ticket.securityKey }}
diff --git a/tests/Entity/BilletOrderTest.php b/tests/Entity/BilletOrderTest.php index 3b006c8..e54343a 100644 --- a/tests/Entity/BilletOrderTest.php +++ b/tests/Entity/BilletOrderTest.php @@ -21,6 +21,7 @@ class BilletOrderTest extends TestCase self::assertSame(0.0, $ticket->getUnitPriceHTDecimal()); self::assertSame(BilletOrder::STATE_VALID, $ticket->getState()); self::assertTrue($ticket->isValid()); + self::assertSame('', $ticket->getSecurityKey()); self::assertNull($ticket->isInvitation()); self::assertNull($ticket->getFirstScannedAt()); self::assertMatchesRegularExpression('/^ETICKET-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $ticket->getReference()); @@ -83,6 +84,29 @@ class BilletOrderTest extends TestCase self::assertTrue($ticket->isValid()); } + public function testSetAndGetSecurityKey(): void + { + $ticket = new BilletOrder(); + $result = $ticket->setSecurityKey('ABC123'); + + self::assertSame('ABC123', $ticket->getSecurityKey()); + self::assertSame($ticket, $result); + } + + public function testGenerateSecurityKey(): void + { + $key = BilletOrder::generateSecurityKey('ETICKET-ABCD-1234-EFGH', 'my-secret'); + + self::assertSame(16, \strlen($key)); + self::assertMatchesRegularExpression('/^[A-F0-9]{16}$/', $key); + + $key2 = BilletOrder::generateSecurityKey('ETICKET-ABCD-1234-EFGH', 'my-secret'); + self::assertSame($key, $key2); + + $key3 = BilletOrder::generateSecurityKey('ETICKET-XXXX-YYYY-ZZZZ', 'my-secret'); + self::assertNotSame($key, $key3); + } + public function testSetAndGetIsInvitation(): void { $ticket = new BilletOrder();