* @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 { #[Route('/login', name: 'MineSeekerBundle_login')] public function login(AuthenticationUtils $authenticationUtils): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } return $this->render('Security/login.html.twig', [ 'last_username' => $authenticationUtils->getLastUsername(), 'error' => $authenticationUtils->getLastAuthenticationError(), ]); } #[Route('/logout', name: 'MineSeekerBundle_logout', methods: ['POST'])] public function logout(): void { // Intercepted by the security firewall — never executed. } #[Route('/register', name: 'MineSeekerBundle_register')] public function register( Request $request, UserPasswordHasherInterface $hasher, EntityManagerInterface $em, MailerInterface $mailer, ): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } $errors = []; if ($request->isMethod('POST')) { $username = trim((string) $request->request->get('_username', '')); $email = trim((string) $request->request->get('_email', '')); $password = (string) $request->request->get('_password', ''); $passwordConfirm = (string) $request->request->get('_password_confirm', ''); if (mb_strlen($username) < 3) { $errors['username'] = 'Username must be at least 3 characters.'; } elseif ($em->getRepository(User::class)->findOneBy(['username' => $username])) { $errors['username'] = 'This username is already taken.'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Please enter a valid email address.'; } elseif ($em->getRepository(User::class)->findOneBy(['email' => $email])) { $errors['email'] = 'This email address is already registered.'; } if (mb_strlen($password) < 6) { $errors['password'] = 'Password must be at least 6 characters.'; } elseif ($password !== $passwordConfirm) { $errors['password_confirm'] = 'Passwords do not match.'; } if (empty($errors)) { $token = bin2hex(random_bytes(32)); $user = new User() ->setUsername($username) ->setEmail($email) ->setIsVerified(false) ->setVerificationToken($token); $user->setPassword($hasher->hashPassword($user, $password)); $em->persist($user); $em->flush(); $activationUrl = $this->generateUrl( 'MineSeekerBundle_activate', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL, ); $mailer->send( new TemplatedEmail() ->from('noreply@mineseeker.ninja') ->to($email) ->subject('Activate your MineSeeker account') ->htmlTemplate('emails/activation.html.twig') ->context([ 'username' => $username, 'activation_url' => $activationUrl, ]) ); $this->addFlash('verify_email', $email); return $this->redirectToRoute('MineSeekerBundle_register'); } } return $this->render('Security/register.html.twig', [ 'errors' => $errors, 'last_username' => $request->request->get('_username', ''), 'last_email' => $request->request->get('_email', ''), ]); } #[Route('/forgot-password', name: 'MineSeekerBundle_forgot_password')] public function forgotPassword( Request $request, UserRepository $userRepository, EntityManagerInterface $em, MailerInterface $mailer, ): Response { if ($this->getUser()) { return $this->redirectToRoute('MineSeekerBundle_homepage'); } if ($request->isMethod('POST')) { $email = trim((string) $request->request->get('_email', '')); $user = $userRepository->findOneByEmail($email); if ($user && $user->isVerified()) { $token = bin2hex(random_bytes(32)); $user ->setResetToken($token) ->setResetTokenExpiresAt(new DateTime('+1 hour')); $em->flush(); $resetUrl = $this->generateUrl( 'MineSeekerBundle_reset_password', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL, ); $mailer->send( new TemplatedEmail() ->from('noreply@mineseeker.ninja') ->to($email) ->subject('Reset your MineSeeker password') ->htmlTemplate('emails/reset_password.html.twig') ->context([ 'username' => $user->getUsername(), 'reset_url' => $resetUrl, ]) ); } // Always show the same flash to prevent email enumeration $this->addFlash('reset_sent', $email); return $this->redirectToRoute('MineSeekerBundle_forgot_password'); } return $this->render('Security/forgot_password.html.twig'); } #[Route('/reset-password/{token}', name: 'MineSeekerBundle_reset_password')] public function resetPassword( string $token, Request $request, UserRepository $userRepository, EntityManagerInterface $em, UserPasswordHasherInterface $hasher, ): Response { $user = $userRepository->findOneByResetToken($token); if (!$user || $user->getResetTokenExpiresAt() < new DateTime()) { $this->addFlash('error', 'This password reset link is invalid or has expired.'); return $this->redirectToRoute('MineSeekerBundle_forgot_password'); } $errors = []; if ($request->isMethod('POST')) { $password = (string) $request->request->get('_password', ''); $passwordConfirm = (string) $request->request->get('_password_confirm', ''); if (mb_strlen($password) < 6) { $errors['password'] = 'Password must be at least 6 characters.'; } elseif ($password !== $passwordConfirm) { $errors['password_confirm'] = 'Passwords do not match.'; } if (empty($errors)) { $user ->setPassword($hasher->hashPassword($user, $password)) ->setResetToken(null) ->setResetTokenExpiresAt(null); $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', ['errors' => $errors]); } #[Route('/activate/{token}', name: 'MineSeekerBundle_activate')] public function activate(string $token, EntityManagerInterface $em): Response { $user = $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->setIsVerified(true)->setVerificationToken(null); $em->flush(); $this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!'); return $this->redirectToRoute('MineSeekerBundle_login'); } }