Add email tracking, CSP reporting, security controllers and services
- EmailTracking entity + repository + pixel tracking controller - CspReportController: filter noise, alert on real violations - RedirectController: external redirect warning page - UnsubscribeManager: HMAC-based unsubscribe with hashed storage - MailerService: rewrite with S/MIME, tracking, unsubscribe headers - ViteAssetExtension: add nonce CSP via Nelmio, isMobile - composer: add stripe/stripe-php, mobiledetect - Templates: add home/index, update base.html.twig with vite_asset - Email template: rebrand to E-Ticket Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,11 @@
|
|||||||
"league/flysystem-aws-s3-v3": "^3.32",
|
"league/flysystem-aws-s3-v3": "^3.32",
|
||||||
"league/flysystem-bundle": "^3.6",
|
"league/flysystem-bundle": "^3.6",
|
||||||
"liip/imagine-bundle": "^2.17",
|
"liip/imagine-bundle": "^2.17",
|
||||||
|
"mobiledetect/mobiledetectlib": "*",
|
||||||
"nelmio/security-bundle": "^3.9",
|
"nelmio/security-bundle": "^3.9",
|
||||||
"phpdocumentor/reflection-docblock": "^6.0",
|
"phpdocumentor/reflection-docblock": "^6.0",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"stripe/stripe-php": "*",
|
||||||
"symfony/asset": "8.0.*",
|
"symfony/asset": "8.0.*",
|
||||||
"symfony/console": "8.0.*",
|
"symfony/console": "8.0.*",
|
||||||
"symfony/doctrine-messenger": "8.0.*",
|
"symfony/doctrine-messenger": "8.0.*",
|
||||||
|
|||||||
176
composer.lock
generated
176
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "57b0fe3e59760d7d8cb08112bed744a4",
|
"content-hash": "06d85967d9503db645c96c90e761fa2a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
@@ -2865,6 +2865,70 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-07-25T09:04:22+00:00"
|
"time": "2025-07-25T09:04:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mobiledetect/mobiledetectlib",
|
||||||
|
"version": "4.8.10",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/serbanghita/Mobile-Detect.git",
|
||||||
|
"reference": "96b1e1fa9a968de7660a031106ab529f659d0192"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96b1e1fa9a968de7660a031106ab529f659d0192",
|
||||||
|
"reference": "96b1e1fa9a968de7660a031106ab529f659d0192",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0",
|
||||||
|
"psr/simple-cache": "^3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^v3.75.0",
|
||||||
|
"phpbench/phpbench": "^1.2",
|
||||||
|
"phpstan/phpstan": "^2.1.11",
|
||||||
|
"phpunit/phpunit": "^9.6.22",
|
||||||
|
"squizlabs/php_codesniffer": "^3.12.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Detection\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Serban Ghita",
|
||||||
|
"email": "serbanghita@gmail.com",
|
||||||
|
"homepage": "http://mobiledetect.net",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
|
||||||
|
"homepage": "https://github.com/serbanghita/Mobile-Detect",
|
||||||
|
"keywords": [
|
||||||
|
"detect mobile devices",
|
||||||
|
"mobile",
|
||||||
|
"mobile detect",
|
||||||
|
"mobile detector",
|
||||||
|
"php mobile detect"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/serbanghita/Mobile-Detect/issues",
|
||||||
|
"source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.10"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/serbanghita",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-01-09T16:21:59+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@@ -3797,6 +3861,57 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-09-11T13:17:53+00:00"
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/simple-cache",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/simple-cache.git",
|
||||||
|
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||||
|
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\SimpleCache\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interfaces for simple caching",
|
||||||
|
"keywords": [
|
||||||
|
"cache",
|
||||||
|
"caching",
|
||||||
|
"psr",
|
||||||
|
"psr-16",
|
||||||
|
"simple-cache"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2021-10-29T13:26:27+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ralouphie/getallheaders",
|
"name": "ralouphie/getallheaders",
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -3921,6 +4036,65 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-03-03T17:31:43+00:00"
|
"time": "2026-03-03T17:31:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stripe/stripe-php",
|
||||||
|
"version": "v19.4.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stripe/stripe-php.git",
|
||||||
|
"reference": "095384404587d07de2ad1154c389c4051c5ed92f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/095384404587d07de2ad1154c389c4051c5ed92f",
|
||||||
|
"reference": "095384404587d07de2ad1154c389c4051c5ed92f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "3.94.0",
|
||||||
|
"phpstan/phpstan": "^1.2",
|
||||||
|
"phpunit/phpunit": "^5.7 || ^9.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stripe\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Stripe and contributors",
|
||||||
|
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Stripe PHP Library",
|
||||||
|
"homepage": "https://stripe.com/",
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"payment processing",
|
||||||
|
"stripe"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||||
|
"source": "https://github.com/stripe/stripe-php/tree/v19.4.1"
|
||||||
|
},
|
||||||
|
"time": "2026-03-06T22:53:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/asset",
|
"name": "symfony/asset",
|
||||||
"version": "v8.0.6",
|
"version": "v8.0.6",
|
||||||
|
|||||||
@@ -38,10 +38,3 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
|
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
|
||||||
|
|
||||||
App\Service\MailerService:
|
|
||||||
arguments:
|
|
||||||
$smimeCertificate: '%kernel.project_dir%/cert/certificate.pem'
|
|
||||||
$smimePrivateKey: '%kernel.project_dir%/cert/private-key.pem'
|
|
||||||
$smimePassphrase: '%env(SMIME_PASSPHRASE)%'
|
|
||||||
$fromEmail: 'contact@e-cosplay.fr'
|
|
||||||
$realMail: '%env(bool:REAL_MAIL)%'
|
|
||||||
|
|||||||
83
src/Controller/CspReportController.php
Normal file
83
src/Controller/CspReportController.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class CspReportController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/my-csp-report', name: 'app_csp_report', methods: ['POST'])]
|
||||||
|
public function report(Request $request, MailerInterface $mailer, LoggerInterface $logger): Response
|
||||||
|
{
|
||||||
|
$data = $request->getContent();
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return new Response('No data provided', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = json_decode($data, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$logger->error('CSP Report JSON decode error: ' . json_last_error_msg());
|
||||||
|
return new Response('Invalid JSON', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = $report['csp-report'] ?? $report;
|
||||||
|
|
||||||
|
$sourceFile = $details['source-file'] ?? '';
|
||||||
|
$blockedUri = $details['blocked-uri'] ?? '';
|
||||||
|
$documentUri = $details['document-uri'] ?? '';
|
||||||
|
$violatedDirective = $details['violated-directive'] ?? '';
|
||||||
|
|
||||||
|
$shouldIgnore = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($sourceFile, 'extension://') ||
|
||||||
|
str_contains($sourceFile, 'chrome-extension') ||
|
||||||
|
str_contains($sourceFile, 'moz-extension') ||
|
||||||
|
str_contains($documentUri, '.local') ||
|
||||||
|
str_contains($sourceFile, 'localhost') ||
|
||||||
|
$blockedUri === 'wasm-eval' ||
|
||||||
|
$blockedUri === 'inline' && str_contains($sourceFile, 'node_modules') ||
|
||||||
|
$blockedUri === 'about:blank'
|
||||||
|
) {
|
||||||
|
$shouldIgnore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldIgnore) {
|
||||||
|
$logger->info('CSP Violation ignored (Extension or Local Dev): ' . $sourceFile);
|
||||||
|
return new Response('Report ignored', 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger->warning('REAL CSP VIOLATION: ' . $data);
|
||||||
|
|
||||||
|
$email = (new Email())
|
||||||
|
->from('security-notify@e-cosplay.fr')
|
||||||
|
->to('contact@e-cosplay.fr')
|
||||||
|
->subject('Alerte Securite : Violation CSP detectee')
|
||||||
|
->priority(Email::PRIORITY_HIGH)
|
||||||
|
->text(
|
||||||
|
"Un rapport de violation CSP potentiellement critique a ete intercepte.\n\n" .
|
||||||
|
"Document: " . $documentUri . "\n" .
|
||||||
|
"Directive violee: " . $violatedDirective . "\n" .
|
||||||
|
"Element bloque: " . $blockedUri . "\n" .
|
||||||
|
"Fichier source: " . $sourceFile . "\n\n" .
|
||||||
|
"Details complets :\n" .
|
||||||
|
json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mailer->send($email);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$logger->error('Failed to send CSP report email: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Report processed', 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Controller/EmailTrackingController.php
Normal file
35
src/Controller/EmailTrackingController.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\EmailTrackingRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class EmailTrackingController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/track/{messageId}/logo.jpg', name: 'app_email_track', methods: ['GET'])]
|
||||||
|
public function track(
|
||||||
|
string $messageId,
|
||||||
|
EmailTrackingRepository $repository,
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
#[Autowire('%kernel.project_dir%')] string $projectDir,
|
||||||
|
): Response {
|
||||||
|
$tracking = $repository->findOneBy(['messageId' => $messageId]);
|
||||||
|
|
||||||
|
if ($tracking !== null) {
|
||||||
|
$tracking->markAsOpened();
|
||||||
|
$em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new BinaryFileResponse($projectDir . '/public/logo.jpg');
|
||||||
|
$response->headers->set('Content-Type', 'image/jpeg');
|
||||||
|
$response->headers->set('Cache-Control', 'no-store');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Controller/RedirectController.php
Normal file
23
src/Controller/RedirectController.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class RedirectController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/external-redirect', name: 'app_external_redirect', methods: ['GET'])]
|
||||||
|
public function warning(Request $request): Response
|
||||||
|
{
|
||||||
|
$url = $request->query->get('redirUrl');
|
||||||
|
|
||||||
|
if (!$url) {
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('pages/external_redirect.twig');
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/Entity/EmailTracking.php
Normal file
85
src/Entity/EmailTracking.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\EmailTrackingRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: EmailTrackingRepository::class)]
|
||||||
|
class EmailTracking
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 64, unique: true)]
|
||||||
|
private ?string $messageId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $recipient = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $subject = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 10)]
|
||||||
|
private ?string $state = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?\DateTimeImmutable $sentAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $openedAt = null;
|
||||||
|
|
||||||
|
public function __construct(string $messageId, string $recipient, string $subject)
|
||||||
|
{
|
||||||
|
$this->messageId = $messageId;
|
||||||
|
$this->recipient = $recipient;
|
||||||
|
$this->subject = $subject;
|
||||||
|
$this->state = 'sent';
|
||||||
|
$this->sentAt = new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessageId(): ?string
|
||||||
|
{
|
||||||
|
return $this->messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecipient(): ?string
|
||||||
|
{
|
||||||
|
return $this->recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubject(): ?string
|
||||||
|
{
|
||||||
|
return $this->subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState(): ?string
|
||||||
|
{
|
||||||
|
return $this->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSentAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->sentAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOpenedAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->openedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsOpened(): void
|
||||||
|
{
|
||||||
|
if ($this->state === 'sent') {
|
||||||
|
$this->state = 'opened';
|
||||||
|
$this->openedAt = new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Repository/EmailTrackingRepository.php
Normal file
18
src/Repository/EmailTrackingRepository.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\EmailTracking;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<EmailTracking>
|
||||||
|
*/
|
||||||
|
class EmailTrackingRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, EmailTracking::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/Service/UnsubscribeManager.php
Normal file
74
src/Service/UnsubscribeManager.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
class UnsubscribeManager
|
||||||
|
{
|
||||||
|
private string $storagePath;
|
||||||
|
private string $secret;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%kernel.project_dir%')] string $projectDir,
|
||||||
|
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||||
|
) {
|
||||||
|
$this->storagePath = $projectDir . '/var/unsubscribed.json';
|
||||||
|
$this->secret = $appSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateToken(string $email): string
|
||||||
|
{
|
||||||
|
return hash_hmac('sha256', strtolower(trim($email)), $this->secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValidToken(string $email, string $token): bool
|
||||||
|
{
|
||||||
|
return hash_equals($this->generateToken($email), $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUnsubscribed(string $email): bool
|
||||||
|
{
|
||||||
|
$hash = $this->generateToken($email);
|
||||||
|
|
||||||
|
return \in_array($hash, $this->loadHashes(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unsubscribe(string $email): void
|
||||||
|
{
|
||||||
|
$hash = $this->generateToken($email);
|
||||||
|
$hashes = $this->loadHashes();
|
||||||
|
|
||||||
|
if (!\in_array($hash, $hashes, true)) {
|
||||||
|
$hashes[] = $hash;
|
||||||
|
$this->saveHashes($hashes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function loadHashes(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->storagePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents($this->storagePath), true);
|
||||||
|
|
||||||
|
return \is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $hashes
|
||||||
|
*/
|
||||||
|
private function saveHashes(array $hashes): void
|
||||||
|
{
|
||||||
|
$dir = \dirname($this->storagePath);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0o755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($this->storagePath, json_encode(array_values($hashes)), \LOCK_EX);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use Detection\MobileDetect;
|
|||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
|
use Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener;
|
||||||
|
|
||||||
class ViteAssetExtension extends AbstractExtension
|
class ViteAssetExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ class ViteAssetExtension extends AbstractExtension
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $manifest,
|
private readonly string $manifest,
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
|
private readonly ?ContentSecurityPolicyListener $cspListener = null,
|
||||||
) {
|
) {
|
||||||
$this->isDev = $_ENV['VITE_LOAD'] === "0";
|
$this->isDev = $_ENV['VITE_LOAD'] === "0";
|
||||||
}
|
}
|
||||||
@@ -26,18 +26,21 @@ class ViteAssetExtension extends AbstractExtension
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']]),
|
new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']]),
|
||||||
|
new TwigFunction('isMobile', $this->isMobile(...), ['is_safe' => ['html']]),
|
||||||
new TwigFunction('vite_favicons', $this->favicons(...), ['is_safe' => ['html']])
|
new TwigFunction('vite_favicons', $this->favicons(...), ['is_safe' => ['html']])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère le nonce pour les scripts via le Listener de Nelmio
|
|
||||||
*/
|
|
||||||
protected function getNonce(): string
|
protected function getNonce(): string
|
||||||
{
|
{
|
||||||
return '';
|
return $this->cspListener?->getNonce('script') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isMobile(): bool
|
||||||
|
{
|
||||||
|
$detect = new MobileDetect();
|
||||||
|
return $detect->isMobile() || $detect->isTablet();
|
||||||
|
}
|
||||||
|
|
||||||
private function loadManifest(): void
|
private function loadManifest(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,15 @@
|
|||||||
"bin/phpunit"
|
"bin/phpunit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"stripe/stripe-php": {
|
||||||
|
"version": "19.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "19.0",
|
||||||
|
"ref": "d6829c693e3927a8972c7671d74a1a5c505712b0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"symfony/console": {
|
"symfony/console": {
|
||||||
"version": "8.0",
|
"version": "8.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
{% block stylesheets %}{% endblock %}
|
{% block stylesheets %}{% endblock %}
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ vite_asset('app.js') }}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
{% block javascripts %}{% endblock %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -85,14 +85,14 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🎫 E-Cosplay Ticket</h1>
|
<h1>🎫 E-Ticket</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<p>© {{ "now"|date("Y") }} E-Cosplay — <a href="https://e-cosplay.fr">e-cosplay.fr</a></p>
|
<p>© {{ "now"|date("Y") }} E-Ticket — <a href="https://ticket.e-cosplay.fr">e-cosplay.fr</a></p>
|
||||||
<p>Cet email a été envoyé depuis contact@e-cosplay.fr</p>
|
<p>Cet email a été envoyé depuis contact@e-cosplay.fr</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
templates/home/index.html.twig
Normal file
7
templates/home/index.html.twig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Accueil - E-Ticket{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user