Merge branch 'main' into feature/organizational-unit-hierarchy

pull/7/head
Manuel Aranda Rosales 2024-06-14 10:52:22 +02:00
commit 26f4595b2a
8 changed files with 178 additions and 6 deletions

View File

@ -9,6 +9,7 @@ resources:
groups: ['user:write'] groups: ['user:write']
operations: operations:
ApiPlatform\Metadata\GetCollection: ApiPlatform\Metadata\GetCollection:
security: 'is_granted("ROLE_SUPER_ADMIN")'
provider: App\State\Provider\UserProvider provider: App\State\Provider\UserProvider
filters: filters:
- 'api_platform.filter.user.order' - 'api_platform.filter.user.order'
@ -21,9 +22,20 @@ resources:
ApiPlatform\Metadata\Patch: ApiPlatform\Metadata\Patch:
provider: App\State\Provider\UserProvider provider: App\State\Provider\UserProvider
ApiPlatform\Metadata\Post: ApiPlatform\Metadata\Post:
security: 'is_granted("ROLE_SUPER_ADMIN")'
validationContext: validationContext:
groups: [ 'default', 'user:post' ] groups: [ 'default', 'user:post' ]
ApiPlatform\Metadata\Delete: ~ ApiPlatform\Metadata\Delete:
security: 'is_granted("ROLE_SUPER_ADMIN")'
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: properties:
App\Entity\User: App\Entity\User:

View File

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

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 final class UserInput
{ {
#[Assert\NotBlank] #[Assert\NotBlank]
#[Groups(['user:write'])] #[Groups('user:write')]
public ?string $username = null; public ?string $username = null;
/** /**
* @var OrganizationalUnit[] * @var OrganizationalUnit[]
*/ */
#[Groups(['user:write'])] #[Groups('user:write')]
#[ApiProperty(readableLink: false, writableLink: false)] #[ApiProperty(readableLink: false, writableLink: false)]
public array $allowedOrganizationalUnits = []; public array $allowedOrganizationalUnits = [];
#[Assert\NotBlank(groups: ['user:post'])] #[Assert\NotBlank(groups: ['user:post'])]
#[Assert\Length(min: 8, groups: ['user:write', 'user:post'])] #[Assert\Length(min: 8, groups: ['user:write', 'user:post'])]
#[Groups(['user:write'])] #[Groups('user:write')]
public ?string $password = null; public ?string $password = null;
#[Assert\NotNull] #[Assert\NotNull]
#[Groups(['user:write'])] #[Groups('user:write')]
public ?bool $enabled = true; public ?bool $enabled = true;
/** /**
* @var UserGroup[] * @var UserGroup[]
*/ */
#[Groups(['user:write'])] #[Groups('user:write')]
#[ApiProperty(readableLink: false, writableLink: false)] #[ApiProperty(readableLink: false, writableLink: false)]
public array $userGroups = []; 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) public function __construct(?User $user = null)
{ {
if (!$user) { if (!$user) {
@ -74,6 +89,18 @@ final class UserInput
} }
$user->setAllowedOrganizationalUnits( $allowedOrganizationalUnitToAdd ?? [] ); $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; return $user;
} }
} }

View File

@ -47,6 +47,11 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
#[ORM\ManyToMany(targetEntity: OrganizationalUnit::class, inversedBy: 'users')] #[ORM\ManyToMany(targetEntity: OrganizationalUnit::class, inversedBy: 'users')]
private Collection $allowedOrganizationalUnits; private Collection $allowedOrganizationalUnits;
private ?string $currentPassword = null;
private ?string $newPassword = null;
private ?string $repeatNewPassword = null;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@ -204,4 +209,40 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
return $this; 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

@ -0,0 +1,29 @@
<?php
namespace App\Lexik;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: Events::JWT_CREATED, priority: 1)]
class JWTCreatedListener
{
public function __invoke(JWTCreatedEvent $event): void
{
$user = $event->getUser();
if (!$user instanceof User) {
return;
}
$payload = $event->getData();
$payload['id'] = $user->getId();
$payload['username'] = $user->getUsername();
$payload['uuid'] = $user->getUuid();
$payload['roles'] = $user->getRoles();
$event->setData($payload);
}
}

View File

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