108 lines
3.4 KiB
PHP
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();
|
|
}
|
|
}
|