* @category Class * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License * @link www.splendidbear.org * @since 2026. 04. 11. */ #[AsController] class SecurityController extends AbstractController { public function __construct( private readonly EntityManagerInterface $em, private readonly RequestStack $requestStack, private readonly UserRepository $userRepository, private readonly UserPasswordHasherInterface $passwordHasher, private readonly AuthenticationUtils $authenticationUtils, private readonly SendActivationEmailService $activationEmail, private readonly SendPasswordResetEmailService $passwordResetEmail, private readonly SendUserActivationNotificationService $activationNotificationEmail, private readonly SendUserRegistrationNotificationService $registrationNotificationEmail, ) { } #[Route('/login', name: 'MineSeekerBundle_login')] public function login(): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } return $this->render('Security/login.html.twig', [ 'last_username' => $this->authenticationUtils->getLastUsername(), 'error' => $this->authenticationUtils->getLastAuthenticationError(), ]); } #[Route('/logout', name: 'MineSeekerBundle_logout', methods: ['POST'])] public function logout(): never { throw new LogicException('This action is intercepted by the security firewall.'); } #[Route('/register', name: 'MineSeekerBundle_register')] public function register(): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } $user = new User(); $form = $this->createForm(RegistrationFormType::class, $user); $form->handleRequest($this->requestStack->getCurrentRequest()); if ($form->isSubmitted() && $form->isValid()) { $token = bin2hex(random_bytes(32)); $user->isVerified = false; $user->verificationToken = $token; $user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData()); $this->em->persist($user); $this->em->flush(); $activationUrl = $this->generateUrl( 'MineSeekerBundle_activate', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL, ); /** Ensure HTTPS scheme in production */ if ($this->getParameter('kernel.environment') === 'prod') { $activationUrl = str_replace('http://', 'https://', $activationUrl); } $this->activationEmail->send($user, $activationUrl); $this->registrationNotificationEmail->send($user, new DateTime()); $this->addFlash('verify_email', $user->email); return $this->redirectToRoute('MineSeekerBundle_register'); } return $this->render('Security/register.html.twig', ['form' => $form]); } #[Route('/forgot-password', name: 'MineSeekerBundle_forgot_password')] public function forgotPassword(): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } $form = $this->createForm(ForgotPasswordFormType::class); $form->handleRequest($this->requestStack->getCurrentRequest()); if ($form->isSubmitted() && $form->isValid()) { $email = $form->get('email')->getData(); $user = $this->userRepository->findOneByEmail($email); if ($user && $user->isVerified) { $token = bin2hex(random_bytes(32)); $user->resetToken = $token; $user->resetTokenExpiresAt = new DateTime('+1 hour'); $this->em->flush(); $resetUrl = $this->generateUrl( 'MineSeekerBundle_reset_password', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL, ); /** Ensure HTTPS scheme in production */ if ($this->getParameter('kernel.environment') === 'prod') { $resetUrl = str_replace('http://', 'https://', $resetUrl); } $this->passwordResetEmail->send($email, $user->getUsername(), $resetUrl); } $this->addFlash('reset_sent', $email); return $this->redirectToRoute('MineSeekerBundle_forgot_password'); } return $this->render('Security/forgot_password.html.twig', ['form' => $form]); } #[Route('/reset-password/{token}', name: 'MineSeekerBundle_reset_password')] public function resetPassword(string $token): Response { $user = $this->userRepository->findOneByResetToken($token); if (!$user || $user->resetTokenExpiresAt < new DateTime()) { $this->addFlash('error', 'This password reset link is invalid or has expired.'); return $this->redirectToRoute('MineSeekerBundle_forgot_password'); } $form = $this->createForm(ResetPasswordFormType::class); $form->handleRequest($this->requestStack->getCurrentRequest()); if ($form->isSubmitted() && $form->isValid()) { $user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData()); $user->resetToken = null; $user->resetTokenExpiresAt = null; $this->em->flush(); $this->addFlash('success', 'Your password has been reset. You can now sign in.'); return $this->redirectToRoute('MineSeekerBundle_login'); } return $this->render('Security/reset_password.html.twig', ['form' => $form]); } #[Route('/activate/{token}', name: 'MineSeekerBundle_activate')] public function activate(string $token): Response { $user = $this->em->getRepository(User::class)->findOneBy(['verificationToken' => $token]); if (!$user) { $this->addFlash('error', 'This activation link is invalid or has already been used.'); return $this->redirectToRoute('MineSeekerBundle_login'); } $user->isVerified = true; $user->verificationToken = null; $this->em->flush(); $this->activationNotificationEmail->send($user, new DateTime()); $this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!'); return $this->redirectToRoute('MineSeekerBundle_login'); } }