<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use App\Form\SigninRegistrationFormType;
use App\Repository\ClientRepository;
use App\Repository\UserRepository;
use App\Service\RecaptchaService;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use MobileDetectBundle\DeviceDetector\MobileDetectorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
private $userRepository;
private $logger;
public function __construct(UserRepository $userRepository, LoggerInterface $logger)
{
$this->userRepository = $userRepository;
$this->logger = $logger;
}
/**
* Page d'accueil et connexion classique (email/password)
*
* @Route("/", name="home")
* @Route("/login", name="app_login")
*/
public function home(AuthenticationUtils $authenticationUtils, Request $request): Response
{
// Si l'utilisateur est déjà authentifié, rediriger vers le dashboard
if ($this->getUser()) {
return $this->redirectToRoute('suggestion.index');
}
$isInactive = ($request->query->get('isInactive') != null) ? 1 : 0;
$error = $authenticationUtils->getLastAuthenticationError();
$prefilledEmail = $request->query->get('email', '');
$isRedirected = ($request->query->get('redirected') == '1');
// Si un token est présent, récupérer l'email de l'utilisateur correspondant
$token = $request->query->get('token', '');
if (!empty($token) && empty($prefilledEmail)) {
$user = $this->userRepository->findOneBy(['token' => $token]);
// ✅ Vérifications de sécurité (Option 3)
if ($user) {
// Vérifier que le compte est actif
if (!$user->isIsActive()) {
$this->addFlash('error', 'Ce compte est inactif.');
return $this->redirectToRoute('app_login');
}
$prefilledEmail = $user->getEmail();
}
}
// Vérifier si l'utilisateur (si email pré-rempli) n'a pas de mot de passe
// Ce sont les utilisateurs ANCIENS créés avant l'implémentation du système de mot de passe
$userHasNoPassword = false;
if (!empty($prefilledEmail)) {
$user = $this->userRepository->findOneBy(['email' => $prefilledEmail]);
if ($user && empty($user->getPassword())) {
$userHasNoPassword = true;
}
}
return $this->render('page/index.html.twig', [
'error' => $error,
'isInactive' => $isInactive,
'prefilledEmail' => $prefilledEmail,
'isRedirected' => $isRedirected,
'userHasNoPassword' => $userHasNoPassword
]);
}
/**
* Page d'inscription depuis un lien client (slug/token)
*
* @Route("/{slug}/{token}", name="client.signin")
*/
public function signin(
string $slug,
string $token,
ClientRepository $clientRepository,
AuthenticationUtils $authenticationUtils,
SessionInterface $session,
Request $request,
UserRepository $userRepository,
EntityManagerInterface $entityManager,
UserPasswordHasherInterface $userPasswordHasher,
RecaptchaService $recaptchaService,
MobileDetectorInterface $mobileDetector
): Response {
// Si l'utilisateur est déjà authentifié, on exécute directement la logique post-login
if ($this->getUser()) {
return $this->forward(PostLoginController::class . '::handlePostLogin');
}
$session->remove('linkedin_cookies');
$client = $clientRepository->findOneBy(['slug' => $slug]);
$session->set('origin', $request->getRequestUri());
if (!$client) {
return $this->redirectToRoute('home');
}
$canSignIn = 1;
$role = [];
if ($token === $client->getTokenRecruiter()) {
$role = ["ROLE_RECRUITER"];
$maxUser = $client->getMaxRecruiter();
if ($maxUser) {
$coworkers = count($userRepository->getCoworkers($client));
if ($coworkers >= $maxUser) {
$canSignIn = 0;
}
}
} elseif ($token === $client->getTokenCooptor()) {
$role = ["ROLE_COOPTOR"];
$maxUser = $client->getMaxCooptor();
if ($maxUser) {
$coworkers = count($userRepository->getCoworkers($client));
if ($coworkers >= $maxUser) {
$canSignIn = 0;
}
}
} else {
return $this->redirectToRoute('home');
}
$session->set('client_id', $client->getId());
$session->set('role', $role);
$error = $authenticationUtils->getLastAuthenticationError();
$isMobile = $mobileDetector->isMobile();
// Créer le formulaire d'inscription
$user = new User();
$form = $this->createForm(SigninRegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted()) {
$submittedEmail = null;
try {
$submittedEmail = $form->get('email')->getData();
} catch (\Throwable $e) {
$submittedEmail = null;
}
$skipConfirmationRaw = null;
if ($form->has('skipPasswordConfirmation')) {
$skipConfirmationRaw = $form->get('skipPasswordConfirmation')->getData();
}
$confirmationProvided = null;
if ($form->has('plainPasswordConfirmation')) {
$confirmationProvided = !empty($form->get('plainPasswordConfirmation')->getData());
}
$this->logger->info('SIGNUP: formulaire soumis', [
'route' => 'client.signin',
'method' => $request->getMethod(),
'slug' => $slug,
'is_mobile' => $isMobile,
'email' => $submittedEmail,
'skip_confirmation_raw' => $skipConfirmationRaw,
'confirmation_provided' => $confirmationProvided,
]);
}
// Vérifier si un utilisateur existe déjà AVANT la validation du formulaire
// pour éviter que la contrainte @UniqueEntity n'affiche l'erreur
if ($form->isSubmitted()) {
$email = $form->get('email')->getData();
if ($email) {
$existingUser = $userRepository->findOneBy(['email' => $email]);
if ($existingUser) {
$this->logger->warning('SIGNUP: email déjà existant', [
'email' => $email,
'existing_user_id' => $existingUser->getId(),
'is_mobile' => $isMobile,
]);
// Afficher un message et rediriger via JavaScript après 2 secondes
// Récupérer la clé reCAPTCHA uniquement si l'utilisateur n'est pas connecté
$recaptchaSiteKey = '';
if (!$this->getUser()) {
try {
$recaptchaSiteKey = $this->getParameter('app.recaptcha_enterprise_site_key') ?? '';
} catch (\Exception $e) {
$recaptchaSiteKey = '';
}
}
return $this->render('client/signin.html.twig', [
'client' => $client,
'error' => $error,
'canSignIn' => $canSignIn,
'form' => $form->createView(),
'recaptcha_site_key' => $recaptchaSiteKey,
'existingUserEmail' => $email,
'showRedirectMessage' => true,
]);
}
}
}
if ($form->isSubmitted() && !$form->isValid()) {
$errors = [];
foreach ($form->getErrors(true) as $errorItem) {
$origin = $errorItem->getOrigin();
$errors[] = [
'field' => $origin ? $origin->getName() : null,
'message' => $errorItem->getMessage(),
];
}
$this->logger->warning('SIGNUP: formulaire invalide', [
'is_mobile' => $isMobile,
'email' => $form->has('email') ? $form->get('email')->getData() : null,
'errors' => $errors,
]);
}
if ($form->isSubmitted() && $form->isValid()) {
// Validation reCAPTCHA Enterprise uniquement si l'utilisateur n'est pas connecté
if (!$this->getUser()) {
$recaptchaToken = $form->get('recaptcha_token')->getData();
$recaptchaResult = $recaptchaService->verify(
$recaptchaToken ?? '',
$request->getClientIp(),
0.3 // Score minimum requis (abaissé pour être plus souple)
);
// En mode souple, on ne bloque jamais l'inscription même si le reCAPTCHA échoue
// On peut juste logger pour monitoring
if (!$recaptchaResult['success']) {
// Log pour monitoring mais on continue quand même
// (décommenter si vous voulez logger)
// $this->logger->warning('reCAPTCHA validation failed but continuing', ['errors' => $recaptchaResult['errors']]);
}
}
// Encoder le mot de passe
$hashedPassword = $userPasswordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
);
$user->setPassword($hashedPassword);
// Configurer l'utilisateur
$user->setRoles($role);
$user->setClient($client);
$user->setOrigin($request->getRequestUri());
$user->setCreatedAt(new DateTimeImmutable());
$user->setModifiedAt(new DateTimeImmutable());
$user->setIsVerified(false);
$user->setIsActive(true);
$user->setHasExtension(0);
$user->setHasSeenLinkedinPreviewModal(false);
$user->setIsSuggestionTourHidden(false);
$user->setOnboardingStep('phone');
// Génération du token
$bytes = random_bytes(24);
$tokenValue = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
$user->setToken($tokenValue);
// Génération du rememberMeKey
$rememberMeKey = bin2hex(random_bytes(32));
$user->setRememberMeKey($rememberMeKey);
$entityManager->persist($user);
try {
$entityManager->flush();
} catch (\Throwable $e) {
$this->logger->error('SIGNUP: erreur lors du flush utilisateur', [
'is_mobile' => $isMobile,
'email' => $user->getEmail(),
'exception' => $e->getMessage(),
]);
throw $e;
}
$this->logger->info('SIGNUP: utilisateur créé', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'client_id' => $client->getId(),
'is_mobile' => $isMobile,
]);
// Authentifier l'utilisateur automatiquement
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken(
$user,
'main',
$user->getRoles()
);
$this->container->get('security.token_storage')->setToken($token);
$session->set('_security_main', serialize($token));
// Rediriger vers l'étape téléphone
return $this->redirectToRoute('onboarding.phone');
}
// Récupérer la clé reCAPTCHA uniquement si l'utilisateur n'est pas connecté
$recaptchaSiteKey = '';
if (!$this->getUser()) {
try {
$recaptchaSiteKey = $this->getParameter('app.recaptcha_enterprise_site_key') ?? '';
} catch (\Exception $e) {
// Si le paramètre n'existe pas, on continue sans reCAPTCHA
$recaptchaSiteKey = '';
}
}
return $this->render('client/signin.html.twig', [
'client' => $client,
'error' => $error,
'canSignIn' => $canSignIn,
'form' => $form->createView(),
'recaptcha_site_key' => $recaptchaSiteKey,
'showRedirectMessage' => false,
'existingUserEmail' => null,
]);
}
/**
* @Route("/logout", name="app_logout")
*/
public function logout(): void
{
// Ce code ne sera jamais exécuté, Symfony intercepte cette route pour gérer la déconnexion
throw new \LogicException('Logout route');
}
}