src/Controller/SecurityController.php line 41

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use App\Entity\User;
  5. use App\Form\SigninRegistrationFormType;
  6. use App\Repository\ClientRepository;
  7. use App\Repository\UserRepository;
  8. use App\Service\RecaptchaService;
  9. use DateTimeImmutable;
  10. use Doctrine\ORM\EntityManagerInterface;
  11. use MobileDetectBundle\DeviceDetector\MobileDetectorInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  17. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  18. use Symfony\Component\Routing\Annotation\Route;
  19. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  20. class SecurityController extends AbstractController
  21. {
  22.   private $userRepository;
  23.   private $logger;
  24.   public function __construct(UserRepository $userRepositoryLoggerInterface $logger)
  25.   {
  26.     $this->userRepository $userRepository;
  27.     $this->logger $logger;
  28.   }
  29.   /**
  30.    * Page d'accueil et connexion classique (email/password)
  31.    * 
  32.    * @Route("/", name="home")
  33.    * @Route("/login", name="app_login")
  34.    */
  35.   public function home(AuthenticationUtils $authenticationUtilsRequest $request): Response
  36.   {
  37.     // Si l'utilisateur est déjà authentifié, rediriger vers le dashboard
  38.     if ($this->getUser()) {
  39.       return $this->redirectToRoute('suggestion.index');
  40.     }
  41.     $isInactive = ($request->query->get('isInactive') != null) ? 0;
  42.     $error $authenticationUtils->getLastAuthenticationError();
  43.     $prefilledEmail $request->query->get('email''');
  44.     $isRedirected = ($request->query->get('redirected') == '1');
  45.     // Si un token est présent, récupérer l'email de l'utilisateur correspondant
  46.     $token $request->query->get('token''');
  47.     if (!empty($token) && empty($prefilledEmail)) {
  48.       $user $this->userRepository->findOneBy(['token' => $token]);
  49.       // ✅ Vérifications de sécurité (Option 3)
  50.       if ($user) {
  51.         // Vérifier que le compte est actif
  52.         if (!$user->isIsActive()) {
  53.           $this->addFlash('error''Ce compte est inactif.');
  54.           return $this->redirectToRoute('app_login');
  55.         }
  56.         $prefilledEmail $user->getEmail();
  57.       }
  58.     }
  59.     // Vérifier si l'utilisateur (si email pré-rempli) n'a pas de mot de passe
  60.     // Ce sont les utilisateurs ANCIENS créés avant l'implémentation du système de mot de passe
  61.     $userHasNoPassword false;
  62.     if (!empty($prefilledEmail)) {
  63.       $user $this->userRepository->findOneBy(['email' => $prefilledEmail]);
  64.       if ($user && empty($user->getPassword())) {
  65.         $userHasNoPassword true;
  66.       }
  67.     }
  68.     return $this->render('page/index.html.twig', [
  69.       'error' => $error,
  70.       'isInactive' => $isInactive,
  71.       'prefilledEmail' => $prefilledEmail,
  72.       'isRedirected' => $isRedirected,
  73.       'userHasNoPassword' => $userHasNoPassword
  74.     ]);
  75.   }
  76.   /**
  77.    * Page d'inscription depuis un lien client (slug/token)
  78.    * 
  79.    * @Route("/{slug}/{token}", name="client.signin")
  80.    */
  81.   public function signin(
  82.     string $slug,
  83.     string $token,
  84.     ClientRepository $clientRepository,
  85.     AuthenticationUtils $authenticationUtils,
  86.     SessionInterface $session,
  87.     Request $request,
  88.     UserRepository $userRepository,
  89.     EntityManagerInterface $entityManager,
  90.     UserPasswordHasherInterface $userPasswordHasher,
  91.     RecaptchaService $recaptchaService,
  92.     MobileDetectorInterface $mobileDetector
  93.   ): Response {
  94.     // Si l'utilisateur est déjà authentifié, on exécute directement la logique post-login
  95.     if ($this->getUser()) {
  96.       return $this->forward(PostLoginController::class . '::handlePostLogin');
  97.     }
  98.     $session->remove('linkedin_cookies');
  99.     $client $clientRepository->findOneBy(['slug' => $slug]);
  100.     $session->set('origin'$request->getRequestUri());
  101.     if (!$client) {
  102.       return $this->redirectToRoute('home');
  103.     }
  104.     $canSignIn 1;
  105.     $role = [];
  106.     if ($token === $client->getTokenRecruiter()) {
  107.       $role = ["ROLE_RECRUITER"];
  108.       $maxUser $client->getMaxRecruiter();
  109.       if ($maxUser) {
  110.         $coworkers count($userRepository->getCoworkers($client));
  111.         if ($coworkers >= $maxUser) {
  112.           $canSignIn 0;
  113.         }
  114.       }
  115.     } elseif ($token === $client->getTokenCooptor()) {
  116.       $role = ["ROLE_COOPTOR"];
  117.       $maxUser $client->getMaxCooptor();
  118.       if ($maxUser) {
  119.         $coworkers count($userRepository->getCoworkers($client));
  120.         if ($coworkers >= $maxUser) {
  121.           $canSignIn 0;
  122.         }
  123.       }
  124.     } else {
  125.       return $this->redirectToRoute('home');
  126.     }
  127.     $session->set('client_id'$client->getId());
  128.     $session->set('role'$role);
  129.     $error $authenticationUtils->getLastAuthenticationError();
  130.     $isMobile $mobileDetector->isMobile();
  131.     // Créer le formulaire d'inscription
  132.     $user = new User();
  133.     $form $this->createForm(SigninRegistrationFormType::class, $user);
  134.     $form->handleRequest($request);
  135.     if ($form->isSubmitted()) {
  136.       $submittedEmail null;
  137.       try {
  138.         $submittedEmail $form->get('email')->getData();
  139.       } catch (\Throwable $e) {
  140.         $submittedEmail null;
  141.       }
  142.       $skipConfirmationRaw null;
  143.       if ($form->has('skipPasswordConfirmation')) {
  144.         $skipConfirmationRaw $form->get('skipPasswordConfirmation')->getData();
  145.       }
  146.       $confirmationProvided null;
  147.       if ($form->has('plainPasswordConfirmation')) {
  148.         $confirmationProvided = !empty($form->get('plainPasswordConfirmation')->getData());
  149.       }
  150.       $this->logger->info('SIGNUP: formulaire soumis', [
  151.         'route' => 'client.signin',
  152.         'method' => $request->getMethod(),
  153.         'slug' => $slug,
  154.         'is_mobile' => $isMobile,
  155.         'email' => $submittedEmail,
  156.         'skip_confirmation_raw' => $skipConfirmationRaw,
  157.         'confirmation_provided' => $confirmationProvided,
  158.       ]);
  159.     }
  160.     // Vérifier si un utilisateur existe déjà AVANT la validation du formulaire
  161.     // pour éviter que la contrainte @UniqueEntity n'affiche l'erreur
  162.     if ($form->isSubmitted()) {
  163.       $email $form->get('email')->getData();
  164.       if ($email) {
  165.         $existingUser $userRepository->findOneBy(['email' => $email]);
  166.         if ($existingUser) {
  167.           $this->logger->warning('SIGNUP: email déjà existant', [
  168.             'email' => $email,
  169.             'existing_user_id' => $existingUser->getId(),
  170.             'is_mobile' => $isMobile,
  171.           ]);
  172.           // Afficher un message et rediriger via JavaScript après 2 secondes
  173.           // Récupérer la clé reCAPTCHA uniquement si l'utilisateur n'est pas connecté
  174.           $recaptchaSiteKey '';
  175.           if (!$this->getUser()) {
  176.             try {
  177.               $recaptchaSiteKey $this->getParameter('app.recaptcha_enterprise_site_key') ?? '';
  178.             } catch (\Exception $e) {
  179.               $recaptchaSiteKey '';
  180.             }
  181.           }
  182.           return $this->render('client/signin.html.twig', [
  183.             'client' => $client,
  184.             'error' => $error,
  185.             'canSignIn' => $canSignIn,
  186.             'form' => $form->createView(),
  187.             'recaptcha_site_key' => $recaptchaSiteKey,
  188.             'existingUserEmail' => $email,
  189.             'showRedirectMessage' => true,
  190.           ]);
  191.         }
  192.       }
  193.     }
  194.     if ($form->isSubmitted() && !$form->isValid()) {
  195.       $errors = [];
  196.       foreach ($form->getErrors(true) as $errorItem) {
  197.         $origin $errorItem->getOrigin();
  198.         $errors[] = [
  199.           'field' => $origin $origin->getName() : null,
  200.           'message' => $errorItem->getMessage(),
  201.         ];
  202.       }
  203.       $this->logger->warning('SIGNUP: formulaire invalide', [
  204.         'is_mobile' => $isMobile,
  205.         'email' => $form->has('email') ? $form->get('email')->getData() : null,
  206.         'errors' => $errors,
  207.       ]);
  208.     }
  209.     if ($form->isSubmitted() && $form->isValid()) {
  210.       // Validation reCAPTCHA Enterprise uniquement si l'utilisateur n'est pas connecté
  211.       if (!$this->getUser()) {
  212.         $recaptchaToken $form->get('recaptcha_token')->getData();
  213.         $recaptchaResult $recaptchaService->verify(
  214.           $recaptchaToken ?? '',
  215.           $request->getClientIp(),
  216.           0.3 // Score minimum requis (abaissé pour être plus souple)
  217.         );
  218.         // En mode souple, on ne bloque jamais l'inscription même si le reCAPTCHA échoue
  219.         // On peut juste logger pour monitoring
  220.         if (!$recaptchaResult['success']) {
  221.           // Log pour monitoring mais on continue quand même
  222.           // (décommenter si vous voulez logger)
  223.           // $this->logger->warning('reCAPTCHA validation failed but continuing', ['errors' => $recaptchaResult['errors']]);
  224.         }
  225.       }
  226.       // Encoder le mot de passe
  227.       $hashedPassword $userPasswordHasher->hashPassword(
  228.         $user,
  229.         $form->get('plainPassword')->getData()
  230.       );
  231.       $user->setPassword($hashedPassword);
  232.       // Configurer l'utilisateur
  233.       $user->setRoles($role);
  234.       $user->setClient($client);
  235.       $user->setOrigin($request->getRequestUri());
  236.       $user->setCreatedAt(new DateTimeImmutable());
  237.       $user->setModifiedAt(new DateTimeImmutable());
  238.       $user->setIsVerified(false);
  239.       $user->setIsActive(true);
  240.       $user->setHasExtension(0);
  241.       $user->setHasSeenLinkedinPreviewModal(false);
  242.       $user->setIsSuggestionTourHidden(false);
  243.       $user->setOnboardingStep('phone');
  244.       // Génération du token
  245.       $bytes random_bytes(24);
  246.       $tokenValue rtrim(strtr(base64_encode($bytes), '+/''-_'), '=');
  247.       $user->setToken($tokenValue);
  248.       // Génération du rememberMeKey
  249.       $rememberMeKey bin2hex(random_bytes(32));
  250.       $user->setRememberMeKey($rememberMeKey);
  251.       $entityManager->persist($user);
  252.       try {
  253.         $entityManager->flush();
  254.       } catch (\Throwable $e) {
  255.         $this->logger->error('SIGNUP: erreur lors du flush utilisateur', [
  256.           'is_mobile' => $isMobile,
  257.           'email' => $user->getEmail(),
  258.           'exception' => $e->getMessage(),
  259.         ]);
  260.         throw $e;
  261.       }
  262.       $this->logger->info('SIGNUP: utilisateur créé', [
  263.         'user_id' => $user->getId(),
  264.         'email' => $user->getEmail(),
  265.         'client_id' => $client->getId(),
  266.         'is_mobile' => $isMobile,
  267.       ]);
  268.       // Authentifier l'utilisateur automatiquement
  269.       $token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken(
  270.         $user,
  271.         'main',
  272.         $user->getRoles()
  273.       );
  274.       $this->container->get('security.token_storage')->setToken($token);
  275.       $session->set('_security_main'serialize($token));
  276.       // Rediriger vers l'étape téléphone
  277.       return $this->redirectToRoute('onboarding.phone');
  278.     }
  279.     // Récupérer la clé reCAPTCHA uniquement si l'utilisateur n'est pas connecté
  280.     $recaptchaSiteKey '';
  281.     if (!$this->getUser()) {
  282.       try {
  283.         $recaptchaSiteKey $this->getParameter('app.recaptcha_enterprise_site_key') ?? '';
  284.       } catch (\Exception $e) {
  285.         // Si le paramètre n'existe pas, on continue sans reCAPTCHA
  286.         $recaptchaSiteKey '';
  287.       }
  288.     }
  289.     return $this->render('client/signin.html.twig', [
  290.       'client' => $client,
  291.       'error' => $error,
  292.       'canSignIn' => $canSignIn,
  293.       'form' => $form->createView(),
  294.       'recaptcha_site_key' => $recaptchaSiteKey,
  295.       'showRedirectMessage' => false,
  296.       'existingUserEmail' => null,
  297.     ]);
  298.   }
  299.   /**
  300.    * @Route("/logout", name="app_logout")
  301.    */
  302.   public function logout(): void
  303.   {
  304.     // Ce code ne sera jamais exécuté, Symfony intercepte cette route pour gérer la déconnexion
  305.     throw new \LogicException('Logout route');
  306.   }
  307. }