feat: ajout signature HMAC SHA-256 sur Devis, Advert et Facture

src/Entity/Devis.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "devis|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals (timing-safe)

src/Entity/Advert.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "advert|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals

src/Entity/Facture.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "facture|numOrder|splitIndex|createdAt"
  signe avec APP_SECRET (inclut splitIndex pour differencier les splits)
- verifyHmac(): verification par hash_equals

src/Service/DevisService.php, AdvertService.php, FactureService.php:
- Injection de APP_SECRET via #[Autowire('%env(APP_SECRET)%')]
- Passage du hmacSecret aux constructeurs des entites

migrations/Version20260402203207.php:
- Ajout colonne hmac VARCHAR(128) NOT NULL sur devis, advert, facture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-02 22:32:18 +02:00
parent 5b0e4707f7
commit cdd5c656a9
7 changed files with 124 additions and 23 deletions

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260402203207 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE advert ADD hmac VARCHAR(128) NOT NULL');
$this->addSql('ALTER TABLE devis ADD hmac VARCHAR(128) NOT NULL');
$this->addSql('ALTER TABLE facture ADD hmac VARCHAR(128) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE advert DROP hmac');
$this->addSql('ALTER TABLE devis DROP hmac');
$this->addSql('ALTER TABLE facture DROP hmac');
}
}

View File

@@ -23,6 +23,9 @@ class Advert
#[ORM\JoinColumn(nullable: true)] #[ORM\JoinColumn(nullable: true)]
private ?Devis $devis = null; private ?Devis $devis = null;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column] #[ORM\Column]
private \DateTimeImmutable $createdAt; private \DateTimeImmutable $createdAt;
@@ -30,11 +33,12 @@ class Advert
#[ORM\OneToMany(targetEntity: Facture::class, mappedBy: 'advert')] #[ORM\OneToMany(targetEntity: Facture::class, mappedBy: 'advert')]
private Collection $factures; private Collection $factures;
public function __construct(OrderNumber $orderNumber) public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{ {
$this->orderNumber = $orderNumber; $this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new \DateTimeImmutable();
$this->factures = new ArrayCollection(); $this->factures = new ArrayCollection();
$this->hmac = $this->generateHmac($hmacSecret);
} }
public function getId(): ?int public function getId(): ?int
@@ -57,6 +61,11 @@ class Advert
$this->devis = $devis; $this->devis = $devis;
} }
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): \DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
@@ -67,4 +76,20 @@ class Advert
{ {
return $this->factures; return $this->factures;
} }
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'advert',
$this->orderNumber->getNumOrder(),
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
} }

View File

@@ -19,6 +19,9 @@ class Devis
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private OrderNumber $orderNumber; private OrderNumber $orderNumber;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column] #[ORM\Column]
private \DateTimeImmutable $createdAt; private \DateTimeImmutable $createdAt;
@@ -26,11 +29,12 @@ class Devis
#[ORM\OneToMany(targetEntity: Advert::class, mappedBy: 'devis')] #[ORM\OneToMany(targetEntity: Advert::class, mappedBy: 'devis')]
private Collection $adverts; private Collection $adverts;
public function __construct(OrderNumber $orderNumber) public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{ {
$this->orderNumber = $orderNumber; $this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new \DateTimeImmutable();
$this->adverts = new ArrayCollection(); $this->adverts = new ArrayCollection();
$this->hmac = $this->generateHmac($hmacSecret);
} }
public function getId(): ?int public function getId(): ?int
@@ -43,6 +47,11 @@ class Devis
return $this->orderNumber; return $this->orderNumber;
} }
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): \DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
@@ -53,4 +62,20 @@ class Devis
{ {
return $this->adverts; return $this->adverts;
} }
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'devis',
$this->orderNumber->getNumOrder(),
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
} }

View File

@@ -27,13 +27,17 @@ class Facture
#[ORM\Column(type: 'smallint', options: ['default' => 0])] #[ORM\Column(type: 'smallint', options: ['default' => 0])]
private int $splitIndex = 0; private int $splitIndex = 0;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column] #[ORM\Column]
private \DateTimeImmutable $createdAt; private \DateTimeImmutable $createdAt;
public function __construct(OrderNumber $orderNumber) public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{ {
$this->orderNumber = $orderNumber; $this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new \DateTimeImmutable();
$this->hmac = $this->generateHmac($hmacSecret);
} }
public function getId(): ?int public function getId(): ?int
@@ -77,8 +81,30 @@ class Facture
return $this->splitIndex > 0 ? $num.'-'.$this->splitIndex : $num; return $this->splitIndex > 0 ? $num.'-'.$this->splitIndex : $num;
} }
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): \DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'facture',
$this->orderNumber->getNumOrder(),
(string) $this->splitIndex,
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
} }

View File

@@ -5,25 +5,24 @@ namespace App\Service;
use App\Entity\Advert; use App\Entity\Advert;
use App\Entity\Devis; use App\Entity\Devis;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class AdvertService class AdvertService
{ {
public function __construct( public function __construct(
private OrderNumberService $orderNumberService, private OrderNumberService $orderNumberService,
private EntityManagerInterface $em, private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) { ) {
} }
/**
* Cree un advert avec un nouveau numero de commande.
*/
public function create(?Devis $devis = null): Advert public function create(?Devis $devis = null): Advert
{ {
$orderNumber = null !== $devis $orderNumber = null !== $devis
? $devis->getOrderNumber() ? $devis->getOrderNumber()
: $this->orderNumberService->generateAndUse(); : $this->orderNumberService->generateAndUse();
$advert = new Advert($orderNumber); $advert = new Advert($orderNumber, $this->hmacSecret);
if (null !== $devis) { if (null !== $devis) {
$advert->setDevis($devis); $advert->setDevis($devis);
@@ -35,9 +34,6 @@ class AdvertService
return $advert; return $advert;
} }
/**
* Cree un advert a partir d'un devis existant (meme numero).
*/
public function createFromDevis(Devis $devis): Advert public function createFromDevis(Devis $devis): Advert
{ {
return $this->create($devis); return $this->create($devis);

View File

@@ -4,22 +4,21 @@ namespace App\Service;
use App\Entity\Devis; use App\Entity\Devis;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class DevisService class DevisService
{ {
public function __construct( public function __construct(
private OrderNumberService $orderNumberService, private OrderNumberService $orderNumberService,
private EntityManagerInterface $em, private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) { ) {
} }
/**
* Cree un devis avec un nouveau numero de commande.
*/
public function create(): Devis public function create(): Devis
{ {
$orderNumber = $this->orderNumberService->generateAndUse(); $orderNumber = $this->orderNumberService->generateAndUse();
$devis = new Devis($orderNumber); $devis = new Devis($orderNumber, $this->hmacSecret);
$this->em->persist($devis); $this->em->persist($devis);
$this->em->flush(); $this->em->flush();

View File

@@ -5,18 +5,17 @@ namespace App\Service;
use App\Entity\Advert; use App\Entity\Advert;
use App\Entity\Facture; use App\Entity\Facture;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class FactureService class FactureService
{ {
public function __construct( public function __construct(
private OrderNumberService $orderNumberService, private OrderNumberService $orderNumberService,
private EntityManagerInterface $em, private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) { ) {
} }
/**
* Cree une facture avec un nouveau numero de commande (facture seule).
*/
public function create(?Advert $advert = null): Facture public function create(?Advert $advert = null): Facture
{ {
if (null !== $advert) { if (null !== $advert) {
@@ -24,7 +23,7 @@ class FactureService
} }
$orderNumber = $this->orderNumberService->generateAndUse(); $orderNumber = $this->orderNumberService->generateAndUse();
$facture = new Facture($orderNumber); $facture = new Facture($orderNumber, $this->hmacSecret);
$this->em->persist($facture); $this->em->persist($facture);
$this->em->flush(); $this->em->flush();
@@ -32,20 +31,16 @@ class FactureService
return $facture; return $facture;
} }
/**
* Cree une facture a partir d'un advert (meme numero + splitIndex si plusieurs).
*/
public function createFromAdvert(Advert $advert): Facture public function createFromAdvert(Advert $advert): Facture
{ {
$existingCount = $advert->getFactures()->count(); $existingCount = $advert->getFactures()->count();
$facture = new Facture($advert->getOrderNumber()); $facture = new Facture($advert->getOrderNumber(), $this->hmacSecret);
$facture->setAdvert($advert); $facture->setAdvert($advert);
if ($existingCount > 0) { if ($existingCount > 0) {
$facture->setSplitIndex($existingCount + 1); $facture->setSplitIndex($existingCount + 1);
// La premiere facture existante doit aussi avoir un splitIndex si elle n'en a pas
$firstFacture = $advert->getFactures()->first(); $firstFacture = $advert->getFactures()->first();
if (false !== $firstFacture && 0 === $firstFacture->getSplitIndex()) { if (false !== $firstFacture && 0 === $firstFacture->getSplitIndex()) {
$firstFacture->setSplitIndex(1); $firstFacture->setSplitIndex(1);