Files
myep/src/Service/AccommodationBookingLinkSigner.php
T

108 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Groups\AccommodationBooking;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AccommodationBookingLinkSigner
{
private const int LINK_TTL_DAYS = 90;
private const string TIMESTAMP_PARAM = 't';
private readonly UriSigner $uriSigner;
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
#[Autowire(env: 'ACCOMMODATION_OFFER_LINK_SECRET')]
string $secret,
) {
$this->uriSigner = new UriSigner($secret);
}
public function sign(AccommodationBooking $booking): string
{
$issuedAt = $booking->getAccessLinkIssuedAt();
if (null === $issuedAt) {
throw new \LogicException('Cannot sign an access link before accessLinkIssuedAt is set.');
}
$url = $this->urlGenerator->generate(
'app_groups_offer',
['uuid' => $booking->getUuid(), self::TIMESTAMP_PARAM => $issuedAt->getTimestamp()],
UrlGeneratorInterface::ABSOLUTE_URL,
);
return $this->uriSigner->sign($url);
}
public function expiresAt(AccommodationBooking $booking): ?\DateTimeImmutable
{
return $booking->getAccessLinkIssuedAt()?->modify(sprintf('+%d days', self::LINK_TTL_DAYS));
}
public function isValidLinkRequest(Request $request, AccommodationBooking $booking): bool
{
$issuedAt = $booking->getAccessLinkIssuedAt();
if (null === $issuedAt) {
return false;
}
if (!$this->uriSigner->checkRequest($request)) {
return false;
}
$timestampParam = $request->query->get(self::TIMESTAMP_PARAM);
if (null === $timestampParam || (int) $timestampParam !== $issuedAt->getTimestamp()) {
return false;
}
$expiresAt = $this->expiresAt($booking);
return null !== $expiresAt && new \DateTimeImmutable() <= $expiresAt;
}
/**
* Marks the current session as authorized to view/act on this booking's offer.
* Called once, after a successful {@see isValidLinkRequest()} check at the
* signed-link entry point — every other route in this flow then trusts the
* session instead of re-deriving cryptographic validity from its own URL
* (which wouldn't work anyway, since the signature is bound to the exact
* URI it was generated for).
*/
public function authorizeSession(Request $request, AccommodationBooking $booking): void
{
$request->getSession()->set($this->sessionKey($booking), $booking->getAccessLinkIssuedAt()?->getTimestamp());
}
public function isSessionAuthorized(Request $request, AccommodationBooking $booking): bool
{
$issuedAt = $booking->getAccessLinkIssuedAt();
if (null === $issuedAt) {
return false;
}
if ($request->getSession()->get($this->sessionKey($booking)) !== $issuedAt->getTimestamp()) {
return false;
}
$expiresAt = $this->expiresAt($booking);
return null !== $expiresAt && new \DateTimeImmutable() <= $expiresAt;
}
private function sessionKey(AccommodationBooking $booking): string
{
return 'accommodation_offer_access_'.$booking->getUuid();
}
}