src/Controller/SecurityController.php line 54

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use App\Entity\User;
  5. use App\Form\RequestAccessFormType;
  6. use App\Form\SigninRegistrationFormType;
  7. use App\Repository\ClientRepository;
  8. use App\Repository\UserRepository;
  9. use App\Service\EmailSenderService;
  10. use App\Service\MagicLinkService;
  11. use App\Service\RecaptchaService;
  12. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  13. use MobileDetectBundle\DeviceDetector\MobileDetectorInterface;
  14. use Psr\Log\LoggerInterface;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpFoundation\Response;
  18. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  21. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  22. class SecurityController extends AbstractController
  23. {
  24.   private $userRepository;
  25.   private $clientRepository;
  26.   private $emailSenderService;
  27.   private $magicLinkService;
  28.   private $logger;
  29.   public function __construct(
  30.     UserRepository $userRepository,
  31.     ClientRepository $clientRepository,
  32.     EmailSenderService $emailSenderService,
  33.     MagicLinkService $magicLinkService,
  34.     LoggerInterface $logger
  35.   ) {
  36.     $this->userRepository $userRepository;
  37.     $this->clientRepository $clientRepository;
  38.     $this->emailSenderService $emailSenderService;
  39.     $this->magicLinkService $magicLinkService;
  40.     $this->logger $logger;
  41.   }
  42.   /**
  43.    * Page d'accueil et connexion classique (email/password)
  44.    *
  45.    * @Route("/", name="home")
  46.    * @Route("/login", name="app_login")
  47.    */
  48.   public function home(AuthenticationUtils $authenticationUtilsRequest $requestMobileDetectorInterface $mobileDetectorSessionInterface $session): Response
  49.   {
  50.     // Si l'utilisateur est déjà authentifié, rediriger vers le dashboard
  51.     if ($this->getUser()) {
  52.       return $this->redirectToRoute('suggestion.index');
  53.     }
  54.     $isInactive = ($request->query->get('isInactive') != null) ? 0;
  55.     $error $authenticationUtils->getLastAuthenticationError();
  56.     $prefilledEmail $request->query->get('email''');
  57.     $isRedirected = ($request->query->get('redirected') == '1');
  58.     $loginErrorType $request->query->get('login_error'null);
  59.     $showCreateAccountInfo $request->query->get('create_account') === '1';
  60.     $clientCreateAccountUrl null;
  61.     $hideLoginActions false;
  62.     $showMagicLinkConfirmation = ($request->query->get('magic_link_sent') === '1' && $prefilledEmail !== '');
  63.     $magicLinkType $request->query->get('magic_link_type''connexion');
  64.     $showRequestAccessForm = ($request->query->get('show') === 'demander-mon-acces');
  65.     $requestAccessForm $this->createForm(RequestAccessFormType::class);
  66.     $requestAccessForm->handleRequest($request);
  67.     $requestAccessEligible $request->query->get('eligible');
  68.     $requestAccessSigninUrl null;
  69.     if ($showRequestAccessForm && $requestAccessEligible === '1') {
  70.       $requestAccessSigninUrl $session->get('request_access_signin_url');
  71.     }
  72.     // Si un token est présent, récupérer l'email de l'utilisateur correspondant
  73.     $token $request->query->get('token''');
  74.     if (!empty($token) && empty($prefilledEmail)) {
  75.       $user $this->userRepository->findOneBy(['token' => $token]);
  76.       // ✅ Vérifications de sécurité (Option 3)
  77.       if ($user) {
  78.         // Vérifier que le compte est actif
  79.         if (!$user->isIsActive()) {
  80.           $this->addFlash('error''Ce compte est inactif.');
  81.           return $this->redirectToRoute('app_login');
  82.         }
  83.         $prefilledEmail $user->getEmail();
  84.       }
  85.     }
  86.     // Vérifier si l'utilisateur (si email pré-rempli) n'a pas de mot de passe
  87.     // Ce sont les utilisateurs ANCIENS créés avant l'implémentation du système de mot de passe
  88.     $userHasNoPassword false;
  89.     if (!empty($prefilledEmail)) {
  90.       $user $this->userRepository->findOneBy(['email' => $prefilledEmail]);
  91.       if ($user && empty($user->getPassword())) {
  92.         $userHasNoPassword true;
  93.       }
  94.     }
  95.     // Scénario "compte inexistant/non activé" + entreprise détectée via domain_restriction
  96.     if ($loginErrorType === 'account_missing_client' && !empty($prefilledEmail)) {
  97.       $client $this->clientRepository->findClientByEmailDomain($prefilledEmail);
  98.       if ($client) {
  99.         $clientCreateAccountUrl $this->generateUrl('client.signin', [
  100.           'slug' => $client->getSlug(),
  101.           'token' => $client->getTokenCooptor(),
  102.         ]);
  103.         $hideLoginActions true;
  104.       } else {
  105.         // Fallback: si aucun client finalement trouvé, on bascule sur le message "RH"
  106.         $loginErrorType 'account_missing_no_client';
  107.       }
  108.     }
  109.     return $this->render('page/index.html.twig', [
  110.       'error' => $error,
  111.       'isInactive' => $isInactive,
  112.       'prefilledEmail' => $prefilledEmail,
  113.       'isRedirected' => $isRedirected,
  114.       'userHasNoPassword' => $userHasNoPassword,
  115.       'login_error_type' => $loginErrorType,
  116.       'show_create_account_info' => $showCreateAccountInfo,
  117.       'client_create_account_url' => $clientCreateAccountUrl,
  118.       'hide_login_actions' => $hideLoginActions,
  119.       'is_mobile' => $mobileDetector->isMobile(),
  120.       'show_magic_link_confirmation' => $showMagicLinkConfirmation,
  121.       'magic_link_email' => $showMagicLinkConfirmation $prefilledEmail null,
  122.       'magic_link_type' => $showMagicLinkConfirmation $magicLinkType null,
  123.       'show_request_access_form' => $showRequestAccessForm,
  124.       'request_access_form' => $requestAccessForm->createView(),
  125.       'request_access_eligible' => $requestAccessEligible,
  126.       'request_access_signin_url' => $requestAccessSigninUrl,
  127.     ]);
  128.   }
  129.   /**
  130.    * Page "Demander mon accès" : formulaire de vérification d'éligibilité par email professionnel.
  131.    * Redirige vers la page d'accueil avec le formulaire affiché (?show=demander-mon-acces).
  132.    */
  133.   public function requestAccess(Request $requestSessionInterface $session): Response
  134.   {
  135.     if ($this->getUser()) {
  136.       return $this->redirectToRoute('suggestion.index');
  137.     }
  138.     if ($request->isMethod('GET')) {
  139.       return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  140.     }
  141.     $form $this->createForm(RequestAccessFormType::class);
  142.     $form->handleRequest($request);
  143.     $eligible null;
  144.     $signinUrl null;
  145.     if ($form->isSubmitted() && $form->isValid()) {
  146.       $email = (string) $form->get('email')->getData();
  147.       $email strtolower(trim($email));
  148.       // Si l'utilisateur a déjà un compte, on envoie un lien de connexion (Magic Link)
  149.       // plutôt que de le rediriger vers le flux "inscription" (évite la confusion UX).
  150.       $existingUser $this->userRepository->findOneByLoginEmail($email);
  151.       if ($existingUser !== null) {
  152.         if (!$existingUser->isIsActive()) {
  153.           $this->addFlash('error''Ce compte est inactif.');
  154.           return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  155.         }
  156.         try {
  157.           $magicToken $this->magicLinkService->generateForExistingUser($existingUser);
  158.           $magicLinkUrl $this->generateUrl('app_magic_link_verify', ['token' => $magicToken], UrlGeneratorInterface::ABSOLUTE_URL);
  159.           $firstname trim((string) $existingUser->getFirstname());
  160.           $this->emailSenderService->sendTemplatedEmail(
  161.             $email,
  162.             'Connexion à votre espace Bambboo',
  163.             'emails/magic_link.html.twig',
  164.             ['firstname' => $firstname'magic_link_url' => $magicLinkUrl]
  165.           );
  166.         } catch (\Throwable $e) {
  167.           $this->logger->error('requestAccess: envoi Magic Link échoué', ['email' => $email'exception' => $e->getMessage()]);
  168.           $this->addFlash('error''L\'envoi du lien de connexion a échoué. Veuillez réessayer dans quelques minutes ou contacter le support.');
  169.           return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  170.         }
  171.         return $this->redirectToRoute('home', [
  172.           'email' => $email,
  173.           'magic_link_sent' => '1',
  174.           'magic_link_type' => 'connexion',
  175.         ]);
  176.       }
  177.       $client $this->clientRepository->findClientByEmailDomain($email);
  178.       $eligible = ($client !== null);
  179.       if ($client !== null) {
  180.         $slug $client->getSlug();
  181.         $tokenCooptor $client->getTokenCooptor();
  182.         $signinUrl $this->generateUrl('client.signin', ['slug' => $slug'token' => $tokenCooptor], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
  183.         $session->set('request_access_signin_url'$signinUrl);
  184.         $session->set('request_access_email'$email);
  185.       }
  186.     } elseif ($form->isSubmitted() && !$form->isValid()) {
  187.       $this->addFlash('error''Veuillez saisir une adresse email professionnelle valide.');
  188.     }
  189.     return $this->redirectToRoute('home', [
  190.       'show' => 'demander-mon-acces',
  191.       'eligible' => $eligible === true '1' : ($eligible === false '0' null),
  192.     ]);
  193.   }
  194.   /**
  195.    * Renvoie l'email de lien d'inscription (après succès "Demander mon accès").
  196.    */
  197.   public function requestAccessResend(Request $requestSessionInterface $session): Response
  198.   {
  199.     if ($this->getUser()) {
  200.       return $this->redirectToRoute('suggestion.index');
  201.     }
  202.     $token $request->request->get('_csrf_token''');
  203.     if (!$this->isCsrfTokenValid('request_access_resend'$token)) {
  204.       $this->addFlash('error''Token de sécurité invalide.');
  205.       return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  206.     }
  207.     $email $session->get('request_access_email');
  208.     $signinUrl $session->get('request_access_signin_url');
  209.     if ($email === null || $signinUrl === null) {
  210.       $this->addFlash('warning''Session expirée. Veuillez vérifier à nouveau votre éligibilité.');
  211.       return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  212.     }
  213.     $user $this->userRepository->findOneByLoginEmail($email);
  214.     $firstname $user !== null trim((string) $user->getFirstname()) : '';
  215.     try {
  216.       $this->sendRequestAccessEmail($email$firstname$signinUrl);
  217.       $this->addFlash('success''L\'email a été renvoyé.');
  218.     } catch (TransportExceptionInterface $e) {
  219.       $this->logger->error('Request access resend email failed', ['email' => $email'exception' => $e->getMessage()]);
  220.       $this->addFlash('error''Impossible de renvoyer l\'email. Veuillez réessayer plus tard.');
  221.     }
  222.     return $this->redirectToRoute('home', ['show' => 'demander-mon-acces']);
  223.   }
  224.   /**
  225.    * Demande d'envoi d'un Magic Link depuis la page d'accueil (POST).
  226.    * Si le compte existe : envoi du lien de connexion.
  227.    * Si le compte n'existe pas et que le domaine est reconnu : création du contexte pending signup et envoi du lien.
  228.    */
  229.   public function requestMagicLink(Request $request): Response
  230.   {
  231.     if ($this->getUser()) {
  232.       return $this->redirectToRoute('suggestion.index');
  233.     }
  234.     $email trim((string) $request->request->get('email'''));
  235.     if ($email === '') {
  236.       $this->addFlash('error''Veuillez saisir votre adresse email.');
  237.       return $this->redirectToRoute('home');
  238.     }
  239.     if (!$this->isCsrfTokenValid('request_magic_link'$request->request->get('_csrf_token'''))) {
  240.       $this->addFlash('error''Token de sécurité invalide.');
  241.       return $this->redirectToRoute('home');
  242.     }
  243.     $email strtolower($email);
  244.     $user $this->userRepository->findOneBy(['email' => $email]);
  245.     if ($user !== null) {
  246.       if (!$user->isIsActive()) {
  247.         return $this->redirectToRoute('home', ['email' => $email'magic_link_sent' => '1''magic_link_type' => 'connexion']);
  248.       }
  249.       try {
  250.         $magicToken $this->magicLinkService->generateForExistingUser($user);
  251.         $magicLinkUrl $this->generateUrl('app_magic_link_verify', ['token' => $magicToken], UrlGeneratorInterface::ABSOLUTE_URL);
  252.         $firstname trim((string) $user->getFirstname());
  253.         $this->emailSenderService->sendTemplatedEmail(
  254.           $email,
  255.           'Connexion à votre espace Bambboo',
  256.           'emails/magic_link.html.twig',
  257.           ['firstname' => $firstname'magic_link_url' => $magicLinkUrl]
  258.         );
  259.       } catch (\Throwable $e) {
  260.         $this->logger->error('requestMagicLink: envoi échoué', ['email' => $email'exception' => $e->getMessage()]);
  261.         $this->addFlash('error''L\'envoi du lien de connexion a échoué. Veuillez réessayer dans quelques minutes ou contacter le support.');
  262.         return $this->redirectToRoute('home', ['email' => $email]);
  263.       }
  264.       return $this->redirectToRoute('home', ['email' => $email'magic_link_sent' => '1''magic_link_type' => 'connexion']);
  265.     }
  266.     $client $this->clientRepository->findClientByEmailDomain($email);
  267.     if ($client !== null) {
  268.       try {
  269.         $magicToken $this->magicLinkService->generateForPendingSignup($email$client'ROLE_COOPTOR');
  270.         $magicLinkUrl $this->generateUrl('app_magic_link_verify', ['token' => $magicToken], UrlGeneratorInterface::ABSOLUTE_URL);
  271.         $firstname '';
  272.         $this->emailSenderService->sendTemplatedEmail(
  273.           $email,
  274.           'Vérifiez votre email pour activer votre compte Bambboo',
  275.           'emails/signup_verify_email.html.twig',
  276.           ['firstname' => $firstname'magic_link_url' => $magicLinkUrl]
  277.         );
  278.       } catch (\Throwable $e) {
  279.         $this->logger->error('requestMagicLink: pending signup envoi échoué', ['email' => $email'exception' => $e->getMessage()]);
  280.         $this->addFlash('error''L\'envoi du lien d\'activation a échoué. Veuillez réessayer dans quelques minutes ou contacter le support.');
  281.         return $this->redirectToRoute('home', ['email' => $email]);
  282.       }
  283.       return $this->redirectToRoute('home', ['email' => $email'magic_link_sent' => '1''magic_link_type' => 'inscription']);
  284.     }
  285.     return $this->redirectToRoute('home', ['email' => $email'login_error' => 'domain_not_partner']);
  286.   }
  287.   /**
  288.    * Envoie l'email contenant le lien d'inscription (demander mon accès).
  289.    *
  290.    * @param string $email Destinataire
  291.    * @param string $firstname Prénom pour la salutation
  292.    * @param string $signinUrl URL absolue du formulaire d'inscription client
  293.    */
  294.   private function sendRequestAccessEmail(string $emailstring $firstnamestring $signinUrl): void
  295.   {
  296.     $this->emailSenderService->sendTemplatedEmail(
  297.       $email,
  298.       'Accéder à mon espace Bambboo',
  299.       'emails/request_access_link.html.twig',
  300.       [
  301.         'firstname' => $firstname,
  302.         'signin_url' => $signinUrl,
  303.       ]
  304.     );
  305.   }
  306.   /**
  307.    * Extrait un prénom pour la salutation à partir de la partie locale de l'email.
  308.    */
  309.   private function extractFirstnameFromEmail(string $email): string
  310.   {
  311.     $pos strpos($email'@');
  312.     if ($pos === false || $pos === 0) {
  313.       return '';
  314.     }
  315.     $local substr($email0$pos);
  316.     $local preg_replace('/[^a-zA-ZÀ-ÿ]/'''$local);
  317.     return $local !== '' ucfirst(strtolower($local)) : '';
  318.   }
  319.   /**
  320.    * Page d'inscription depuis un lien client (slug/token)
  321.    * 
  322.    * @Route("/{slug}/{token}", name="client.signin")
  323.    */
  324.   public function signin(
  325.     string $slug,
  326.     string $token,
  327.     ClientRepository $clientRepository,
  328.     AuthenticationUtils $authenticationUtils,
  329.     SessionInterface $session,
  330.     Request $request,
  331.     UserRepository $userRepository,
  332.     RecaptchaService $recaptchaService,
  333.     MobileDetectorInterface $mobileDetector
  334.   ): Response {
  335.     // Si l'utilisateur est déjà authentifié, on exécute directement la logique post-login
  336.     if ($this->getUser()) {
  337.       return $this->forward(PostLoginController::class . '::handlePostLogin');
  338.     }
  339.     $session->remove('linkedin_cookies');
  340.     $client $clientRepository->findOneBy(['slug' => $slug]);
  341.     $session->set('origin'$request->getRequestUri());
  342.     if (!$client) {
  343.       return $this->redirectToRoute('home');
  344.     }
  345.     $canSignIn 1;
  346.     $role = [];
  347.     if ($token === $client->getTokenRecruiter()) {
  348.       $role = ["ROLE_RECRUITER"];
  349.       $maxUser $client->getMaxRecruiter();
  350.       if ($maxUser) {
  351.         $coworkers count($userRepository->getCoworkers($client));
  352.         if ($coworkers >= $maxUser) {
  353.           $canSignIn 0;
  354.         }
  355.       }
  356.     } elseif ($token === $client->getTokenCooptor()) {
  357.       $role = ["ROLE_COOPTOR"];
  358.       $maxUser $client->getMaxCooptor();
  359.       if ($maxUser) {
  360.         $coworkers count($userRepository->getCoworkers($client));
  361.         if ($coworkers >= $maxUser) {
  362.           $canSignIn 0;
  363.         }
  364.       }
  365.     } else {
  366.       return $this->redirectToRoute('home');
  367.     }
  368.     $session->set('client_id'$client->getId());
  369.     $session->set('role'$role);
  370.     $error $authenticationUtils->getLastAuthenticationError();
  371.     $isMobile $mobileDetector->isMobile();
  372.     // Créer le formulaire d'inscription
  373.     $user = new User();
  374.     $form $this->createForm(SigninRegistrationFormType::class, $user);
  375.     $form->handleRequest($request);
  376.     if ($form->isSubmitted()) {
  377.       $submittedEmail null;
  378.       try {
  379.         $submittedEmail $form->get('email')->getData();
  380.       } catch (\Throwable $e) {
  381.         $submittedEmail null;
  382.       }
  383.       $this->logger->info('SIGNUP: formulaire soumis', [
  384.         'route' => 'client.signin',
  385.         'method' => $request->getMethod(),
  386.         'slug' => $slug,
  387.         'is_mobile' => $isMobile,
  388.         'email' => $submittedEmail,
  389.       ]);
  390.     }
  391.     // Si l'utilisateur existe déjà : envoyer un Magic Link de connexion
  392.     if ($form->isSubmitted()) {
  393.       $email $form->get('email')->getData();
  394.       $email is_string($email) ? strtolower(trim($email)) : '';
  395.       if ($email !== '') {
  396.         $existingUser $userRepository->findOneByLoginEmail($email);
  397.         if ($existingUser) {
  398.           $this->logger->info('SIGNUP: email déjà existant, envoi Magic Link', [
  399.             'email' => $email,
  400.             'existing_user_id' => $existingUser->getId(),
  401.             'is_mobile' => $isMobile,
  402.           ]);
  403.           if (!$existingUser->isIsActive()) {
  404.             $recaptchaSiteKey $this->getRecaptchaSiteKey();
  405.             return $this->render('client/signin.html.twig', [
  406.               'client' => $client,
  407.               'error' => $error,
  408.               'canSignIn' => $canSignIn,
  409.               'form' => $form->createView(),
  410.               'recaptcha_site_key' => $recaptchaSiteKey,
  411.               'magic_link_sent' => false,
  412.             ]);
  413.           }
  414.           try {
  415.             $magicToken $this->magicLinkService->generateForExistingUser($existingUser);
  416.             $magicLinkUrl $this->generateUrl('app_magic_link_verify', ['token' => $magicToken], UrlGeneratorInterface::ABSOLUTE_URL);
  417.             $firstname trim((string) $existingUser->getFirstname());
  418.             $this->emailSenderService->sendTemplatedEmail(
  419.               $email,
  420.               'Connexion à votre espace Bambboo',
  421.               'emails/magic_link.html.twig',
  422.               ['firstname' => $firstname'magic_link_url' => $magicLinkUrl]
  423.             );
  424.           } catch (\Throwable $e) {
  425.             $this->logger->error('SIGNUP: envoi Magic Link échoué', ['email' => $email'exception' => $e->getMessage()]);
  426.           }
  427.           $recaptchaSiteKey $this->getRecaptchaSiteKey();
  428.           return $this->render('client/signin.html.twig', [
  429.             'client' => $client,
  430.             'error' => $error,
  431.             'canSignIn' => $canSignIn,
  432.             'form' => $form->createView(),
  433.             'recaptcha_site_key' => $recaptchaSiteKey,
  434.             'magic_link_sent' => true,
  435.             'magic_link_email' => $email,
  436.             'magic_link_type' => 'connexion',
  437.           ]);
  438.         }
  439.       }
  440.     }
  441.     if ($form->isSubmitted() && !$form->isValid()) {
  442.       $errors = [];
  443.       foreach ($form->getErrors(true) as $errorItem) {
  444.         $origin $errorItem->getOrigin();
  445.         $errors[] = [
  446.           'field' => $origin $origin->getName() : null,
  447.           'message' => $errorItem->getMessage(),
  448.         ];
  449.       }
  450.       $this->logger->warning('SIGNUP: formulaire invalide', [
  451.         'is_mobile' => $isMobile,
  452.         'email' => $form->has('email') ? $form->get('email')->getData() : null,
  453.         'errors' => $errors,
  454.       ]);
  455.     }
  456.     if ($form->isSubmitted() && $form->isValid()) {
  457.       // Validation reCAPTCHA Enterprise uniquement si l'utilisateur n'est pas connecté
  458.       if (!$this->getUser()) {
  459.         $recaptchaToken $form->get('recaptcha_token')->getData();
  460.         $recaptchaResult $recaptchaService->verify(
  461.           $recaptchaToken ?? '',
  462.           $request->getClientIp(),
  463.           0.3 // Score minimum requis (abaissé pour être plus souple)
  464.         );
  465.         // En mode souple, on ne bloque jamais l'inscription même si le reCAPTCHA échoue
  466.         if (!$recaptchaResult['success']) {
  467.           // Log pour monitoring
  468.         }
  469.       }
  470.       $email $form->get('email')->getData();
  471.       $email is_string($email) ? strtolower(trim($email)) : '';
  472.       $roleValue $role[0] ?? 'ROLE_COOPTOR';
  473.       try {
  474.         $magicToken $this->magicLinkService->generateForPendingSignup($email$client$roleValue);
  475.         $magicLinkUrl $this->generateUrl('app_magic_link_verify', ['token' => $magicToken], UrlGeneratorInterface::ABSOLUTE_URL);
  476.         $firstname '';
  477.         $this->emailSenderService->sendTemplatedEmail(
  478.           $email,
  479.           'Vérifiez votre email pour activer votre compte Bambboo',
  480.           'emails/signup_verify_email.html.twig',
  481.           ['firstname' => $firstname'magic_link_url' => $magicLinkUrl]
  482.         );
  483.       } catch (\Throwable $e) {
  484.         $this->logger->error('SIGNUP: envoi Magic Link pending signup échoué', ['email' => $email'exception' => $e->getMessage()]);
  485.         $recaptchaSiteKey $this->getRecaptchaSiteKey();
  486.         return $this->render('client/signin.html.twig', [
  487.           'client' => $client,
  488.           'error' => $error,
  489.           'canSignIn' => $canSignIn,
  490.           'form' => $form->createView(),
  491.           'recaptcha_site_key' => $recaptchaSiteKey,
  492.           'magic_link_sent' => false,
  493.         ]);
  494.       }
  495.       $recaptchaSiteKey $this->getRecaptchaSiteKey();
  496.       return $this->render('client/signin.html.twig', [
  497.         'client' => $client,
  498.         'error' => $error,
  499.         'canSignIn' => $canSignIn,
  500.         'form' => $form->createView(),
  501.         'recaptcha_site_key' => $recaptchaSiteKey,
  502.         'magic_link_sent' => true,
  503.         'magic_link_email' => $email,
  504.         'magic_link_type' => 'inscription',
  505.       ]);
  506.     }
  507.     $recaptchaSiteKey $this->getRecaptchaSiteKey();
  508.     return $this->render('client/signin.html.twig', [
  509.       'client' => $client,
  510.       'error' => $error,
  511.       'canSignIn' => $canSignIn,
  512.       'form' => $form->createView(),
  513.       'recaptcha_site_key' => $recaptchaSiteKey,
  514.       'magic_link_sent' => false,
  515.     ]);
  516.   }
  517.   /**
  518.    * Retourne la clé reCAPTCHA Enterprise si configurée.
  519.    */
  520.   private function getRecaptchaSiteKey(): string
  521.   {
  522.     if ($this->getUser()) {
  523.       return '';
  524.     }
  525.     try {
  526.       return (string) ($this->getParameter('app.recaptcha_enterprise_site_key') ?? '');
  527.     } catch (\Exception $e) {
  528.       return '';
  529.     }
  530.   }
  531.   /**
  532.    * @Route("/logout", name="app_logout")
  533.    */
  534.   public function logout(): void
  535.   {
  536.     // Ce code ne sera jamais exécuté, Symfony intercepte cette route pour gérer la déconnexion
  537.     throw new \LogicException('Logout route');
  538.   }
  539. }