<?php
namespace Boldr\Cms\UsersBundle\Controller;
use Boldr\Cms\UsersBundle\Form\{ ForgotPasswordType, ResetPasswordType };
use Boldr\Cms\UsersBundle\Entity\User;
use Boldr\Cms\UsersBundle\EmailFactory;
use Boldr\Cms\UsersBundle\Event\UserPasswordChangedEvent;
use Symfony\Component\HttpFoundation\{ Request, Response };
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Extension\Core\Type\{ FormType, TextType, PasswordType, CheckboxType };
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Psr\EventDispatcher\EventDispatcherInterface;
use DateTimeImmutable;
use DateInterval;
class SecurityController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
MailerInterface::class,
AuthenticationUtils::class,
UserPasswordHasherInterface::class,
EmailFactory::class,
TranslatorInterface::class,
EventDispatcherInterface::class
]);
}
/**
* @Route({
* "nl": "/inloggen",
* "de": "/anmelden",
* "en": "/login"
* }, name="boldr_users_login")
*/
public function login(Request $request): Response
{
if ($this->getUser())
{
return $this->redirectToRoute('cms_home');
}
$form = $this->get('form.factory')->createNamedBuilder('', FormType::class, null, [
'translation_domain' => 'BoldrUsersBundle',
'csrf_field_name' => '_csrf_token',
'csrf_token_id' => 'authenticate'
])
->add('_username', TextType::class, [
'label' => 'username',
'attr' => [
'autofocus' => 'autofocus'
]
])
->add('_password', PasswordType::class, [
'label' => 'password'
])
->add('_remember_me', CheckboxType::class, [
'label' => 'remember_me',
'required' => false
])
->getForm();
$authenticationUtils = $this->get(AuthenticationUtils::class);
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
$form->get('_username')->setData($lastUsername);
if ($error)
{
$form->addError(new FormError($this->get(TranslatorInterface::class)->trans($error->getMessageKey(), $error->getMessageData(), 'security')));
}
return $this->render('@BoldrUsers/login.html.twig', [
'error' => $error,
'form' => $form->createView()
]);
}
/**
* @Route({
* "nl": "/uitloggen",
* "de": "/abmelden",
* "en": "/logout"
* }, name="boldr_users_logout")
*/
public function logout()
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
/**
* @Route({
* "en": "/forgot-password",
* "nl": "/wachtwoord-vergeten",
* "de": "/passwort-vergessen"
* }, name="boldr_users_forgot_password")
* @Route("/boldr-api/boldr-users/forgot-password", name="boldr_users_api_forgot_password")
*/
public function forgotPassword(Request $request)
{
if ($this->getUser() !== null)
{
return $this->redirectToRoute('cms_home');
}
$form = $this->createForm(ForgotPasswordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$userRepository = $this->getDoctrine()->getRepository(User::class);
$user = $userRepository->createQueryBuilder('u')
->andWhere('u.username = ?1 OR u.emailAddress = ?1')
->setMaxResults(1)
->setParameter(1, $form['usernameOrEmailAddress']->getData())
->getQuery()
->getOneOrNullResult();
if ($user !== null)
{
if ($user->getForgotPasswordToken() === null || $user->getTimeForgotPasswordTokenExpires() < new DateTimeImmutable)
{
$forgotPasswordToken = bin2hex(random_bytes(32));
$timeForgotPasswordTokenExpires = (new DateTimeImmutable)->add(new DateInterval('PT24H'));
$user->setForgotPasswordToken($forgotPasswordToken);
$user->setTimeForgotPasswordTokenExpires($timeForgotPasswordTokenExpires);
$this->getDoctrine()->getManager()->flush();
}
$email = $this->get(EmailFactory::class)->createForgotPasswordEmail($user);
try
{
$this->get(MailerInterface::class)->send($email);
}
catch (\Exception $ex)
{
// could not send
}
}
if (in_array('application/json', $request->getAcceptableContentTypes()))
{
return $this->json(['success' => true]);
}
return $this->render('@BoldrUsers/password-link-sent.html.twig');
}
return $this->render('@BoldrUsers/forgot-password.html.twig', [
'form' => $form->createView()
]);
}
/**
* @Route({
* "en": "/reset-password/{token}",
* "nl": "/wachtwoord-opnieuw-instellen/{token}",
* "de": "/passwort-zurucksetzen/{token}"
* }, name="boldr_users_reset_password")
* @Route("/boldr-api/boldr-users/reset-password", name="boldr_users_api_reset_password", methods={"POST"})
*/
public function resetPassword(Request $request, ?string $token = null)
{
$isJsonRequest = in_array('application/json', $request->getAcceptableContentTypes());
if ($token === null)
{
if (!$request->request->has('token'))
{
return $this->json(['success' => false, 'error' => 'missing_token_parameter']);
}
$token = $request->request->get('token');
}
$userRepository = $this->getDoctrine()->getRepository(User::class);
$user = $userRepository->createQueryBuilder('u')
->andWhere('u.forgotPasswordToken = :token AND u.timeForgotPasswordTokenExpires >= :now')
->setParameter('token', $token)
->setParameter('now', new DateTimeImmutable)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
if ($user === null)
{
if ($isJsonRequest)
{
return $this->json(['success' => false, 'error' => 'invalid_or_expired_token']);
}
return $this->render('@BoldrUsers/reset-link-expired.html.twig');
}
$form = $this->createForm(ResetPasswordType::class, null, ['allow_extra_fields' => true]);
$form->handleRequest($request);
$newPassword = null;
if ($form->isSubmitted() && $form->isValid())
{
$newPassword = $form['newPassword']->getData();
}
if ($request->request->has('new_password'))
{
$newPassword = $request->request->get('new_password');
}
if ($newPassword !== null)
{
$passwordHash = $this->get(UserPasswordHasherInterface::class)->hashPassword($user, $newPassword);
$user->setPasswordHash($passwordHash);
$user->setForgotPasswordToken(null);
$user->setTimeForgotPasswordTokenExpires(null);
$this->getDoctrine()->getManager()->flush();
$event = new UserPasswordChangedEvent($user, $newPassword, true);
$this->get(EventDispatcherInterface::class)->dispatch($event);
$email = $this->get(EmailFactory::class)->createPasswordResetEmail($user);
try
{
$this->get(MailerInterface::class)->send($email);
}
catch (\Exception $ex)
{
// could not send email
}
if ($isJsonRequest)
{
return $this->json(['success' => true]);
}
return $this->render('@BoldrUsers/password-reset.html.twig');
}
if ($isJsonRequest)
{
return $this->json(['success' => false, 'error' => 'missing_fields']);
}
return $this->render('@BoldrUsers/reset-password.html.twig', [
'form' => $form->createView()
]);
}
/**
* @Route({
* "en": "/confirm-email-address/{token}",
* "nl": "/bevestig-email-adres/{token}",
* "de": "/bestatige-email-adresse/{token}"
* }, name="boldr_users_confirm_email_address")
*/
public function confirmEmailAddress(Request $request, string $token): Response
{
$userRepository = $this->getDoctrine()->getRepository(User::class);
$user = $userRepository->findOneBy([
'emailAddressIsConfirmed' => false,
'confirmEmailAddressToken' => $token
]);
if ($user === null)
{
return $this->render('@BoldrUsers/invalid-confirm-token.html.twig');
}
$user->setEmailAddressIsConfirmed(true);
$user->setConfirmEmailAddressToken(null);
$this->getDoctrine()->getManager()->flush();
if (in_array('application/json', $request->getAcceptableContentTypes()))
{
return $this->json(['success' => true]);
}
/** @FIXME Make sure it does not redirect to outside URL */
if ($request->query->has('redirect'))
{
return $this->redirect($request->query->get('redirect'));
}
return $this->render('@BoldrUsers/confirmed.html.twig', [
'user' => $user
]);
}
/**
* @Route({
* "en": "/resend-confirmation-email",
* "nl": "/verstuur-bevestigingsemail-opnieuw",
* "de": "/bestatigungsmail-erneut-senden"
* }, name="boldr_users_resend_confirmation_email")
* @Route("/boldr-api/boldr-users/resend-confirmation-email", name="boldr_users_api_resend_confirmation_email")
*/
public function resendConfirmationEmail(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$isJsonRequest = in_array('application/json', $request->getAcceptableContentTypes());
/** @var User */
$user = $this->getUser();
if ($user->getEmailAddressIsConfirmed())
{
if ($isJsonRequest)
{
return $this->json(['success' => false, 'error' => 'already_confirmed']);
}
return $this->render('@BoldrUsers/already-confirmed.html.twig');
}
// Generate a confirmation token if it does not exist yet
if ($user->getConfirmEmailAddressToken() === null)
{
$confirmEmailAddressToken = bin2hex(random_bytes(32));
$user->setConfirmEmailAddressToken($confirmEmailAddressToken);
$this->getDoctrine()->getManager()->flush();
}
$email = $this->get(EmailFactory::class)->createResendConfirmationEmail($user);
try
{
$this->get(MailerInterface::class)->send($email);
}
catch (\Exception $ex)
{
// could not send email
}
if ($isJsonRequest)
{
return $this->json(['success' => true]);
}
/** @FIXME Make sure it does not redirect to outside URL */
if ($request->query->has('redirect'))
{
return $this->redirect($request->query->get('redirect'));
}
return $this->render('@BoldrUsers/confirmation-email-resent.html.twig');
}
}