src/Controller/BetaController.php line 39

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Users;
  4. use App\Form\BetaAccessRequestType;
  5. use App\Repository\UsersRepository;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use App\Service\BetaAccessMailer;
  8. use App\Service\BetaCookie;
  9. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\Response;
  12. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  13. use Symfony\Component\Routing\Annotation\Route;
  14. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  15. class BetaController extends AbstractController
  16. {
  17.     private const TOKEN_TTL 1800// 30 minutes
  18.     /**
  19.      * Expéditeur des emails partant de SELECT.
  20.      *
  21.      * Le nom affiché est libre ; l'adresse, elle, doit rester un expéditeur
  22.      * validé côté Mailjet — `contact@lelorrain.fr` y est actif et déclaré
  23.      * transactionnel. En changer demande de la déclarer d'abord chez Mailjet.
  24.      */
  25.     private const SENDER_EMAIL 'contact@lelorrain.fr';
  26.     private const SENDER_NAME  'LE LORRAIN SELECT';
  27.     public function __construct(
  28.         private UsersRepository $usersRepository,
  29.         private BetaCookie $betaCookie,
  30.         private BetaAccessMailer $betaAccessMailer
  31.     ) {}
  32.     #[Route('/beta-acces'name'beta_request')]
  33.     public function request(Request $request): Response
  34.     {
  35.         $sent false;
  36.         $error null;
  37.         if ($request->isMethod('POST')) {
  38.             $email trim($request->request->get('email'''));
  39.             if ($email === '') {
  40.                 $error 'Merci de saisir votre adresse email.';
  41.             } elseif (!$this->hasBetaAccess($email)) {
  42.                 // Message volontairement générique : il ne révèle pas si l'adresse
  43.                 // existe ni si elle dispose d'un accès.
  44.                 //
  45.                 // Transmis en variable et non en flash : base.html.twig inclut
  46.                 // _flash.html.twig AVANT le bloc body, et `app.flashes` consomme le
  47.                 // sac — un flash serait donc affiché hors de la carte, sous l'en-tête
  48.                 // fixe, et resterait invisible.
  49.                 $error "L'accès à LE LORRAIN SELECT n'est pas possible avec cette adresse email. "
  50.                     'Pour demander un accès, contactez nos équipes via le formulaire de contact '
  51.                     'en bas de cette page.';
  52.             } else {
  53.                 $token bin2hex(random_bytes(32));
  54.                 $request->getSession()->set('subscription', [
  55.                     'email'       => $email,
  56.                     'token'       => $token,
  57.                     'is_verified' => false,
  58.                     'expires_at'  => time() + self::TOKEN_TTL,
  59.                 ]);
  60.                 $link $this->generateUrl(
  61.                     'confirm_email',
  62.                     ['token' => $token],
  63.                     UrlGeneratorInterface::ABSOLUTE_URL
  64.                 );
  65.                 $this->sendEmail(
  66.                     $email,
  67.                     $email,
  68.                     'Accès à LE LORRAIN SELECT – Confirmation de votre email',
  69.                     self::SENDER_EMAIL,
  70.                     self::SENDER_NAME,
  71.                     $this->buildEmailBody($link)
  72.                 );
  73.                 $sent true;
  74.             }
  75.         }
  76.         return $this->render('security/beta_request.html.twig', [
  77.             'sent'  => $sent,
  78.             'error' => $error,
  79.         ]);
  80.     }
  81.     /**
  82.      * Dépôt d'une demande d'accès par une personne qui n'en dispose pas.
  83.      *
  84.      * Crée un compte marqué « demande en attente », inutilisable en l'état :
  85.      * non vérifié, sans flag bêta, avec un mot de passe aléatoire. Un
  86.      * administrateur tranche ensuite depuis /admin/utilisateurs?filtre=demande-beta.
  87.      */
  88.     #[Route('/beta-acces/demande'name'beta_access_request'methods: ['GET''POST'])]
  89.     public function accessRequest(
  90.         Request $request,
  91.         EntityManagerInterface $em,
  92.         UserPasswordHasherInterface $passwordHasher
  93.     ): Response {
  94.         $form $this->createForm(BetaAccessRequestType::class);
  95.         $form->handleRequest($request);
  96.         $sent false;
  97.         if ($form->isSubmitted() && $form->isValid()) {
  98.             $data $form->getData();
  99.             $email trim((string) $data['email']);
  100.             $user $this->usersRepository->findOneBy(['email' => $email]);
  101.             if ($user === null) {
  102.                 $user = new Users();
  103.                 $user->setEmail($email);
  104.                 $user->setFirstname(trim((string) $data['firstname']));
  105.                 $user->setLastname(trim((string) $data['lastname']));
  106.                 $user->setCompanyName(trim((string) $data['companyName']));
  107.                 // Colonnes NOT NULL non demandées dans ce formulaire
  108.                 $user->setAddress('');
  109.                 $user->setZipcode('');
  110.                 $user->setCity('');
  111.                 // Le compte n'est pas utilisable : ni vérifié, ni bêta. Le mot de
  112.                 // passe aléatoire empêche toute connexion tant qu'il n'est pas
  113.                 // réinitialisé par la personne ou redéfini par un administrateur.
  114.                 $user->setIsVerified(false);
  115.                 $user->setPassword($passwordHasher->hashPassword($userbin2hex(random_bytes(16))));
  116.                 $em->persist($user);
  117.             }
  118.             // Compte déjà connu : on marque simplement la demande, sans rien écraser
  119.             $user->registerBetaRequest();
  120.             $em->flush();
  121.             $sent true;
  122.         }
  123.         return $this->render('security/beta_access_request.html.twig', [
  124.             'requestForm' => $form->createView(),
  125.             'sent'        => $sent,
  126.         ]);
  127.     }
  128.     #[Route('/confirm/{token}'name'confirm_email')]
  129.     public function confirm(string $tokenRequest $request): Response
  130.     {
  131.         // Le visiteur possède déjà un accès valide : le lien a donc déjà été
  132.         // consommé. On l'envoie sur l'outil plutôt que de lui opposer une erreur.
  133.         if ($this->betaCookie->isValid($request)) {
  134.             return $this->redirectToRoute('main');
  135.         }
  136.         $subscription $request->getSession()->get('subscription');
  137.         if (!$subscription) {
  138.             $this->addFlash('danger''Votre demande a expiré ou a été faite depuis un autre navigateur. Merci de redemander un lien.');
  139.             return $this->redirectToRoute('beta_request');
  140.         }
  141.         if (!hash_equals($subscription['token'], $token)) {
  142.             $this->addFlash('danger''Lien de connexion invalide.');
  143.             return $this->redirectToRoute('beta_request');
  144.         }
  145.         if ($subscription['expires_at'] < time()) {
  146.             $request->getSession()->remove('subscription');
  147.             $this->addFlash('danger''Ce lien a expiré. Merci d\'en demander un nouveau.');
  148.             return $this->redirectToRoute('beta_request');
  149.         }
  150.         // L'accès a pu être retiré entre l'envoi du lien et son ouverture
  151.         if (!$this->hasBetaAccess($subscription['email'])) {
  152.             $request->getSession()->remove('subscription');
  153.             $this->addFlash('danger'"Cette adresse email n'a plus accès à la bêta de LE LORRAIN SELECT.");
  154.             return $this->redirectToRoute('beta_request');
  155.         }
  156.         // Jeton à usage unique : on le retire de la session dès qu'il a servi.
  157.         // Rejouer le même lien ne redonnera plus l'accès — seul le cookie posé
  158.         // ci-dessous fait foi, pour la durée définie par BetaCookie::LIFETIME.
  159.         $request->getSession()->remove('subscription');
  160.         // Le cookie porte l'email signé : il identifie la personne pour les
  161.         // indicateurs SELECT et n'est pas falsifiable.
  162.         $response $this->redirectToRoute('main');
  163.         $response->headers->setCookie($this->betaCookie->create($subscription['email'], $request));
  164.         return $response;
  165.     }
  166.     private function hasBetaAccess(string $email): bool
  167.     {
  168.         $user $this->usersRepository->findOneBy(['email' => $email]);
  169.         return $user !== null && $user->isBeta();
  170.     }
  171.     private function buildEmailBody(string $link): string
  172.     {
  173.         // En-tête logo mutualisé avec BetaAccessMailer : une seule définition,
  174.         // une seule URL à changer le jour où le logo bouge.
  175.         return '<html><body style="font-family:Arial,Helvetica,sans-serif;color:#212529;line-height:1.5;">'
  176.             $this->betaAccessMailer->buildLogoHeader()
  177.             . '<p>Bonjour,</p>'
  178.             '<p>Vous avez demandé à accéder à la version bêta de LE LORRAIN SELECT.</p>'
  179.             '<p>Pour sécuriser votre accès, merci de confirmer votre adresse email en cliquant sur le lien ci-dessous :</p>'
  180.             '<p><a href="' $link '" style="display:inline-block;padding:12px 24px;background:#0d6efd;color:#fff;text-decoration:none;border-radius:6px;font-weight:bold;">Confirmer mon adresse email</a></p>'
  181.             '<p>Ce lien est valable <strong>30 minutes</strong>, ne fonctionne qu\'<strong>une seule fois</strong>, '
  182.             'et doit être ouvert depuis le navigateur où vous avez fait la demande.</p>'
  183.             '<p>Il vous ouvrira ensuite l\'accès pendant <strong>24 heures</strong>.</p>'
  184.             '<p>Si vous n\'êtes pas à l\'origine de cette demande, vous pouvez ignorer cet email.</p>'
  185.             '<p>À très bientôt sur LE LORRAIN SELECT,<br>L\'équipe LE LORRAIN SELECT</p>'
  186.             '</body></html>';
  187.     }
  188.     private function sendEmail(string $tostring $toTxtstring $subjectstring $fromstring $fromTxtstring $message): bool
  189.     {
  190.         $apiKey    'a9b94b25be48de689d7bbef6386dbd19';
  191.         $apiSecret 'be54ed1f3e34a2e6dc6f8b01634f81d3';
  192.         $data = [
  193.             'Messages' => [[
  194.                 'From' => ['Email' => $from'Name' => $fromTxt],
  195.                 'To'   => [['Email' => $to'Name' => $toTxt]],
  196.                 'Subject'  => $subject,
  197.                 'TextPart' => strip_tags($message),
  198.                 'HTMLPart' => $message,
  199.             ]],
  200.         ];
  201.         $ch curl_init('https://api.mailjet.com/v3.1/send');
  202.         curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
  203.         curl_setopt($chCURLOPT_HTTPAUTHCURLAUTH_BASIC);
  204.         curl_setopt($chCURLOPT_USERPWD$apiKey ':' $apiSecret);
  205.         curl_setopt($chCURLOPT_POSTtrue);
  206.         curl_setopt($chCURLOPT_POSTFIELDSjson_encode($data));
  207.         curl_setopt($chCURLOPT_HTTPHEADER, ['Content-Type: application/json']);
  208.         $response curl_exec($ch);
  209.         $httpCode curl_getinfo($chCURLINFO_HTTP_CODE);
  210.         curl_close($ch);
  211.         if ($httpCode === 200 && $response) {
  212.             $decoded json_decode($responsetrue);
  213.             return isset($decoded['Messages'][0]['Status']) && $decoded['Messages'][0]['Status'] === 'success';
  214.         }
  215.         return false;
  216.     }
  217. }