Private
Public Access
1
0

chg: dev: refactor the SecurityController #7

This commit is contained in:
2026-04-20 12:13:08 +02:00
parent f493f94368
commit 6be0d52fb7
5 changed files with 333 additions and 90 deletions

View File

@@ -15,16 +15,17 @@ use App\Form\ForgotPasswordFormType;
use App\Form\RegistrationFormType; use App\Form\RegistrationFormType;
use App\Form\ResetPasswordFormType; use App\Form\ResetPasswordFormType;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Service\Email\SendActivationEmailService;
use App\Service\Email\SendPasswordResetEmailService;
use App\Service\Email\SendUserActivationNotificationService;
use App\Service\Email\SendUserRegistrationNotificationService;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use LogicException; use LogicException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -44,21 +45,28 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
public function __construct( public function __construct(
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')] private readonly EntityManagerInterface $em,
private readonly string $appContactMailAddress, 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')] #[Route('/login', name: 'MineSeekerBundle_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(): Response
{ {
if ($this->getUser()) { if ($this->getUser()) {
return $this->redirectToRoute('MineSeekerBundle_homepage'); return $this->redirectToRoute('MineSeekerBundle_homepage');
} }
return $this->render('Security/login.html.twig', [ return $this->render('Security/login.html.twig', [
'last_username' => $authenticationUtils->getLastUsername(), 'last_username' => $this->authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(), 'error' => $this->authenticationUtils->getLastAuthenticationError(),
]); ]);
} }
@@ -69,29 +77,25 @@ class SecurityController extends AbstractController
} }
#[Route('/register', name: 'MineSeekerBundle_register')] #[Route('/register', name: 'MineSeekerBundle_register')]
public function register( public function register(): Response
Request $request, {
UserPasswordHasherInterface $hasher,
EntityManagerInterface $em,
MailerInterface $mailer,
): Response {
if ($this->getUser()) { if ($this->getUser()) {
return $this->redirectToRoute('MineSeekerBundle_homepage'); return $this->redirectToRoute('MineSeekerBundle_homepage');
} }
$user = new User(); $user = new User();
$form = $this->createForm(RegistrationFormType::class, $user); $form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request); $form->handleRequest($this->requestStack->getCurrentRequest());
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$token = bin2hex(random_bytes(32)); $token = bin2hex(random_bytes(32));
$user->isVerified = false; $user->isVerified = false;
$user->verificationToken = $token; $user->verificationToken = $token;
$user->password = $hasher->hashPassword($user, $form->get('plainPassword')->getData()); $user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData());
$em->persist($user); $this->em->persist($user);
$em->flush(); $this->em->flush();
$activationUrl = $this->generateUrl( $activationUrl = $this->generateUrl(
'MineSeekerBundle_activate', 'MineSeekerBundle_activate',
@@ -104,30 +108,8 @@ class SecurityController extends AbstractController
$activationUrl = str_replace('http://', 'https://', $activationUrl); $activationUrl = str_replace('http://', 'https://', $activationUrl);
} }
$mailer->send( $this->activationEmail->send($user, $activationUrl);
new TemplatedEmail() $this->registrationNotificationEmail->send($user, new DateTime());
->from('noreply@mineseeker.hu')
->to($user->email)
->subject('Activate your MineSeeker account')
->htmlTemplate('emails/activation.html.twig')
->context([
'username' => $user->getUsername(),
'activation_url' => $activationUrl,
])
);
/** Send admin notification about new user registration */
$mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject('🎉 New User Registration: ' . $user->getUsername())
->htmlTemplate('emails/user_registration_notification.html.twig')
->context([
'user' => $user,
'registeredAt' => new DateTime(),
])
);
$this->addFlash('verify_email', $user->email); $this->addFlash('verify_email', $user->email);
@@ -138,28 +120,24 @@ class SecurityController extends AbstractController
} }
#[Route('/forgot-password', name: 'MineSeekerBundle_forgot_password')] #[Route('/forgot-password', name: 'MineSeekerBundle_forgot_password')]
public function forgotPassword( public function forgotPassword(): Response
Request $request, {
UserRepository $userRepository,
EntityManagerInterface $em,
MailerInterface $mailer,
): Response {
if ($this->getUser()) { if ($this->getUser()) {
return $this->redirectToRoute('MineSeekerBundle_homepage'); return $this->redirectToRoute('MineSeekerBundle_homepage');
} }
$form = $this->createForm(ForgotPasswordFormType::class); $form = $this->createForm(ForgotPasswordFormType::class);
$form->handleRequest($request); $form->handleRequest($this->requestStack->getCurrentRequest());
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData(); $email = $form->get('email')->getData();
$user = $userRepository->findOneByEmail($email); $user = $this->userRepository->findOneByEmail($email);
if ($user && $user->isVerified) { if ($user && $user->isVerified) {
$token = bin2hex(random_bytes(32)); $token = bin2hex(random_bytes(32));
$user->resetToken = $token; $user->resetToken = $token;
$user->resetTokenExpiresAt = new DateTime('+1 hour'); $user->resetTokenExpiresAt = new DateTime('+1 hour');
$em->flush(); $this->em->flush();
$resetUrl = $this->generateUrl( $resetUrl = $this->generateUrl(
'MineSeekerBundle_reset_password', 'MineSeekerBundle_reset_password',
@@ -172,20 +150,9 @@ class SecurityController extends AbstractController
$resetUrl = str_replace('http://', 'https://', $resetUrl); $resetUrl = str_replace('http://', 'https://', $resetUrl);
} }
$mailer->send( $this->passwordResetEmail->send($email, $user->getUsername(), $resetUrl);
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->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); $this->addFlash('reset_sent', $email);
return $this->redirectToRoute('MineSeekerBundle_forgot_password'); return $this->redirectToRoute('MineSeekerBundle_forgot_password');
@@ -195,14 +162,9 @@ class SecurityController extends AbstractController
} }
#[Route('/reset-password/{token}', name: 'MineSeekerBundle_reset_password')] #[Route('/reset-password/{token}', name: 'MineSeekerBundle_reset_password')]
public function resetPassword( public function resetPassword(string $token): Response
string $token, {
Request $request, $user = $this->userRepository->findOneByResetToken($token);
UserRepository $userRepository,
EntityManagerInterface $em,
UserPasswordHasherInterface $hasher,
): Response {
$user = $userRepository->findOneByResetToken($token);
if (!$user || $user->resetTokenExpiresAt < new DateTime()) { if (!$user || $user->resetTokenExpiresAt < new DateTime()) {
$this->addFlash('error', 'This password reset link is invalid or has expired.'); $this->addFlash('error', 'This password reset link is invalid or has expired.');
@@ -210,13 +172,14 @@ class SecurityController extends AbstractController
} }
$form = $this->createForm(ResetPasswordFormType::class); $form = $this->createForm(ResetPasswordFormType::class);
$form->handleRequest($request); $form->handleRequest($this->requestStack->getCurrentRequest());
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$user->password = $hasher->hashPassword($user, $form->get('plainPassword')->getData()); $user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData());
$user->resetToken = null; $user->resetToken = null;
$user->resetTokenExpiresAt = null; $user->resetTokenExpiresAt = null;
$em->flush();
$this->em->flush();
$this->addFlash('success', 'Your password has been reset. You can now sign in.'); $this->addFlash('success', 'Your password has been reset. You can now sign in.');
@@ -227,9 +190,9 @@ class SecurityController extends AbstractController
} }
#[Route('/activate/{token}', name: 'MineSeekerBundle_activate')] #[Route('/activate/{token}', name: 'MineSeekerBundle_activate')]
public function activate(string $token, EntityManagerInterface $em, MailerInterface $mailer): Response public function activate(string $token): Response
{ {
$user = $em->getRepository(User::class)->findOneBy(['verificationToken' => $token]); $user = $this->em->getRepository(User::class)->findOneBy(['verificationToken' => $token]);
if (!$user) { if (!$user) {
$this->addFlash('error', 'This activation link is invalid or has already been used.'); $this->addFlash('error', 'This activation link is invalid or has already been used.');
@@ -238,20 +201,9 @@ class SecurityController extends AbstractController
$user->isVerified = true; $user->isVerified = true;
$user->verificationToken = null; $user->verificationToken = null;
$em->flush(); $this->em->flush();
/** Send admin notification about account activation */ $this->activationNotificationEmail->send($user, new DateTime());
$mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject('✅ User Account Activated: ' . $user->getUsername())
->htmlTemplate('emails/user_activation_notification.html.twig')
->context([
'user' => $user,
'activatedAt' => new DateTime(),
])
);
$this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!'); $this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!');

View File

@@ -0,0 +1,71 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service\Email;
use App\Entity\User;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
/**
* Class SendActivationEmailService
*
* @package App\Service\Email
* @author Lang <https://www.splendidbear.org>
* @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. 20.
*/
readonly final class SendActivationEmailService
{
public function __construct(
private LoggerInterface $logger,
private MailerInterface $mailer,
) {
}
public function send(User $user, string $activationUrl): void
{
try {
$this->mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($user->email)
->subject('Activate your MineSeeker account')
->htmlTemplate('emails/activation.html.twig')
->context([
'username' => $user->getUsername(),
'activation_url' => $activationUrl,
])
);
} catch (TransportExceptionInterface|Exception $e) {
$this->logger->error("Failed to send activation email: {$e->getMessage()}", [
'exception' => $e,
'user' => $user->getUsername(),
]);
throw new RuntimeException("Failed to send activation email: {$e->getMessage()}");
}
}
}

View File

@@ -0,0 +1,70 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service\Email;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
/**
* Class SendPasswordResetEmailService
*
* @package App\Service\Email
* @author Lang <https://www.splendidbear.org>
* @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. 20.
*/
readonly final class SendPasswordResetEmailService
{
public function __construct(
private LoggerInterface $logger,
private MailerInterface $mailer,
) {
}
public function send(string $email, string $username, string $resetUrl): void
{
try {
$this->mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($email)
->subject('Reset your MineSeeker password')
->htmlTemplate('emails/reset_password.html.twig')
->context([
'username' => $username,
'reset_url' => $resetUrl,
])
);
} catch (TransportExceptionInterface|Exception $e) {
$this->logger->error("Failed to send password reset email: {$e->getMessage()}", [
'exception' => $e,
'email' => $email,
]);
throw new RuntimeException("Failed to send password reset email: {$e->getMessage()}");
}
}
}

View File

@@ -0,0 +1,75 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service\Email;
use App\Entity\User;
use DateTime;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
/**
* Class SendUserActivationNotificationService
*
* @package App\Service\Email
* @author Lang <https://www.splendidbear.org>
* @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. 20.
*/
readonly final class SendUserActivationNotificationService
{
public function __construct(
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')]
private string $appContactMailAddress,
private LoggerInterface $logger,
private MailerInterface $mailer,
) {
}
public function send(User $user, DateTime $activatedAt): void
{
try {
$this->mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject("✅ User Account Activated: {$user->getUsername()}")
->htmlTemplate('emails/user_activation_notification.html.twig')
->context([
'user' => $user,
'activatedAt' => $activatedAt,
])
);
} catch (TransportExceptionInterface|Exception $e) {
$this->logger->error("Failed to send user activation notification: {$e->getMessage()}", [
'exception' => $e,
'user' => $user->getUsername(),
]);
throw new RuntimeException("Failed to send user activation notification: {$e->getMessage()}");
}
}
}

View File

@@ -0,0 +1,75 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service\Email;
use App\Entity\User;
use DateTime;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
/**
* Class SendUserRegistrationNotificationService
*
* @package App\Service\Email
* @author Lang <https://www.splendidbear.org>
* @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. 20.
*/
readonly final class SendUserRegistrationNotificationService
{
public function __construct(
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')]
private string $appContactMailAddress,
private LoggerInterface $logger,
private MailerInterface $mailer,
) {
}
public function send(User $user, DateTime $registeredAt): void
{
try {
$this->mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject("🎉 New User Registration: {$user->getUsername()}")
->htmlTemplate('emails/user_registration_notification.html.twig')
->context([
'user' => $user,
'registeredAt' => $registeredAt,
])
);
} catch (TransportExceptionInterface|Exception $e) {
$this->logger->error("Failed to send user registration notification: {$e->getMessage()}", [
'exception' => $e,
'user' => $user->getUsername(),
]);
throw new RuntimeException("Failed to send user registration notification: {$e->getMessage()}");
}
}
}