<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use App\Form\ForgotPasswordFormType;
use App\Form\ResetPasswordFormType;
use App\Repository\UserRepository;
use App\Service\EmailSenderService;
use App\Service\PasswordResetRateLimiter;
use App\Service\PasswordResetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Contrôleur pour la réinitialisation de mot de passe
*/
class PasswordResetController extends AbstractController
{
private $passwordResetService;
private $userRepository;
private $emailSenderService;
private $entityManager;
private $rateLimiter;
private $appUrl;
/**
* @param PasswordResetService $passwordResetService Service de réinitialisation
* @param UserRepository $userRepository Repository des utilisateurs
* @param EmailSenderService $emailSenderService Service d'envoi d'emails
* @param EntityManagerInterface $entityManager Entity Manager
* @param PasswordResetRateLimiter $rateLimiter Rate limiter
* @param string $appUrl URL de l'application
*/
public function __construct(
PasswordResetService $passwordResetService,
UserRepository $userRepository,
EmailSenderService $emailSenderService,
EntityManagerInterface $entityManager,
PasswordResetRateLimiter $rateLimiter,
string $appUrl
) {
$this->passwordResetService = $passwordResetService;
$this->userRepository = $userRepository;
$this->emailSenderService = $emailSenderService;
$this->entityManager = $entityManager;
$this->rateLimiter = $rateLimiter;
$this->appUrl = $appUrl;
}
/**
* Affiche le formulaire de demande de réinitialisation
*
* @Route("/forgot-password", name="app_forgot_password")
*/
public function forgotPassword(Request $request): Response
{
// Si l'utilisateur est déjà connecté, rediriger
if ($this->getUser()) {
return $this->redirectToRoute('suggestion.index');
}
// Rate limiting par IP
$clientIp = $request->getClientIp();
if (!$this->rateLimiter->canRequestFromIp($clientIp)) {
$this->addFlash('error', 'Trop de demandes depuis votre adresse IP. Veuillez réessayer dans 1 heure.');
return $this->render('security/forgot_password.html.twig', [
'form' => $this->createForm(ForgotPasswordFormType::class)->createView(),
]);
}
$form = $this->createForm(ForgotPasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData();
// Rate limiting par email
if (!$this->rateLimiter->canRequestForEmail($email)) {
$this->addFlash('error', 'Trop de demandes pour cet email. Veuillez attendre 5 minutes avant de réessayer.');
return $this->render('security/forgot_password.html.twig', [
'form' => $form->createView(),
]);
}
$user = $this->userRepository->findOneBy(['email' => $email]);
// Pour la sécurité, on ne révèle pas si l'email existe ou non
// On affiche toujours le même message
if ($user && $user->isIsActive()) {
// Invalider les anciens tokens pour cet utilisateur
$this->passwordResetService->invalidateExistingTokens($user);
// Générer un nouveau token
$token = $this->passwordResetService->generatePasswordResetToken($user);
// Générer l'URL de réinitialisation
$resetUrl = $this->generateUrl(
'app_reset_password',
['token' => $token],
UrlGeneratorInterface::ABSOLUTE_URL
);
// Envoyer l'email
try {
$this->emailSenderService->sendTemplatedEmail(
$user->getEmail(),
'Réinitialisation de votre mot de passe bambboo',
'emails/password_reset.html.twig',
[
'user' => $user,
'resetUrl' => $resetUrl,
'token' => $token,
]
);
} catch (\Exception $e) {
// Ne pas révéler l'erreur à l'utilisateur
// Logger l'erreur pour investigation
}
}
// Toujours afficher le même message pour ne pas révéler si l'email existe
$this->addFlash('success', 'Si cet email existe dans notre système, vous recevrez un lien de réinitialisation.');
return $this->redirectToRoute('app_login');
}
return $this->render('security/forgot_password.html.twig', [
'form' => $form->createView(),
]);
}
/**
* Affiche le formulaire de réinitialisation avec le token
*
* @Route("/reset-password/{token}", name="app_reset_password")
*/
public function resetPassword(Request $request, string $token): Response
{
// Si l'utilisateur est déjà connecté, rediriger
if ($this->getUser()) {
return $this->redirectToRoute('suggestion.index');
}
// Rate limiting sur les tentatives de validation
if (!$this->rateLimiter->canValidateToken($token)) {
$this->addFlash('error', 'Trop de tentatives avec ce lien. Veuillez demander un nouveau lien de réinitialisation.');
return $this->redirectToRoute('app_forgot_password');
}
// Valider le token
$user = $this->passwordResetService->validatePasswordResetToken($token);
if (!$user) {
$this->addFlash('error', 'Ce lien de réinitialisation est invalide ou a expiré.');
return $this->redirectToRoute('app_forgot_password');
}
$form = $this->createForm(ResetPasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$newPassword = $form->get('plainPassword')->getData();
// Réinitialiser le mot de passe
$this->passwordResetService->resetPassword($user, $newPassword);
// Réinitialiser les compteurs de rate limiting pour cet email
$this->rateLimiter->resetEmailCounter($user->getEmail());
$this->addFlash('success', 'Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter.');
return $this->redirectToRoute('app_login');
}
return $this->render('security/reset_password.html.twig', [
'form' => $form->createView(),
'token' => $token,
]);
}
}