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:
tracer 2022-11-01 16:19:33 +01:00
commit 6e9a07b62c
10 changed files with 291 additions and 238 deletions

View File

@ -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:

View File

@ -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.');
}
} }

View File

@ -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');
}
}

View File

@ -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');
}
} }

View File

@ -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);
}
}

View File

@ -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;
}
} }
// /** // /**

View File

@ -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');
} }
} }

View File

@ -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>&nbsp; <span class="fa fa-lg fa-fw fa-sign-out" aria-hidden="true"></span>&nbsp;
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>

View File

@ -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>

View File

@ -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 %}