Merge pull request 'merge_ResetPassword_to_Security' (#1) from merge_ResetPassword_to_Security into master
Reviewed-on: tracer/24unix.net.web#1
This commit is contained in:
commit
6e9a07b62c
|
@ -24,11 +24,8 @@ security:
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
custom_authenticator: App\Security\LoginFormAuthenticator
|
custom_authenticator: App\Security\LoginFormAuthenticator
|
||||||
|
|
||||||
##form_login:
|
|
||||||
# login_path: app_login
|
|
||||||
# check_path: app_login
|
|
||||||
logout:
|
logout:
|
||||||
path: app_logout
|
path: security_logout
|
||||||
switch_user: true
|
switch_user: true
|
||||||
|
|
||||||
remember_me:
|
remember_me:
|
||||||
|
|
|
@ -6,7 +6,6 @@ declare(strict_types=1);
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Repository\QuotesRepository;
|
use App\Repository\QuotesRepository;
|
||||||
use Exception;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
@ -27,13 +26,4 @@ class FrontendController extends AbstractController
|
||||||
'quote' => json_encode(value: $quote->getQuote())
|
'quote' => json_encode(value: $quote->getQuote())
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
#[Route(path: '/logout', name: 'app_logout')]
|
|
||||||
public function logout(): never
|
|
||||||
{
|
|
||||||
throw new Exception(message: 'Logout should never be reached.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Form\ChangePasswordFormType;
|
|
||||||
use App\Form\ResetPasswordRequestFormType;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
|
||||||
use Symfony\Component\Mime\Address;
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
|
|
||||||
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
|
|
||||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
|
||||||
|
|
||||||
class ResetPasswordController extends AbstractController
|
|
||||||
{
|
|
||||||
use ResetPasswordControllerTrait;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly ResetPasswordHelperInterface $resetPasswordHelper,
|
|
||||||
private readonly EntityManagerInterface $entityManager
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// empty body
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display & process form to request a password reset.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/security/forgot/password', name: 'security_forgot_password')]
|
|
||||||
public function request(Request $request, MailerInterface $mailer): Response
|
|
||||||
{
|
|
||||||
$form = $this->createForm(type: ResetPasswordRequestFormType::class);
|
|
||||||
$form->handleRequest(request: $request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
return $this->processSendingPasswordResetEmail(
|
|
||||||
formData: $form->get(name: 'account')->getData(),
|
|
||||||
mailer: $mailer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(view: '@default/security/forgot_password.html.twig', parameters: [
|
|
||||||
'requestForm' => $form->createView(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirmation page after a user has requested a password reset.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/security/recovery/mail/sent', name: 'security_recovery_mail_sent')]
|
|
||||||
public function checkEmail(): Response
|
|
||||||
{
|
|
||||||
// Generate a fake token if the user does not exist or someone hit this page directly.
|
|
||||||
// This prevents exposing whether a user was found with the given email address or username or not
|
|
||||||
if (null === ($resetToken = $this->getTokenObjectFromSession())) {
|
|
||||||
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(view: '@default/security/recovery_mail_sent.html.twig', parameters: [
|
|
||||||
'resetToken' => $resetToken,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates and process the reset URL that the user clicked in their email.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/security/recovery/reset/{token}', name: 'security_recovery_reset')]
|
|
||||||
public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response
|
|
||||||
{
|
|
||||||
if ($token) {
|
|
||||||
// We store the token in session and remove it from the URL, to avoid the URL being
|
|
||||||
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
|
|
||||||
$this->storeTokenInSession(token: $token);
|
|
||||||
|
|
||||||
return $this->redirectToRoute(route: 'security_recovery_reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $this->getTokenFromSession();
|
|
||||||
if (null === $token) {
|
|
||||||
throw $this->createNotFoundException(message: 'No reset password token found in the URL or in the session.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$user = $this->resetPasswordHelper->validateTokenAndFetchUser(fullToken: $token);
|
|
||||||
} catch (ResetPasswordExceptionInterface $e) {
|
|
||||||
$this->addFlash(type: 'reset_password_error', message: sprintf(
|
|
||||||
'%s - %s',
|
|
||||||
$translator->trans(id: ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, parameters: [], domain: 'ResetPasswordBundle'),
|
|
||||||
$translator->trans(id: $e->getReason(), parameters: [], domain: 'ResetPasswordBundle')
|
|
||||||
));
|
|
||||||
|
|
||||||
return $this->redirectToRoute(route: 'app_forgot_password_request');
|
|
||||||
}
|
|
||||||
|
|
||||||
// The token is valid; allow the user to change their password.
|
|
||||||
$form = $this->createForm(type: ChangePasswordFormType::class);
|
|
||||||
$form->handleRequest(request: $request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
// A password reset token should be used only once, remove it.
|
|
||||||
$this->resetPasswordHelper->removeResetRequest(fullToken: $token);
|
|
||||||
|
|
||||||
// Encode(hash) the plain password, and set it.
|
|
||||||
$encodedPassword = $passwordHasher->hashPassword(
|
|
||||||
user: $user,
|
|
||||||
plainPassword: $form->get(name: 'plainPassword')->getData()
|
|
||||||
);
|
|
||||||
|
|
||||||
$user->setPassword($encodedPassword);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
// The session is cleaned up after the password has been changed.
|
|
||||||
$this->cleanSessionAfterReset();
|
|
||||||
|
|
||||||
$this->addFlash(type: 'success', message: 'Your password has been changed.');
|
|
||||||
|
|
||||||
return $this->redirectToRoute(route: 'app_main');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(view: '@default/security/reset_password.html.twig', parameters: [
|
|
||||||
'resetForm' => $form->createView(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function processSendingPasswordResetEmail(string $formData, MailerInterface $mailer): RedirectResponse
|
|
||||||
{
|
|
||||||
$user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
|
|
||||||
'email' => $formData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
$user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
|
|
||||||
'username' => $formData,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not reveal whether a user account was found or not.
|
|
||||||
// if (!$user) {
|
|
||||||
// return $this->redirectToRoute(route: 'app_check_email');
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
|
||||||
$resetToken = $this->resetPasswordHelper->generateResetToken(user: $user);
|
|
||||||
} catch (ResetPasswordExceptionInterface $e) {
|
|
||||||
$this->addFlash(type: 'reset_password_error', message: sprintf(
|
|
||||||
'%s - %s',
|
|
||||||
ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE,
|
|
||||||
$e->getReason()
|
|
||||||
));
|
|
||||||
|
|
||||||
return $this->redirectToRoute(route: 'security_forgot_password');
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = (new TemplatedEmail())
|
|
||||||
->from(new Address(address: 'tracer@24unix.net', name: '24unix.net'))
|
|
||||||
->to($user->getEmail())
|
|
||||||
->subject(subject: 'Your password reset request')
|
|
||||||
->htmlTemplate(template: '@default/security/mail/recovery.html.twig')
|
|
||||||
->context(context: [
|
|
||||||
'resetToken' => $resetToken,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$mailer->send(message: $email);
|
|
||||||
|
|
||||||
// Store the token object in session for retrieval in check-email route.
|
|
||||||
$this->setTokenObjectInSession(token: $resetToken);
|
|
||||||
|
|
||||||
return $this->redirectToRoute(route: 'security_recovery_mail_sent');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,31 +2,44 @@
|
||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Form\LoginFormType;
|
use App\Entity\User;
|
||||||
|
use App\Form\ChangePasswordFormType;
|
||||||
use App\Form\RegistrationFormType;
|
use App\Form\RegistrationFormType;
|
||||||
|
use App\Form\ResetPasswordRequestFormType;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Security\EmailVerifier;
|
use App\Security\EmailVerifier;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Exception;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Mime\Address;
|
use Symfony\Component\Mime\Address;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
|
||||||
|
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
|
||||||
|
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||||
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
|
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
|
||||||
|
|
||||||
class SecurityController extends AbstractController
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
|
use ResetPasswordControllerTrait;
|
||||||
|
|
||||||
public function __construct(private readonly EmailVerifier $emailVerifier)
|
public function __construct(private readonly EmailVerifier $emailVerifier,
|
||||||
|
private readonly ResetPasswordHelperInterface $resetPasswordHelper,
|
||||||
|
private readonly EntityManagerInterface $entityManager
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// empty body
|
// empty body
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/login', name: 'app_login')]
|
#[Route(path: '/security/login', name: 'security_login')]
|
||||||
public function index(AuthenticationUtils $authenticationUtils): Response
|
public function index(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
// get the login error if there is one
|
// get the login error if there is one
|
||||||
|
@ -43,8 +56,16 @@ class SecurityController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
#[Route(path: '/security/logout', name: 'security_logout')]
|
||||||
|
public function logout(): never
|
||||||
|
{
|
||||||
|
throw new Exception(message: 'Logout should never be reached.');
|
||||||
|
}
|
||||||
|
|
||||||
#[Route(path: '/register', name: 'app_register')]
|
#[Route(path: '/security/register', name: 'security_register')]
|
||||||
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
|
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
|
||||||
{
|
{
|
||||||
$form = $this->createForm(type: RegistrationFormType::class);
|
$form = $this->createForm(type: RegistrationFormType::class);
|
||||||
|
@ -67,15 +88,15 @@ class SecurityController extends AbstractController
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|
||||||
// generate a signed url and email it to the user
|
// generate a signed url and email it to the user
|
||||||
$this->emailVerifier->sendEmailConfirmation(verifyEmailRouteName: 'app_verify_email', user: $user,
|
$this->emailVerifier->sendEmailConfirmation(verifyEmailRouteName: 'security_verify_email', user: $user,
|
||||||
email: (new TemplatedEmail())
|
email: (new TemplatedEmail())
|
||||||
->from(new Address(address: 'info@24unix.net', name: '24unix.net'))
|
->from(new Address(address: 'info@24unix.net', name: '24unix.net'))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->subject(subject: 'Please Confirm your Email')
|
->subject(subject: 'Please Confirm your Email')
|
||||||
->htmlTemplate(template: '@default/security/mail/registration.html.twig')
|
->htmlTemplate(template: '@default/security/mail/registration.html.twig')
|
||||||
->context(context: [
|
->context(context: [
|
||||||
'username' => $user->getUsername()
|
'username' => $user->getUsername()
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->render(view: '@default/security/registration_finished.html.twig');
|
return $this->render(view: '@default/security/registration_finished.html.twig');
|
||||||
|
@ -86,7 +107,7 @@ class SecurityController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/verify/email', name: 'app_verify_email')]
|
#[Route(path: '/security/verify/email', name: 'security_verify_email')]
|
||||||
public function verifyUserEmail(Request $request, TranslatorInterface $translator, UserRepository $userRepository): Response
|
public function verifyUserEmail(Request $request, TranslatorInterface $translator, UserRepository $userRepository): Response
|
||||||
{
|
{
|
||||||
$id = $request->get(key: 'id');
|
$id = $request->get(key: 'id');
|
||||||
|
@ -115,4 +136,152 @@ class SecurityController extends AbstractController
|
||||||
return $this->redirectToRoute(route: 'app_main');
|
return $this->redirectToRoute(route: 'app_main');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display & process form to request a password reset.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/security/forgot/password', name: 'security_forgot_password')]
|
||||||
|
public function request(Request $request, MailerInterface $mailer): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(type: ResetPasswordRequestFormType::class);
|
||||||
|
$form->handleRequest(request: $request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
return $this->processSendingPasswordResetEmail(
|
||||||
|
formData: $form->get(name: 'account')->getData(),
|
||||||
|
mailer: $mailer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render(view: '@default/security/forgot_password.html.twig', parameters: [
|
||||||
|
'requestForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmation page after a user has requested a password reset.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/security/recovery/mail/sent', name: 'security_recovery_mail_sent')]
|
||||||
|
public function checkEmail(): Response
|
||||||
|
{
|
||||||
|
// Generate a fake token if the user does not exist or someone hit this page directly.
|
||||||
|
// This prevents exposing whether a user was found with the given email address or username or not
|
||||||
|
if (null === ($resetToken = $this->getTokenObjectFromSession())) {
|
||||||
|
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render(view: '@default/security/recovery_mail_sent.html.twig', parameters: [
|
||||||
|
'resetToken' => $resetToken,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and process the reset URL that the user clicked in their email.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/security/recovery/reset/{token}', name: 'security_recovery_reset')]
|
||||||
|
public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response
|
||||||
|
{
|
||||||
|
if ($token) {
|
||||||
|
// We store the token in session and remove it from the URL, to avoid the URL being
|
||||||
|
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
|
||||||
|
$this->storeTokenInSession(token: $token);
|
||||||
|
|
||||||
|
return $this->redirectToRoute(route: 'security_recovery_reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->getTokenFromSession();
|
||||||
|
if (null === $token) {
|
||||||
|
throw $this->createNotFoundException(message: 'No reset password token found in the URL or in the session.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user = $this->resetPasswordHelper->validateTokenAndFetchUser(fullToken: $token);
|
||||||
|
} catch (ResetPasswordExceptionInterface $e) {
|
||||||
|
$this->addFlash(type: 'reset_password_error', message: sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
$translator->trans(id: ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, parameters: [], domain: 'ResetPasswordBundle'),
|
||||||
|
$translator->trans(id: $e->getReason(), parameters: [], domain: 'ResetPasswordBundle')
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this->redirectToRoute(route: 'app_forgot_password_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The token is valid; allow the user to change their password.
|
||||||
|
$form = $this->createForm(type: ChangePasswordFormType::class);
|
||||||
|
$form->handleRequest(request: $request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
// A password reset token should be used only once, remove it.
|
||||||
|
$this->resetPasswordHelper->removeResetRequest(fullToken: $token);
|
||||||
|
|
||||||
|
// Encode(hash) the plain password, and set it.
|
||||||
|
$encodedPassword = $passwordHasher->hashPassword(
|
||||||
|
user: $user,
|
||||||
|
plainPassword: $form->get(name: 'plainPassword')->getData()
|
||||||
|
);
|
||||||
|
|
||||||
|
$user->setPassword($encodedPassword);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// The session is cleaned up after the password has been changed.
|
||||||
|
$this->cleanSessionAfterReset();
|
||||||
|
|
||||||
|
$this->addFlash(type: 'success', message: 'Your password has been changed.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute(route: 'app_main');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render(view: '@default/security/reset_password.html.twig', parameters: [
|
||||||
|
'resetForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processSendingPasswordResetEmail(string $formData, MailerInterface $mailer): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
|
||||||
|
'email' => $formData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$user = $this->entityManager->getRepository(entityName: User::class)->findOneBy(criteria: [
|
||||||
|
'username' => $formData,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not reveal whether a user account was found or not.
|
||||||
|
// if (!$user) {
|
||||||
|
// return $this->redirectToRoute(route: 'app_check_email');
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resetToken = $this->resetPasswordHelper->generateResetToken(user: $user);
|
||||||
|
} catch (ResetPasswordExceptionInterface $e) {
|
||||||
|
$this->addFlash(type: 'reset_password_error', message: sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE,
|
||||||
|
$e->getReason()
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this->redirectToRoute(route: 'security_forgot_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = (new TemplatedEmail())
|
||||||
|
->from(new Address(address: 'tracer@24unix.net', name: '24unix.net'))
|
||||||
|
->to($user->getEmail())
|
||||||
|
->subject(subject: 'Your password reset request')
|
||||||
|
->htmlTemplate(template: '@default/security/mail/recovery.html.twig')
|
||||||
|
->context(context: [
|
||||||
|
'resetToken' => $resetToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mailer->send(message: $email);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
$this->addFlash(type: 'error', message: $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the token object in session for retrieval in check-email route.
|
||||||
|
$this->setTokenObjectInSession(token: $resetToken);
|
||||||
|
|
||||||
|
return $this->redirectToRoute(route: 'security_recovery_mail_sent');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use function is_array;
|
||||||
|
|
||||||
|
class UserNotVerifiedException extends AuthenticationException
|
||||||
|
{
|
||||||
|
private ?string $identifier = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getMessageKey(): string
|
||||||
|
{
|
||||||
|
return 'User is not verified.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user identifier (e.g. username or email address).
|
||||||
|
*/
|
||||||
|
public function getUserIdentifier(): ?string
|
||||||
|
{
|
||||||
|
return $this->identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user identifier (e.g. username or email address).
|
||||||
|
*/
|
||||||
|
public function setUserIdentifier(string $identifier): void
|
||||||
|
{
|
||||||
|
$this->identifier = $identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
#[ArrayShape(shape: ['{{ username }}' => "null|string", '{{ user_identifier }}' => "null|string"])]
|
||||||
|
public function getMessageData(): array
|
||||||
|
{
|
||||||
|
return ['{{ username }}' => $this->identifier, '{{ user_identifier }}' => $this->identifier];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
return [$this->identifier, parent::__serialize()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __unserialize(array $data): void
|
||||||
|
{
|
||||||
|
[$this->identifier, $parentData] = $data;
|
||||||
|
$parentData = is_array(value: $parentData) ? $parentData : unserialize(data: $parentData);
|
||||||
|
parent::__unserialize(data: $parentData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,10 @@ namespace App\Repository;
|
||||||
|
|
||||||
use App\Entity\Quotes;
|
use App\Entity\Quotes;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\DBAL\Query\QueryBuilder;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method Quotes|null find($id, $lockMode = null, $lockVersion = null)
|
* @method Quotes|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
@ -21,7 +24,7 @@ class QuotesRepository extends ServiceEntityRepository
|
||||||
|
|
||||||
public function add(Quotes $entity, bool $flush = true): void
|
public function add(Quotes $entity, bool $flush = true): void
|
||||||
{
|
{
|
||||||
$this->_em->persist($entity);
|
$this->_em->persist(entity: $entity);
|
||||||
if ($flush) {
|
if ($flush) {
|
||||||
$this->_em->flush();
|
$this->_em->flush();
|
||||||
}
|
}
|
||||||
|
@ -29,31 +32,40 @@ class QuotesRepository extends ServiceEntityRepository
|
||||||
|
|
||||||
public function remove(Quotes $entity, bool $flush = true): void
|
public function remove(Quotes $entity, bool $flush = true): void
|
||||||
{
|
{
|
||||||
$this->_em->remove($entity);
|
$this->_em->remove(entity: $entity);
|
||||||
if ($flush) {
|
if ($flush) {
|
||||||
$this->_em->flush();
|
$this->_em->flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function findOneRandom(): ?QueryBuilder
|
||||||
* @return float|int|mixed|string|null
|
|
||||||
*
|
|
||||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
|
||||||
*/
|
|
||||||
public function findOneRandom(): mixed
|
|
||||||
{
|
{
|
||||||
$idLimits = $this->createQueryBuilder(alias: 'q')
|
try {
|
||||||
->select('MIN(q.id)', 'MAX(q.id)')
|
$idLimits = $this->createQueryBuilder(alias: 'q')
|
||||||
->getQuery()
|
->select('MIN(q.id)', 'MAX(q.id)')
|
||||||
->getOneOrNullResult();
|
->getQuery()
|
||||||
$randomPossibleId = random_int(min: $idLimits[1], max: $idLimits[2]);
|
->getOneOrNullResult();
|
||||||
|
} catch (NonUniqueResultException) {
|
||||||
|
$idLimits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->createQueryBuilder(alias: 'q')
|
try {
|
||||||
->where(predicates: 'q.id >= :random_id')
|
$randomPossibleId = random_int(min: $idLimits[1], max: $idLimits[2]);
|
||||||
->setParameter(key: 'random_id', value: $randomPossibleId)
|
} catch(Exception) {
|
||||||
->setMaxResults(maxResults: 1)
|
$randomPossibleId = 0; // return first, if any
|
||||||
->getQuery()
|
}
|
||||||
->getOneOrNullResult();
|
|
||||||
|
try {
|
||||||
|
return $this->createQueryBuilder(alias: 'q')
|
||||||
|
->where(predicates: 'q.id >= :random_id')
|
||||||
|
->setParameter(key: 'random_id', value: $randomPossibleId)
|
||||||
|
->setMaxResults(maxResults: 1)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
} catch (NonUniqueResultException) {
|
||||||
|
// max results is 1
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
|
|
|
@ -2,28 +2,21 @@
|
||||||
|
|
||||||
namespace App\Security;
|
namespace App\Security;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Exception\UserNotVerifiedException;
|
||||||
use App\Form\LoginFormType;
|
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\Form\FormBuilder;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
|
||||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
|
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
|
||||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,7 +34,7 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
|
||||||
public function authenticate(Request $request): Passport
|
public function authenticate(Request $request): Passport
|
||||||
{
|
{
|
||||||
$username = $request->request->get(key: 'username');
|
$username = $request->request->get(key: 'username');
|
||||||
$password = $request->request->get(key: 'password');
|
$password = $request->request->get(key: 'password');
|
||||||
$csrfToken = $request->request->get(key: '_csrf_token');
|
$csrfToken = $request->request->get(key: '_csrf_token');
|
||||||
|
|
||||||
$request->getSession()->set(name: Security::LAST_USERNAME, value: $username);
|
$request->getSession()->set(name: Security::LAST_USERNAME, value: $username);
|
||||||
|
@ -58,6 +51,10 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
|
||||||
throw new UserNotFoundException();
|
throw new UserNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$user->isVerified()) {
|
||||||
|
throw new UserNotVerifiedException();
|
||||||
|
}
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -82,7 +79,7 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
|
||||||
|
|
||||||
protected function getLoginUrl(Request $request): string
|
protected function getLoginUrl(Request $request): string
|
||||||
{
|
{
|
||||||
return $this->router->generate(name: 'app_login');
|
return $this->router->generate(name: 'security_login');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="dropdown-item" href="{{ path('app_logout') }}">
|
<a class="dropdown-item" href="{{ path('security_logout') }}">
|
||||||
<span class="fa fa-lg fa-fw fa-sign-out" aria-hidden="true"></span>
|
<span class="fa fa-lg fa-fw fa-sign-out" aria-hidden="true"></span>
|
||||||
Logout
|
Logout
|
||||||
</a>
|
</a>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="btn btn-primary button-login" href="{{ path('app_login') }}" role="button"
|
<a class="btn btn-primary button-login" href="{{ path('security_login') }}" role="button"
|
||||||
id="buttonLogin">
|
id="buttonLogin">
|
||||||
<span class="fa fa-sign-in fa-lg fa-fw" aria-hidden="true"></span>Log In
|
<span class="fa fa-sign-in fa-lg fa-fw" aria-hidden="true"></span>Log In
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -27,14 +27,18 @@
|
||||||
<label for="inputPassword">Password</label>
|
<label for="inputPassword">Password</label>
|
||||||
<input type="password" name="password" id="inputPassword" class="form-control" required>
|
<input type="password" name="password" id="inputPassword" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mx-3">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="_remember_me" class="form-check-input">Remember me
|
<input type="checkbox" name="_remember_me" class="form-check-input">Remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-lg btn-primary float-end" type="submit">
|
<div>
|
||||||
Sign in
|
<a href="{{ path('security_forgot_password') }}">Forgot password?</a>
|
||||||
</button>
|
<button class="btn btn-primary float-end" type="submit">
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="{{ path('security_register') }}">Need an account? Register now.</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
|
|
||||||
{{ form_start(resetForm) }}
|
{{ form_start(resetForm) }}
|
||||||
{{ form_row(resetForm.plainPassword) }}
|
{{ form_row(resetForm.plainPassword) }}
|
||||||
<button class="btn btn-primary">Reset password</button>
|
<button class="btn btn-primary">Update password</button>
|
||||||
{{ form_end(resetForm) }}
|
{{ form_end(resetForm) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue