refs #450. Reset password from user area

pull/6/head
Manuel Aranda Rosales 2024-06-04 17:25:27 +02:00
parent 9aa0514720
commit 1f53f186fa
8 changed files with 145 additions and 6 deletions

View File

@ -8,7 +8,6 @@ resources:
groups: ['default', 'user:read']
denormalization_context:
groups: ['user:write']
operations:
ApiPlatform\Metadata\GetCollection:
provider: App\State\Provider\UserProvider
@ -26,6 +25,15 @@ resources:
validationContext:
groups: [ 'default', 'user:post' ]
ApiPlatform\Metadata\Delete: ~
reset_password:
provider: App\State\Provider\UserProvider
class: ApiPlatform\Metadata\Put
method: PUT
input: App\Dto\Input\UserInput
uriTemplate: /users/{uuid}/reset-password
controller: App\Controller\ResetPasswordAction
validationContext:
groups: [ 'user:reset-password' ]
properties:
App\Entity\User:

View File

@ -9,6 +9,7 @@ api_platform:
jsonld: ['application/ld+json', 'application/json']
mapping:
paths: ['%kernel.project_dir%/config/api_platform', '%kernel.project_dir%/src/Dto']
use_symfony_listeners: true
defaults:
pagination_client_items_per_page: true
denormalization_context:

View File

View File

@ -0,0 +1,31 @@
<?php
namespace App\Controller;
use App\Dto\Input\UserInput;
use App\Entity\User;
use App\Handler\ResetPasswordHandler;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
class ResetPasswordAction extends AbstractController
{
public function __construct(
private readonly ResetPasswordHandler $resetPasswordHandler,
private readonly UserRepository $userRepository
)
{
}
public function __invoke(string $uuid, UserInput $input): UserInput
{
/** @var User $user */
$user = $this->userRepository->findOneByUuid($uuid);
$this->resetPasswordHandler->handle($user, $input->currentPassword, $input->newPassword);
return new UserInput($user);
}
}

View File

@ -13,32 +13,47 @@ use Symfony\Component\Validator\Constraints as Assert;
final class UserInput
{
#[Assert\NotBlank]
#[Groups(['user:write'])]
#[Groups('user:write')]
public ?string $username = null;
/**
* @var OrganizationalUnit[]
*/
#[Groups(['user:write'])]
#[Groups('user:write')]
#[ApiProperty(readableLink: false, writableLink: false)]
public array $allowedOrganizationalUnits = [];
#[Assert\NotBlank(groups: ['user:post'])]
#[Assert\Length(min: 8, groups: ['user:write', 'user:post'])]
#[Groups(['user:write'])]
#[Groups('user:write')]
public ?string $password = null;
#[Assert\NotNull]
#[Groups(['user:write'])]
#[Groups('user:write')]
public ?bool $enabled = true;
/**
* @var UserGroup[]
*/
#[Groups(['user:write'])]
#[Groups('user:write')]
#[ApiProperty(readableLink: false, writableLink: false)]
public array $userGroups = [];
#[Assert\NotBlank(groups: ['user:reset-password'])]
#[Groups('user:reset-password')]
public ?string $currentPassword = null;
#[Assert\NotBlank(groups: ['user:reset-password'])]
#[Assert\Length(min: 8, groups: ['user:reset-password'])]
#[Groups('user:reset-password')]
public ?string $newPassword = null;
#[Assert\NotBlank(groups: ['user:reset-password'])]
#[Assert\Length(min: 8, groups: ['user:reset-password'])]
#[Assert\Expression(expression: 'this.newPassword === this.repeatNewPassword', message: 'This value should be the same as the new password', groups: ['user:reset-password'])]
#[Groups('user:reset-password')]
public ?string $repeatNewPassword = null;
public function __construct(?User $user = null)
{
if (!$user) {
@ -74,6 +89,18 @@ final class UserInput
}
$user->setAllowedOrganizationalUnits( $allowedOrganizationalUnitToAdd ?? [] );
if ($this->currentPassword !== null) {
$user->setCurrentPassword($this->currentPassword);
}
if ($this->newPassword !== null) {
$user->setNewPassword($this->newPassword);
}
if ($this->repeatNewPassword !== null) {
$user->setRepeatNewPassword($this->repeatNewPassword);
}
return $user;
}
}

View File

@ -47,6 +47,11 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
#[ORM\ManyToMany(targetEntity: OrganizationalUnit::class, inversedBy: 'users')]
private Collection $allowedOrganizationalUnits;
private ?string $currentPassword = null;
private ?string $newPassword = null;
private ?string $repeatNewPassword = null;
public function __construct()
{
parent::__construct();
@ -204,4 +209,40 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
return $this;
}
public function getCurrentPassword(): ?string
{
return $this->currentPassword;
}
public function setCurrentPassword(?string $currentPassword): static
{
$this->currentPassword = $currentPassword;
return $this;
}
public function getNewPassword(): ?string
{
return $this->newPassword;
}
public function setNewPassword(?string $newPassword): static
{
$this->newPassword = $newPassword;
return $this;
}
public function getRepeatNewPassword(): ?string
{
return $this->repeatNewPassword;
}
public function setRepeatNewPassword(?string $repeatNewPassword): static
{
$this->repeatNewPassword = $repeatNewPassword;
return $this;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Handler;
use App\Dto\Input\UserInput;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class ResetPasswordHandler
{
public function __construct(
Security $security,
private readonly UserPasswordHasherInterface $userPasswordHasher
)
{
}
public function handle(User $user, string $currentPassword, string $newPassword): User
{
$currentHashedPassword = $this->userPasswordHasher->isPasswordValid($user, $currentPassword);
if ($currentHashedPassword === false) {
throw new \InvalidArgumentException('The current password is invalid.');
}
$user->setPassword($this->userPasswordHasher->hashPassword($user, $newPassword));
return $user;
}
}

View File

@ -6,6 +6,7 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;