Password change #13
|
@ -53,6 +53,12 @@ user_account:
|
||||||
requirements:
|
requirements:
|
||||||
method: GET
|
method: GET
|
||||||
|
|
||||||
|
user_account_password_change:
|
||||||
|
path: /account/password
|
||||||
|
controller: App\Controller\AccountController::changePassword
|
||||||
|
requirements:
|
||||||
|
method: POST
|
||||||
|
|
||||||
user_account_token_create:
|
user_account_token_create:
|
||||||
path: /profile/api/token/create
|
path: /profile/api/token/create
|
||||||
controller: App\Controller\AccountController::addApiToken
|
controller: App\Controller\AccountController::addApiToken
|
||||||
|
|
|
@ -44,7 +44,7 @@ class AddInvitesCommand extends Command
|
||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$output->writeln(sprintf('<info>%d invites added to \'%s\'.</info>', $number, $user->getUsername()));
|
$output->writeln(sprintf('<info>%d invites added to \'%s\'.</info>', $number, $user->getUserIdentifier()));
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ class AddUserCommand extends Command
|
||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$output->writeln(sprintf('<info>User \'%s\' registered, %d invites added.</info>', $user->getUsername(), $invites));
|
$output->writeln(sprintf('<info>User \'%s\' registered, %d invites added.</info>', $user->getUserIdentifier(), $invites));
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,21 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\{ApiToken, User};
|
use App\Entity\{ApiToken, User};
|
||||||
use App\Repository\{ApiTokenRepository, InviteRepository};
|
use App\Repository\{ApiTokenRepository, InviteRepository};
|
||||||
|
use App\Form\Data\PasswordChangeData;
|
||||||
|
use App\Form\PasswordChangeType;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||||
|
|
||||||
class AccountController extends AbstractController
|
class AccountController extends AbstractController
|
||||||
{
|
{
|
||||||
public function account(InviteRepository $inviteRepo, ApiTokenRepository $apiTokenRepo): Response
|
public function account(
|
||||||
{
|
InviteRepository $inviteRepo,
|
||||||
|
ApiTokenRepository $apiTokenRepo
|
||||||
|
): Response {
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
if (null === $user = $this->getUser()) {
|
if (null === $user = $this->getUser()) {
|
||||||
throw $this->createAccessDeniedException('User not found exception');
|
throw $this->createAccessDeniedException('User not found exception');
|
||||||
|
@ -26,6 +33,32 @@ class AccountController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function changePassword(
|
||||||
|
Request $request,
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
PasswordHasherFactoryInterface $hasherFactory,
|
||||||
|
): Response {
|
||||||
|
$data = new PasswordChangeData();
|
||||||
|
$form = $this->createChangePasswordForm($data);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
$hasher = $hasherFactory->getPasswordHasher($user);
|
||||||
|
|
||||||
|
$user->changePassword($hasher, $data->newPassword);
|
||||||
|
$em->flush();
|
||||||
|
$this->addFlash('success', 'Password changed.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('user_account');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForm('Account/password.html.twig', [
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function addApiToken(EntityManagerInterface $em): Response
|
public function addApiToken(EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
/** @var User|null $user */
|
/** @var User|null $user */
|
||||||
|
@ -57,4 +90,13 @@ class AccountController extends AbstractController
|
||||||
|
|
||||||
return $this->redirectToRoute('user_account');
|
return $this->redirectToRoute('user_account');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createChangePasswordForm(PasswordChangeData $data): FormInterface
|
||||||
|
{
|
||||||
|
return $this
|
||||||
|
->createForm(PasswordChangeType::class, $data, [
|
||||||
|
'action' => $this->generateUrl('user_account_password_change'),
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,15 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\{Invite, PasswordResetToken};
|
use App\Entity\{Invite, PasswordResetToken};
|
||||||
use App\Repository\PasswordResetTokenRepository;
|
use App\Repository\PasswordResetTokenRepository;
|
||||||
use App\Form\{Data\PasswordResetRequestData, Data\PasswordResetData, PasswordResetRequestType, PasswordResetType, RegisterType, Data\RegisterData};
|
use App\Form\Data\{PasswordResetRequestData, PasswordResetData, RegisterData};
|
||||||
|
use App\Form\{PasswordResetRequestType, PasswordResetType, RegisterType};
|
||||||
use App\Repository\InviteRepository;
|
use App\Repository\InviteRepository;
|
||||||
use App\User\{Exception\UserNotFoundException, InviteManager, PasswordResetManager, UserManager};
|
use App\User\{Exception\UserNotFoundException, InviteManager, PasswordResetManager, UserManager};
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\{Extension\Core\Type\SubmitType, FormError, FormInterface};
|
use Symfony\Component\Form\{Extension\Core\Type\SubmitType, FormError, FormInterface};
|
||||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||||
|
|
||||||
class UserController extends AbstractController
|
class UserController extends AbstractController
|
||||||
{
|
{
|
||||||
|
@ -23,8 +25,8 @@ class UserController extends AbstractController
|
||||||
InviteManager $inviteManager,
|
InviteManager $inviteManager,
|
||||||
InviteRepository $inviteRepo
|
InviteRepository $inviteRepo
|
||||||
): Response {
|
): Response {
|
||||||
$formData = new RegisterData($code);
|
$data = new RegisterData($code);
|
||||||
$form = $this->createRegisterForm($formData, $code);
|
$form = $this->createRegisterForm($data, $code);
|
||||||
|
|
||||||
/** @var Invite $invite */
|
/** @var Invite $invite */
|
||||||
$invite = $inviteRepo->findOneBy(['code' => $code, 'usedBy' => null]);
|
$invite = $inviteRepo->findOneBy(['code' => $code, 'usedBy' => null]);
|
||||||
|
@ -33,9 +35,9 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$user = $userManager->createUserByInvite(
|
$user = $userManager->createUserByInvite(
|
||||||
$formData->username,
|
$data->username,
|
||||||
$formData->password,
|
$data->password,
|
||||||
$formData->email,
|
$data->email,
|
||||||
$invite
|
$invite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -54,8 +56,8 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
public function requestReset(Request $request, EntityManagerInterface $em, PasswordResetManager $manager): Response
|
public function requestReset(Request $request, EntityManagerInterface $em, PasswordResetManager $manager): Response
|
||||||
{
|
{
|
||||||
$formData = new PasswordResetRequestData();
|
$data = new PasswordResetRequestData();
|
||||||
$form = $this->createResetRequestForm($formData);
|
$form = $this->createResetRequestForm($data);
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$manager->sendResetLink($formData->email);
|
$manager->sendResetLink($data->email);
|
||||||
|
|
||||||
$message = 'Password reset link was sent';
|
$message = 'Password reset link was sent';
|
||||||
} catch (UserNotFoundException $e) {
|
} catch (UserNotFoundException $e) {
|
||||||
|
@ -84,12 +86,12 @@ class UserController extends AbstractController
|
||||||
string $code,
|
string $code,
|
||||||
Request $request,
|
Request $request,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
UserManager $manager,
|
PasswordHasherFactoryInterface $hasherFactory,
|
||||||
PasswordResetTokenRepository $tokenRepository
|
PasswordResetTokenRepository $tokenRepository,
|
||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$formData = new PasswordResetData();
|
$data = new PasswordResetData();
|
||||||
$form = $this->createResetForm($formData, $code);
|
$form = $this->createPasswordResetForm($data, $code);
|
||||||
|
|
||||||
/** @var PasswordResetToken $token */
|
/** @var PasswordResetToken $token */
|
||||||
$token = $tokenRepository->find($code);
|
$token = $tokenRepository->find($code);
|
||||||
|
@ -98,15 +100,17 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
if ($token && $token->isValid()) {
|
if ($token && $token->isValid()) {
|
||||||
$manager->changePassword($token->getUser(), $formData->password);
|
$user = $token->getUser();
|
||||||
|
$hasher = $hasherFactory->getPasswordHasher($user);
|
||||||
|
$user->changePassword($hasher, $data->password);
|
||||||
|
|
||||||
$em->remove($token);
|
$em->remove($token);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
return $this->redirectToRoute('user_auth_login');
|
return $this->redirectToRoute('user_auth_login');
|
||||||
} else {
|
|
||||||
$form->addError(new FormError('Invalid token.'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$form->addError(new FormError('Invalid token.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('User/reset.html.twig', [
|
return $this->render('User/reset.html.twig', [
|
||||||
|
@ -116,32 +120,28 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
private function createResetRequestForm(PasswordResetRequestData $formData): FormInterface
|
private function createResetRequestForm(PasswordResetRequestData $formData): FormInterface
|
||||||
{
|
{
|
||||||
$form = $this->createForm(PasswordResetRequestType::class, $formData, [
|
return $this
|
||||||
|
->createForm(PasswordResetRequestType::class, $formData, [
|
||||||
'action' => $this->generateUrl('user_reset_request'),
|
'action' => $this->generateUrl('user_reset_request'),
|
||||||
]);
|
])
|
||||||
$form->add('submit', SubmitType::class);
|
->add('submit', SubmitType::class);
|
||||||
|
|
||||||
return $form;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createResetForm(PasswordResetData $formData, string $code): FormInterface
|
private function createPasswordResetForm(PasswordResetData $data, string $code): FormInterface
|
||||||
{
|
{
|
||||||
$form = $this->createForm(PasswordResetType::class, $formData, [
|
return $this
|
||||||
|
->createForm(PasswordResetType::class, $data, [
|
||||||
'action' => $this->generateUrl('user_reset', ['code' => $code]),
|
'action' => $this->generateUrl('user_reset', ['code' => $code]),
|
||||||
]);
|
])
|
||||||
$form->add('submit', SubmitType::class);
|
->add('submit', SubmitType::class);
|
||||||
|
|
||||||
return $form;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createRegisterForm(RegisterData $formData, string $code): FormInterface
|
private function createRegisterForm(RegisterData $formData, string $code): FormInterface
|
||||||
{
|
{
|
||||||
$form = $this->createForm(RegisterType::class, $formData, [
|
return $this
|
||||||
|
->createForm(RegisterType::class, $formData, [
|
||||||
'action' => $this->generateUrl('user_register', ['code' => $code]),
|
'action' => $this->generateUrl('user_register', ['code' => $code]),
|
||||||
]);
|
])
|
||||||
|
->add('submit', SubmitType::class);
|
||||||
$form->add('submit', SubmitType::class);
|
|
||||||
|
|
||||||
return $form;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
19
src/Form/Data/PasswordChangeData.php
Normal file
19
src/Form/Data/PasswordChangeData.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Data;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
|
||||||
|
|
||||||
|
class PasswordChangeData
|
||||||
|
{
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[SecurityAssert\UserPassword(message: 'Wrong password')]
|
||||||
|
public string $currentPassword;
|
||||||
|
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(min: 8, max: 4096)]
|
||||||
|
#[Assert\NotCompromisedPassword(skipOnError: true)]
|
||||||
|
public string $newPassword;
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
class LoginType extends AbstractType
|
class LoginType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('_username', TextType::class, ['mapped' => false, 'required' => true])
|
->add('_username', TextType::class, ['mapped' => false, 'required' => true])
|
||||||
|
|
38
src/Form/PasswordChangeType.php
Normal file
38
src/Form/PasswordChangeType.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Form\Data\PasswordChangeData;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\{PasswordType, RepeatedType};
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class PasswordChangeType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('currentPassword', PasswordType::class, [
|
||||||
|
'label' => 'Current password',
|
||||||
|
'invalid_message' => 'Wrong password.',
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('newPassword', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'invalid_message' => 'The password fields must match.',
|
||||||
|
'required' => true,
|
||||||
|
'first_options' => ['label' => 'New password'],
|
||||||
|
'second_options' => ['label' => 'Repeat'],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => PasswordChangeData::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class PasswordResetRequestType extends AbstractType
|
class PasswordResetRequestType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('email', EmailType::class, ['required' => true])
|
->add('email', EmailType::class, ['required' => true])
|
||||||
|
@ -18,7 +18,7 @@ class PasswordResetRequestType extends AbstractType
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => PasswordResetRequestData::class,
|
'data_class' => PasswordResetRequestData::class,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class PasswordResetType extends AbstractType
|
class PasswordResetType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('password', RepeatedType::class, [
|
->add('password', RepeatedType::class, [
|
||||||
|
@ -24,7 +24,7 @@ class PasswordResetType extends AbstractType
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => PasswordResetData::class,
|
'data_class' => PasswordResetData::class,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class RegisterType extends AbstractType
|
class RegisterType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('username', TextType::class, ['required' => true])
|
->add('username', TextType::class, ['required' => true])
|
||||||
|
@ -21,7 +21,7 @@ class RegisterType extends AbstractType
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => RegisterData::class,
|
'data_class' => RegisterData::class,
|
||||||
|
|
|
@ -34,11 +34,6 @@ class UserManager
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function changePassword(User $user, string $rawPassword): void
|
|
||||||
{
|
|
||||||
$user->changePassword($this->hasherFactory->getPasswordHasher(User::class), $rawPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User
|
public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User
|
||||||
{
|
{
|
||||||
if (null !== $invite->getUsedBy()) {
|
if (null !== $invite->getUsedBy()) {
|
||||||
|
|
5
templates/Account/password.html.twig
Normal file
5
templates/Account/password.html.twig
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ form(form) }}
|
||||||
|
{% endblock %}
|
|
@ -10,7 +10,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Username</th>
|
<th scope="row">Username</th>
|
||||||
<td>{{ user.username }}</td>
|
<td>{{ user.userIdentifier }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -20,5 +20,12 @@
|
||||||
<button type="button" class="btn disabled" title="Not implemented">Change</button>
|
<button type="button" class="btn disabled" title="Not implemented">Change</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Password</th>
|
||||||
|
<td>********</td>
|
||||||
|
<td>
|
||||||
|
<a role="button" class="btn btn-primary" href="{{ path('user_account_password_change') }}">Change</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
Loading…
Reference in a new issue