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) <noreply@anthropic.com>
This commit is contained in:
26
migrations/Version20260321230000.php
Normal file
26
migrations/Version20260321230000.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260321230000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add security_key to billet_order';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
<tr>
|
||||
<td style="width: 140px;">
|
||||
<img src="{{ qrBase64 }}" alt="QR" class="qr-img">
|
||||
<div style="font-size: 6px; font-weight: bold; text-align: center; color: #999; margin-top: 4px; text-transform: uppercase;">Presentez ce QR code<br>pour valider votre ticket</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<div class="ref-lbl">Reference billet</div>
|
||||
@@ -313,6 +314,9 @@
|
||||
<br>
|
||||
<div class="ref-lbl">Commande</div>
|
||||
<div class="ref-val">{{ order.orderNumber }}</div>
|
||||
<br>
|
||||
<div class="ref-lbl">Cle de securite</div>
|
||||
<div class="ref-val" style="letter-spacing: 2px;">{{ ticket.securityKey }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user