<?php
namespace App\Controller;
use App\Entity\Users;
use App\Form\BetaAccessRequestType;
use App\Repository\UsersRepository;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\BetaAccessMailer;
use App\Service\BetaCookie;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class BetaController extends AbstractController
{
private const TOKEN_TTL = 1800; // 30 minutes
/**
* Expéditeur des emails partant de SELECT.
*
* Le nom affiché est libre ; l'adresse, elle, doit rester un expéditeur
* validé côté Mailjet — `contact@lelorrain.fr` y est actif et déclaré
* transactionnel. En changer demande de la déclarer d'abord chez Mailjet.
*/
private const SENDER_EMAIL = 'contact@lelorrain.fr';
private const SENDER_NAME = 'LE LORRAIN SELECT';
public function __construct(
private UsersRepository $usersRepository,
private BetaCookie $betaCookie,
private BetaAccessMailer $betaAccessMailer
) {}
#[Route('/beta-acces', name: 'beta_request')]
public function request(Request $request): Response
{
$sent = false;
$error = null;
if ($request->isMethod('POST')) {
$email = trim($request->request->get('email', ''));
if ($email === '') {
$error = 'Merci de saisir votre adresse email.';
} elseif (!$this->hasBetaAccess($email)) {
// Message volontairement générique : il ne révèle pas si l'adresse
// existe ni si elle dispose d'un accès.
//
// Transmis en variable et non en flash : base.html.twig inclut
// _flash.html.twig AVANT le bloc body, et `app.flashes` consomme le
// sac — un flash serait donc affiché hors de la carte, sous l'en-tête
// fixe, et resterait invisible.
$error = "L'accès à LE LORRAIN SELECT n'est pas possible avec cette adresse email. "
. 'Pour demander un accès, contactez nos équipes via le formulaire de contact '
. 'en bas de cette page.';
} else {
$token = bin2hex(random_bytes(32));
$request->getSession()->set('subscription', [
'email' => $email,
'token' => $token,
'is_verified' => false,
'expires_at' => time() + self::TOKEN_TTL,
]);
$link = $this->generateUrl(
'confirm_email',
['token' => $token],
UrlGeneratorInterface::ABSOLUTE_URL
);
$this->sendEmail(
$email,
$email,
'Accès à LE LORRAIN SELECT – Confirmation de votre email',
self::SENDER_EMAIL,
self::SENDER_NAME,
$this->buildEmailBody($link)
);
$sent = true;
}
}
return $this->render('security/beta_request.html.twig', [
'sent' => $sent,
'error' => $error,
]);
}
/**
* Dépôt d'une demande d'accès par une personne qui n'en dispose pas.
*
* Crée un compte marqué « demande en attente », inutilisable en l'état :
* non vérifié, sans flag bêta, avec un mot de passe aléatoire. Un
* administrateur tranche ensuite depuis /admin/utilisateurs?filtre=demande-beta.
*/
#[Route('/beta-acces/demande', name: 'beta_access_request', methods: ['GET', 'POST'])]
public function accessRequest(
Request $request,
EntityManagerInterface $em,
UserPasswordHasherInterface $passwordHasher
): Response {
$form = $this->createForm(BetaAccessRequestType::class);
$form->handleRequest($request);
$sent = false;
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$email = trim((string) $data['email']);
$user = $this->usersRepository->findOneBy(['email' => $email]);
if ($user === null) {
$user = new Users();
$user->setEmail($email);
$user->setFirstname(trim((string) $data['firstname']));
$user->setLastname(trim((string) $data['lastname']));
$user->setCompanyName(trim((string) $data['companyName']));
// Colonnes NOT NULL non demandées dans ce formulaire
$user->setAddress('');
$user->setZipcode('');
$user->setCity('');
// Le compte n'est pas utilisable : ni vérifié, ni bêta. Le mot de
// passe aléatoire empêche toute connexion tant qu'il n'est pas
// réinitialisé par la personne ou redéfini par un administrateur.
$user->setIsVerified(false);
$user->setPassword($passwordHasher->hashPassword($user, bin2hex(random_bytes(16))));
$em->persist($user);
}
// Compte déjà connu : on marque simplement la demande, sans rien écraser
$user->registerBetaRequest();
$em->flush();
$sent = true;
}
return $this->render('security/beta_access_request.html.twig', [
'requestForm' => $form->createView(),
'sent' => $sent,
]);
}
#[Route('/confirm/{token}', name: 'confirm_email')]
public function confirm(string $token, Request $request): Response
{
// Le visiteur possède déjà un accès valide : le lien a donc déjà été
// consommé. On l'envoie sur l'outil plutôt que de lui opposer une erreur.
if ($this->betaCookie->isValid($request)) {
return $this->redirectToRoute('main');
}
$subscription = $request->getSession()->get('subscription');
if (!$subscription) {
$this->addFlash('danger', 'Votre demande a expiré ou a été faite depuis un autre navigateur. Merci de redemander un lien.');
return $this->redirectToRoute('beta_request');
}
if (!hash_equals($subscription['token'], $token)) {
$this->addFlash('danger', 'Lien de connexion invalide.');
return $this->redirectToRoute('beta_request');
}
if ($subscription['expires_at'] < time()) {
$request->getSession()->remove('subscription');
$this->addFlash('danger', 'Ce lien a expiré. Merci d\'en demander un nouveau.');
return $this->redirectToRoute('beta_request');
}
// L'accès a pu être retiré entre l'envoi du lien et son ouverture
if (!$this->hasBetaAccess($subscription['email'])) {
$request->getSession()->remove('subscription');
$this->addFlash('danger', "Cette adresse email n'a plus accès à la bêta de LE LORRAIN SELECT.");
return $this->redirectToRoute('beta_request');
}
// Jeton à usage unique : on le retire de la session dès qu'il a servi.
// Rejouer le même lien ne redonnera plus l'accès — seul le cookie posé
// ci-dessous fait foi, pour la durée définie par BetaCookie::LIFETIME.
$request->getSession()->remove('subscription');
// Le cookie porte l'email signé : il identifie la personne pour les
// indicateurs SELECT et n'est pas falsifiable.
$response = $this->redirectToRoute('main');
$response->headers->setCookie($this->betaCookie->create($subscription['email'], $request));
return $response;
}
private function hasBetaAccess(string $email): bool
{
$user = $this->usersRepository->findOneBy(['email' => $email]);
return $user !== null && $user->isBeta();
}
private function buildEmailBody(string $link): string
{
// En-tête logo mutualisé avec BetaAccessMailer : une seule définition,
// une seule URL à changer le jour où le logo bouge.
return '<html><body style="font-family:Arial,Helvetica,sans-serif;color:#212529;line-height:1.5;">'
. $this->betaAccessMailer->buildLogoHeader()
. '<p>Bonjour,</p>'
. '<p>Vous avez demandé à accéder à la version bêta de LE LORRAIN SELECT.</p>'
. '<p>Pour sécuriser votre accès, merci de confirmer votre adresse email en cliquant sur le lien ci-dessous :</p>'
. '<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>'
. '<p>Ce lien est valable <strong>30 minutes</strong>, ne fonctionne qu\'<strong>une seule fois</strong>, '
. 'et doit être ouvert depuis le navigateur où vous avez fait la demande.</p>'
. '<p>Il vous ouvrira ensuite l\'accès pendant <strong>24 heures</strong>.</p>'
. '<p>Si vous n\'êtes pas à l\'origine de cette demande, vous pouvez ignorer cet email.</p>'
. '<p>À très bientôt sur LE LORRAIN SELECT,<br>L\'équipe LE LORRAIN SELECT</p>'
. '</body></html>';
}
private function sendEmail(string $to, string $toTxt, string $subject, string $from, string $fromTxt, string $message): bool
{
$apiKey = 'a9b94b25be48de689d7bbef6386dbd19';
$apiSecret = 'be54ed1f3e34a2e6dc6f8b01634f81d3';
$data = [
'Messages' => [[
'From' => ['Email' => $from, 'Name' => $fromTxt],
'To' => [['Email' => $to, 'Name' => $toTxt]],
'Subject' => $subject,
'TextPart' => strip_tags($message),
'HTMLPart' => $message,
]],
];
$ch = curl_init('https://api.mailjet.com/v3.1/send');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $apiKey . ':' . $apiSecret);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && $response) {
$decoded = json_decode($response, true);
return isset($decoded['Messages'][0]['Status']) && $decoded['Messages'][0]['Status'] === 'success';
}
return false;
}
}