From b735bff62ebff25f5ec0655b49628d359e52a17b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 11:21:45 +0100 Subject: [PATCH 01/28] Bump benzina to 3.1 --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 8388f78..5d0dfe6 100644 --- a/composer.lock +++ b/composer.lock @@ -3002,16 +3002,16 @@ }, { "name": "goteo/benzina", - "version": "0.3", + "version": "0.3.1", "source": { "type": "git", - "url": "https://github.com/GoteoFoundation/benzina.git", - "reference": "83cc705b963868dc481f6dd7c6d699b72b537586" + "url": "https://github.com/goteo/benzina.git", + "reference": "c19f8671285f1050a11582f8dd17f87888cb727c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GoteoFoundation/benzina/zipball/83cc705b963868dc481f6dd7c6d699b72b537586", - "reference": "83cc705b963868dc481f6dd7c6d699b72b537586", + "url": "https://api.github.com/repos/goteo/benzina/zipball/c19f8671285f1050a11582f8dd17f87888cb727c", + "reference": "c19f8671285f1050a11582f8dd17f87888cb727c", "shasum": "" }, "require": { @@ -3054,10 +3054,10 @@ ], "description": "Migration tool to pump v3 data into a v4 schema", "support": { - "issues": "https://github.com/GoteoFoundation/benzina/issues", - "source": "https://github.com/GoteoFoundation/benzina/tree/0.3" + "issues": "https://github.com/goteo/benzina/issues", + "source": "https://github.com/goteo/benzina/tree/0.3.1" }, - "time": "2025-02-13T20:29:02+00:00" + "time": "2025-02-21T15:13:32+00:00" }, { "name": "jolicode/automapper", From bd6d08a5717738eb1382395e0e4c42e7889e26cc Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 13:27:48 +0100 Subject: [PATCH 02/28] Update username to handle --- config/packages/security.yaml | 2 +- src/ApiResource/User/UserApiResource.php | 8 +------ src/Benzina/UsersPump.php | 30 ++++++++++++------------ src/Dto/UserTokenLoginDto.php | 2 +- src/Entity/User/User.php | 14 +++++------ src/Filter/UserQueryFilter.php | 5 ++-- src/OpenApi/OpenApiDescription.md | 2 +- src/Repository/User/UserRepository.php | 4 ++-- src/Service/Auth/AuthService.php | 2 +- 9 files changed, 31 insertions(+), 38 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 1c6454f..191ba3a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -13,7 +13,7 @@ security: app_user_provider: entity: class: App\Entity\User\User - property: username + property: handle firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 9365cb8..883074a 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -49,13 +49,7 @@ class UserApiResource #[Assert\NotBlank()] #[Assert\Length(min: 4, max: 30)] #[Assert\Regex('/^[a-z0-9_]+$/')] - public string $username; - - /** - * Display name chosen by the User. - */ - #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'partial')] - public string $name; + public string $handle; /** * A list of the roles assigned to this User. Admin scopped property. diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index 3db20c7..06d63e7 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -27,7 +27,7 @@ class UsersPump extends AbstractPump public function pump(mixed $record): void { $user = new User(); - $user->setUsername($this->getUsername($record)); + $user->setHandle($this->buildHandle($record)); $user->setPassword($record['password'] ?? ''); $user->setEmail($record['email']); $user->setEmailConfirmed(false); @@ -42,40 +42,40 @@ class UsersPump extends AbstractPump ++$this->userCount; } - private function normalizeUsername(string $username): ?string + private function normalizeStringForHandle(string $value): ?string { // If email remove provider - if (\str_contains($username, '@') && \preg_match('/^[\w]+[^@]/', $username, $matches)) { - $username = $matches[0]; + if (\str_contains($value, '@') && \preg_match('/^[\w]+[^@]/', $value, $matches)) { + $value = $matches[0]; } - // Only lowercase a-z, numbers and underscore in usernames - $username = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($username)); + // Only lowercase a-z, numbers and underscore in user handles + $value = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($value)); // Min length 4 - $username = \str_pad($username, 4, '_'); + $value = \str_pad($value, 4, '_'); // Max length 30 - $username = \substr($username, 0, 30); + $value = \substr($value, 0, 30); - if (strlen(str_replace('_', '', $username)) < 1) { + if (strlen(str_replace('_', '', $value)) < 1) { return null; } - return $username; + return $value; } - private function getUsername(array $record): string + private function buildHandle(array $record): string { - $username = $this->normalizeUsername($record['id']); + $handle = $this->normalizeStringForHandle($record['id']); - if ($username === null) { - $username = $this->normalizeUsername($record['email']); + if ($handle === null) { + $handle = $this->normalizeStringForHandle($record['email']); } $sequenceNumber = \substr($this->userCount, -2, 2); - return \sprintf('%s_%s', $username, $sequenceNumber); + return \sprintf('%s_%s', $handle, $sequenceNumber); } private function getDateCreated(array $record): \DateTime diff --git a/src/Dto/UserTokenLoginDto.php b/src/Dto/UserTokenLoginDto.php index 5befd58..4264cb4 100644 --- a/src/Dto/UserTokenLoginDto.php +++ b/src/Dto/UserTokenLoginDto.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraints as Assert; final class UserTokenLoginDto { /** - * The identifier (email, username) of the User to be authenticated. + * The identifier (email, handle) of the User to be authenticated. */ #[Assert\NotBlank()] public readonly string $identifier; diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 630fce8..67f19c2 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -30,7 +30,7 @@ use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Index(fields: ['migratedId'])] #[UniqueEntity('email', message: 'This email address is already registered.')] -#[UniqueEntity('username', message: 'This usernames already exists.')] +#[UniqueEntity('handle', message: 'This handle is already in use.')] class User implements UserInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { use MigratedEntity; @@ -57,7 +57,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account */ #[Gedmo\Versioned] #[ORM\Column(length: 255, unique: true)] - private ?string $username = null; + private ?string $handle = null; /** * @var list The user roles. Admin only property. @@ -130,7 +130,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account */ public function getUserIdentifier(): string { - return (string) $this->username; + return (string) $this->handle; } public function getEmail(): ?string @@ -157,14 +157,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function getUsername(): ?string + public function getHandle(): ?string { - return $this->username; + return $this->handle; } - public function setUsername(?string $username): static + public function setHandle(?string $handle): static { - $this->username = strtolower($username); + $this->handle = strtolower($handle); return $this; } diff --git a/src/Filter/UserQueryFilter.php b/src/Filter/UserQueryFilter.php index e179cb3..8aa1950 100644 --- a/src/Filter/UserQueryFilter.php +++ b/src/Filter/UserQueryFilter.php @@ -42,8 +42,7 @@ final class UserQueryFilter extends AbstractFilter $queryBuilder ->andWhere(sprintf('%s.email LIKE :%s', $rootAlias, $parameterName)) - ->orWhere(sprintf('%s.name LIKE :%s', $rootAlias, $parameterName)) - ->orWhere(sprintf('%s.username LIKE :%s', $rootAlias, $parameterName)) + ->orWhere(sprintf('%s.handle LIKE :%s', $rootAlias, $parameterName)) ->setParameter($parameterName, $value); } @@ -55,7 +54,7 @@ final class UserQueryFilter extends AbstractFilter 'property' => $property, 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, - 'description' => 'Query Users by email, name or username. Fuzzy.', + 'description' => 'Query Users by email or handle. Fuzzy.', ]; } diff --git a/src/OpenApi/OpenApiDescription.md b/src/OpenApi/OpenApiDescription.md index 3e53ec8..2417bbe 100644 --- a/src/OpenApi/OpenApiDescription.md +++ b/src/OpenApi/OpenApiDescription.md @@ -23,7 +23,7 @@ curl -X 'GET' \ UserTokens exist under an User's scope, when you obtain a token this will only grant you as much permissions as the User under which it was created. -To obtain a UserToken you must send a POST request to [/v4/user_tokens](/v4/user_tokens) with the User login credentials (username and password) in the payload. If the credentials are correct a UserToken will be created, the `token` property value of which you must include in future requests. +To obtain a UserToken you must send a POST request to [/v4/user_tokens](/v4/user_tokens) with the User login credentials (identifier and password) in the payload. If the credentials are correct a UserToken will be created, the `token` property value of which you must include in future requests. Users can delete UserTokens owned by them at any moment, revoking your application's access to the v4 API on their behalf. When a UserToken fails to authenticate a request you will receive a 401 Unauthorized response from the API, at which point your application must look to get a new UserToken from the User. diff --git a/src/Repository/User/UserRepository.php b/src/Repository/User/UserRepository.php index 6e00a27..84c3c42 100644 --- a/src/Repository/User/UserRepository.php +++ b/src/Repository/User/UserRepository.php @@ -44,14 +44,14 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader /** * Find an User by any of it's unique properties. * - * @param string $identifier An id, email or username + * @param string $identifier An id, email or handle */ public function findOneByIdentifier(string $identifier): ?User { return $this->createQueryBuilder('u') ->andWhere('u.id = :val') ->orWhere('u.email = :val') - ->orWhere('u.username = :val') + ->orWhere('u.handle = :val') ->setParameter('val', $identifier) ->getQuery() ->getOneOrNullResult(); diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 8d507ee..1bd1d47 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -54,7 +54,7 @@ class AuthService } return $this->userRepository->findOneBy( - ['username' => $loggedInUser->getUserIdentifier()] + ['handle' => $loggedInUser->getUserIdentifier()] ); } } From 723f2d31803c7c1ab4739bd76a109f4d1df5b919 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 13:28:24 +0100 Subject: [PATCH 03/28] Minimize direct to user data --- src/ApiResource/User/UserApiResource.php | 34 +++-- src/Dto/UserSignupDto.php | 20 +-- src/Entity/User/User.php | 176 +++++++++-------------- 3 files changed, 99 insertions(+), 131 deletions(-) diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 883074a..5d60285 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -2,7 +2,6 @@ namespace App\ApiResource\User; -use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; @@ -39,9 +38,6 @@ class UserApiResource #[Assert\Email()] public string $email; - #[API\ApiProperty(writable: false)] - public bool $emailConfirmed; - /** * A unique, non white space, byte-safe string identifier for this User. */ @@ -51,14 +47,6 @@ class UserApiResource #[Assert\Regex('/^[a-z0-9_]+$/')] public string $handle; - /** - * A list of the roles assigned to this User. Admin scopped property. - * - * @var array - */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("ROLE_ADMIN")')] - public array $roles; - /** * The Accounting for this User monetary movements. */ @@ -72,4 +60,26 @@ class UserApiResource */ #[API\ApiProperty(writable: false)] public array $projects; + + /** + * A list of the roles assigned to this User. Admin scoped property. + * + * @var array + */ + #[API\ApiProperty( + security: 'is_granted("ROLE_ADMIN")', + securityPostDenormalize: 'is_granted("ROLE_ADMIN")' + )] + public array $roles; + + /** + * Has this User confirmed their email address? + */ + #[API\ApiProperty(writable: false)] + public bool $emailConfirmed; + + /** + * A flag determined by the platform for Users who are known to be active. + */ + public bool $active; } diff --git a/src/Dto/UserSignupDto.php b/src/Dto/UserSignupDto.php index 86146d7..2e47518 100644 --- a/src/Dto/UserSignupDto.php +++ b/src/Dto/UserSignupDto.php @@ -6,24 +6,18 @@ use Symfony\Component\Validator\Constraints as Assert; final class UserSignupDto { + /** + * A valid e-mail address for the new User. + */ #[Assert\NotBlank()] #[Assert\Email()] public string $email; + /** + * The auth password for the new User. Plaintext string, + * will be hashed by the API. + */ #[Assert\NotBlank()] #[Assert\Length(min: 8)] public string $password; - - /** - * A unique, non white space, byte-safe string identifier for this User. - */ - #[Assert\NotBlank()] - #[Assert\Length(min: 4, max: 30)] - #[Assert\Regex('/^[a-z0-9_]+$/')] - public string $username; - - /** - * Display name chosen by the User. - */ - public string $name; } diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 67f19c2..53d2d76 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -47,10 +47,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account private ?string $email = null; /** - * Has this User confirmed their email address? + * @var string The user password */ #[ORM\Column] - private ?bool $emailConfirmed = null; + private ?string $password = null; /** * Human readable, non white space, unique string. @@ -59,12 +59,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\Column(length: 255, unique: true)] private ?string $handle = null; - /** - * @var list The user roles. Admin only property. - */ - #[ORM\Column] - private array $roles = []; - #[ORM\OneToOne(inversedBy: 'user', cascade: ['persist'])] private ?Accounting $accounting = null; @@ -80,42 +74,36 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\OneToMany(mappedBy: 'owner', targetEntity: UserToken::class, orphanRemoval: true)] private Collection $tokens; + /** + * @var list The user roles. Admin only property. + */ + #[ORM\Column] + private array $roles = []; + + /** + * Has this User confirmed their email address? + */ + #[ORM\Column] + private ?bool $emailConfirmed = null; + /** * A flag determined by the platform for Users who are known to be active. */ #[ORM\Column] private ?bool $active = null; - /** - * Path to the Users's avatar image. - */ - #[ORM\Column(length: 255, nullable: true)] - private ?string $avatar = null; - - /** - * Conventional name of the person owning this User. - */ - #[ORM\Column(length: 255, nullable: true)] - private ?string $name = null; - #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] private ?UserPersonal $personalData = null; - /** - * @var string The user password - */ - #[ORM\Column] - private ?string $password = null; - public function __construct() { $this->accounting = Accounting::of($this); - $this->emailConfirmed = false; - $this->active = false; - $this->tokens = new ArrayCollection(); $this->projects = new ArrayCollection(); + + $this->emailConfirmed = false; + $this->active = false; } public function getId(): ?int @@ -145,18 +133,26 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function isEmailConfirmed(): ?bool + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string { - return $this->emailConfirmed; + return $this->password; } - public function setEmailConfirmed(bool $emailConfirmed): static + public function setPassword(string $password): static { - $this->emailConfirmed = $emailConfirmed; + $this->password = $password; return $this; } + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + } + public function getHandle(): ?string { return $this->handle; @@ -169,35 +165,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - /** - * @see UserInterface - * - * @return list - */ - public function getRoles(): array - { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; - - return array_unique($roles); - } - - /** - * @param list $roles - */ - public function setRoles(array $roles): static - { - $this->roles = $roles; - - return $this; - } - - public function hasRoles(array $roles): bool - { - return count(array_intersect($this->getRoles(), $roles)) > 0; - } - public function getAccounting(): ?Accounting { return $this->accounting; @@ -270,6 +237,47 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + public function hasRoles(array $roles): bool + { + return count(array_intersect($this->getRoles(), $roles)) > 0; + } + + public function isEmailConfirmed(): ?bool + { + return $this->emailConfirmed; + } + + public function setEmailConfirmed(bool $emailConfirmed): static + { + $this->emailConfirmed = $emailConfirmed; + + return $this; + } + public function isActive(): ?bool { return $this->active; @@ -282,30 +290,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function getAvatar(): ?string - { - return $this->avatar; - } - - public function setAvatar(?string $avatar): static - { - $this->avatar = $avatar; - - return $this; - } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): static - { - $this->name = $name; - - return $this; - } - public function getPersonalData(): ?UserPersonal { return $this->personalData; @@ -322,24 +306,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - - /** - * @see PasswordAuthenticatedUserInterface - */ - public function getPassword(): string - { - return $this->password; - } - - public function setPassword(string $password): static - { - $this->password = $password; - - return $this; - } - - public function eraseCredentials(): void - { - // If you store any temporary, sensitive data on the user, clear it here - } } From 87beb08723a72b8994ea30e20bd8a5347a2e3082 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 14:33:03 +0100 Subject: [PATCH 04/28] Update user to have avatars and specify their type --- src/ApiResource/User/UserApiResource.php | 36 ++++++++++++++++------- src/Entity/User/User.php | 37 ++++++++++++++++++++++++ src/Entity/User/UserType.php | 16 ++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/Entity/User/UserType.php diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 5d60285..46543ea 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -7,6 +7,7 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\Dto\UserSignupDto; use App\Entity\User\User; +use App\Entity\User\UserType; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; use App\State\ApiResourceStateProcessor; @@ -47,6 +48,29 @@ class UserApiResource #[Assert\Regex('/^[a-z0-9_]+$/')] public string $handle; + /** + * URL to the avatar image of this User. + */ + #[Assert\Url()] + public string $avatar; + + /** + * Is this User for an individual acting on their own or a group of individuals? + */ + #[API\ApiProperty(securityPostDenormalize: 'is_granted("USER_EDIT")')] + public UserType $type; + + /** + * A list of the roles assigned to this User. Admin scoped property. + * + * @var array + */ + #[API\ApiProperty( + security: 'is_granted("ROLE_ADMIN")', + securityPostDenormalize: 'is_granted("ROLE_ADMIN")' + )] + public array $roles; + /** * The Accounting for this User monetary movements. */ @@ -61,17 +85,6 @@ class UserApiResource #[API\ApiProperty(writable: false)] public array $projects; - /** - * A list of the roles assigned to this User. Admin scoped property. - * - * @var array - */ - #[API\ApiProperty( - security: 'is_granted("ROLE_ADMIN")', - securityPostDenormalize: 'is_granted("ROLE_ADMIN")' - )] - public array $roles; - /** * Has this User confirmed their email address? */ @@ -81,5 +94,6 @@ class UserApiResource /** * A flag determined by the platform for Users who are known to be active. */ + #[API\ApiProperty(writable: false)] public bool $active; } diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 53d2d76..fd31e08 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -13,6 +13,7 @@ use App\Repository\User\UserRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -95,6 +96,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] private ?UserPersonal $personalData = null; + /** + * Is this user for an individual acting on their own or a larger group of individuals? + */ + #[ORM\Column(enumType: UserType::class)] + private ?UserType $type = null; + + /** + * URL to the avatar image of this User. + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $avatar = null; + public function __construct() { $this->accounting = Accounting::of($this); @@ -306,4 +319,28 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + + public function getType(): ?UserType + { + return $this->type; + } + + public function setType(UserType $type): static + { + $this->type = $type; + + return $this; + } + + public function getAvatar(): ?string + { + return $this->avatar; + } + + public function setAvatar(?string $avatar): static + { + $this->avatar = $avatar; + + return $this; + } } diff --git a/src/Entity/User/UserType.php b/src/Entity/User/UserType.php new file mode 100644 index 0000000..adb8f8f --- /dev/null +++ b/src/Entity/User/UserType.php @@ -0,0 +1,16 @@ + Date: Mon, 24 Feb 2025 14:50:56 +0100 Subject: [PATCH 05/28] Move user handle normalization to service --- src/Benzina/UsersPump.php | 29 +++-------------------------- src/Service/UserService.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 src/Service/UserService.php diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index 06d63e7..c3a3169 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -3,6 +3,7 @@ namespace App\Benzina; use App\Entity\User\User; +use App\Service\UserService; use Goteo\Benzina\Pump\AbstractPump; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; @@ -31,7 +32,6 @@ class UsersPump extends AbstractPump $user->setPassword($record['password'] ?? ''); $user->setEmail($record['email']); $user->setEmailConfirmed(false); - $user->setName($record['name']); $user->setActive(false); $user->setMigrated(true); $user->setMigratedId($record['id']); @@ -42,35 +42,12 @@ class UsersPump extends AbstractPump ++$this->userCount; } - private function normalizeStringForHandle(string $value): ?string - { - // If email remove provider - if (\str_contains($value, '@') && \preg_match('/^[\w]+[^@]/', $value, $matches)) { - $value = $matches[0]; - } - - // Only lowercase a-z, numbers and underscore in user handles - $value = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($value)); - - // Min length 4 - $value = \str_pad($value, 4, '_'); - - // Max length 30 - $value = \substr($value, 0, 30); - - if (strlen(str_replace('_', '', $value)) < 1) { - return null; - } - - return $value; - } - private function buildHandle(array $record): string { - $handle = $this->normalizeStringForHandle($record['id']); + $handle = UserService::asHandle($record['id']); if ($handle === null) { - $handle = $this->normalizeStringForHandle($record['email']); + $handle = UserService::asHandle($record['email']); } $sequenceNumber = \substr($this->userCount, -2, 2); diff --git a/src/Service/UserService.php b/src/Service/UserService.php new file mode 100644 index 0000000..cd37db1 --- /dev/null +++ b/src/Service/UserService.php @@ -0,0 +1,35 @@ + Date: Mon, 24 Feb 2025 14:55:53 +0100 Subject: [PATCH 06/28] Find users with similar handles --- src/Repository/User/UserRepository.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Repository/User/UserRepository.php b/src/Repository/User/UserRepository.php index 84c3c42..c69fb9c 100644 --- a/src/Repository/User/UserRepository.php +++ b/src/Repository/User/UserRepository.php @@ -66,6 +66,20 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getOneOrNullResult(); } + /** + * @return User[] Returns an array of User objects + */ + public function findLike(string $handle): array + { + return $this->createQueryBuilder('u') + ->andWhere('u.handle LIKE :handle') + ->setParameter('handle', \sprintf('%%%s%%', $handle)) + ->orderBy('u.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + // /** // * @return User[] Returns an array of User objects // */ From 390ea14de6a9a5133076eedafc686af93bbbe40e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 14:56:05 +0100 Subject: [PATCH 07/28] Update user handle generation --- src/Benzina/UsersPump.php | 6 +++--- src/State/User/UserSignupProcessor.php | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index c3a3169..c786a8c 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -44,9 +44,9 @@ class UsersPump extends AbstractPump private function buildHandle(array $record): string { - $handle = UserService::asHandle($record['id']); - - if ($handle === null) { + try { + $handle = UserService::asHandle($record['id']); + } catch (\Exception $e) { $handle = UserService::asHandle($record['email']); } diff --git a/src/State/User/UserSignupProcessor.php b/src/State/User/UserSignupProcessor.php index 87050a0..dd8c773 100644 --- a/src/State/User/UserSignupProcessor.php +++ b/src/State/User/UserSignupProcessor.php @@ -8,6 +8,8 @@ use App\ApiResource\User\UserApiResource; use App\Dto\UserSignupDto; use App\Entity\User\User; use App\Mapping\AutoMapper; +use App\Repository\User\UserRepository; +use App\Service\UserService; use App\State\EntityStateProcessor; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; @@ -16,6 +18,7 @@ final class UserSignupProcessor implements ProcessorInterface public function __construct( private AutoMapper $autoMapper, private EntityStateProcessor $entityStateProcessor, + private UserRepository $userRepository, private UserPasswordHasherInterface $userPasswordHasher, ) {} @@ -28,9 +31,26 @@ final class UserSignupProcessor implements ProcessorInterface { /** @var User */ $user = $this->autoMapper->map($data, User::class); + + $user->setHandle($this->buildHandle($data)); $user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password)); + $user = $this->entityStateProcessor->process($user, $operation, $uriVariables, $context); return $this->autoMapper->map($user, UserApiResource::class); } + + private function buildHandle(UserSignupDto $data): string + { + $base = UserService::asHandle($data->email); + + $users = $this->userRepository->findLike($base); + $usersCount = \count($users); + + if ($usersCount < 1) { + return $base; + } + + return \sprintf('%s_%02d', $base, $usersCount + 1); + } } From b1fcf240c50dac6790421763c79e0d31d7d96cc3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 14:56:16 +0100 Subject: [PATCH 08/28] Add user type to sign up dto --- src/Dto/UserSignupDto.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Dto/UserSignupDto.php b/src/Dto/UserSignupDto.php index 2e47518..7a5b56b 100644 --- a/src/Dto/UserSignupDto.php +++ b/src/Dto/UserSignupDto.php @@ -2,6 +2,7 @@ namespace App\Dto; +use App\Entity\User\UserType; use Symfony\Component\Validator\Constraints as Assert; final class UserSignupDto @@ -20,4 +21,10 @@ final class UserSignupDto #[Assert\NotBlank()] #[Assert\Length(min: 8)] public string $password; + + /** + * Is this User for an individual acting on their own or a group of individuals? + */ + #[Assert\NotBlank()] + public UserType $type = UserType::Individual; } From c68270e7628f512de67bed85da0b611f81ed5433 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 15:47:52 +0100 Subject: [PATCH 09/28] Refactor UserPersonal into Person --- src/Entity/User/Person.php | 98 +++++++++++++++++++ src/Entity/User/User.php | 43 ++++---- src/Entity/User/UserPersonal.php | 92 ----------------- src/Repository/User/PersonRepository.php | 23 +++++ .../User/UserPersonalRepository.php | 23 ----- 5 files changed, 143 insertions(+), 136 deletions(-) create mode 100644 src/Entity/User/Person.php delete mode 100644 src/Entity/User/UserPersonal.php create mode 100644 src/Repository/User/PersonRepository.php delete mode 100644 src/Repository/User/UserPersonalRepository.php diff --git a/src/Entity/User/Person.php b/src/Entity/User/Person.php new file mode 100644 index 0000000..b47bbd7 --- /dev/null +++ b/src/Entity/User/Person.php @@ -0,0 +1,98 @@ +setUser($user); + + return $person; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getTaxId(): ?string + { + return $this->taxId; + } + + public function setTaxId(?string $taxId): static + { + $this->taxId = $taxId; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } +} diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index fd31e08..d4f4220 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -75,6 +75,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\OneToMany(mappedBy: 'owner', targetEntity: UserToken::class, orphanRemoval: true)] private Collection $tokens; + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?Person $personalData = null; + /** * @var list The user roles. Admin only property. */ @@ -93,9 +96,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\Column] private ?bool $active = null; - #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] - private ?UserPersonal $personalData = null; - /** * Is this user for an individual acting on their own or a larger group of individuals? */ @@ -112,8 +112,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account { $this->accounting = Accounting::of($this); - $this->tokens = new ArrayCollection(); $this->projects = new ArrayCollection(); + $this->tokens = new ArrayCollection(); + $this->personalData = Person::for($this); $this->emailConfirmed = false; $this->active = false; @@ -250,6 +251,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + public function getPersonalData(): ?Person + { + return $this->personalData; + } + + public function setPersonalData(Person $personalData): static + { + // set the owning side of the relation if necessary + if ($personalData->getUser() !== $this) { + $personalData->setUser($this); + } + + $this->personalData = $personalData; + + return $this; + } + /** * @see UserInterface * @@ -303,23 +321,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function getPersonalData(): ?UserPersonal - { - return $this->personalData; - } - - public function setPersonalData(UserPersonal $personalData): static - { - // set the owning side of the relation if necessary - if ($personalData->getUser() !== $this) { - $personalData->setUser($this); - } - - $this->personalData = $personalData; - - return $this; - } - public function getType(): ?UserType { return $this->type; diff --git a/src/Entity/User/UserPersonal.php b/src/Entity/User/UserPersonal.php deleted file mode 100644 index 6417864..0000000 --- a/src/Entity/User/UserPersonal.php +++ /dev/null @@ -1,92 +0,0 @@ -user; - } - - public function setUser(User $user): static - { - $this->user = $user; - - return $this; - } - - public function getIdentityDocument(): ?string - { - return $this->identityDocument; - } - - public function setIdentityDocument(?string $identityDocument): static - { - $this->identityDocument = $identityDocument; - - return $this; - } - - public function getIndetityDocumentType(): ?string - { - return $this->identityDocumentType; - } - - public function setIndetityDocumentType(?string $identityDocumentType): static - { - $this->identityDocumentType = $identityDocumentType; - - return $this; - } - - public function getPostalCode(): ?string - { - return $this->postalCode; - } - - public function setPostalCode(?string $postalCode): static - { - $this->postalCode = $postalCode; - - return $this; - } -} diff --git a/src/Repository/User/PersonRepository.php b/src/Repository/User/PersonRepository.php new file mode 100644 index 0000000..4b71c92 --- /dev/null +++ b/src/Repository/User/PersonRepository.php @@ -0,0 +1,23 @@ + + * + * @method Person|null find($id, $lockMode = null, $lockVersion = null) + * @method Person|null findOneBy(array $criteria, array $orderBy = null) + * @method Person[] findAll() + * @method Person[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PersonRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Person::class); + } +} diff --git a/src/Repository/User/UserPersonalRepository.php b/src/Repository/User/UserPersonalRepository.php deleted file mode 100644 index 0bf629a..0000000 --- a/src/Repository/User/UserPersonalRepository.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * @method UserPersonal|null find($id, $lockMode = null, $lockVersion = null) - * @method UserPersonal|null findOneBy(array $criteria, array $orderBy = null) - * @method UserPersonal[] findAll() - * @method UserPersonal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ -class UserPersonalRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, UserPersonal::class); - } -} From 9cf4855542b8b6580fb87c7167986ac408d51101 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 15:56:46 +0100 Subject: [PATCH 10/28] Expose PersonApiResource --- src/ApiResource/User/PersonApiResource.php | 62 ++++++++++++++++++++++ src/ApiResource/User/UserApiResource.php | 7 +++ src/Entity/User/Person.php | 3 ++ src/OpenApi/RedocExtensionsTrait.php | 2 +- 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/ApiResource/User/PersonApiResource.php diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php new file mode 100644 index 0000000..90814eb --- /dev/null +++ b/src/ApiResource/User/PersonApiResource.php @@ -0,0 +1,62 @@ + 'Users', - 'tags' => ['User', 'UserToken', 'UserPersonal'], + 'tags' => ['User', 'UserToken', 'Person'], ], [ 'name' => 'Projects', From 256e8403c25a01bf2f2bbe505e0e289eb669988c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 24 Feb 2025 15:56:58 +0100 Subject: [PATCH 11/28] Add person security voter --- src/Security/Voter/PersonVoter.php | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Security/Voter/PersonVoter.php diff --git a/src/Security/Voter/PersonVoter.php b/src/Security/Voter/PersonVoter.php new file mode 100644 index 0000000..436275f --- /dev/null +++ b/src/Security/Voter/PersonVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::EDIT: + return $subject->user->id === $user->getId(); + case self::VIEW: + return $user->hasRoles(['ROLE_ADMIN']) + || $subject->user->id === $user->getId(); + } + + return false; + } +} From 019d517faefff1703ae3a32313bf4000c2642475 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 02:19:44 +0100 Subject: [PATCH 12/28] Update user <-> person relationship spec --- src/ApiResource/User/PersonApiResource.php | 11 +++++++++-- src/ApiResource/User/UserApiResource.php | 2 +- src/Entity/User/Person.php | 2 +- src/Entity/User/User.php | 16 ++++++++-------- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php index 90814eb..a625d83 100644 --- a/src/ApiResource/User/PersonApiResource.php +++ b/src/ApiResource/User/PersonApiResource.php @@ -18,11 +18,18 @@ use App\State\ApiResourceStateProvider; */ #[API\ApiResource( shortName: 'Person', - stateOptions: new Options(entityClass: Person::class), security: 'is_granted("PERSON_VIEW", object)', securityPostDenormalize: 'is_granted("PERSON_EDIT", object)', + stateOptions: new Options(entityClass: Person::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class + processor: ApiResourceStateProcessor::class, + uriTemplate: '/users/{id}/person', + uriVariables: [ + 'id' => new API\Link( + fromClass: UserApiResource::class, + fromProperty: 'person' + ) + ] )] #[API\Get()] #[API\Patch()] diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 724c5ae..4cb617e 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -76,7 +76,7 @@ class UserApiResource * for `organization` User types: personal data for the organization representative. */ #[API\ApiProperty(writable: false)] - public ?PersonApiResource $personal = null; + public ?PersonApiResource $person = null; /** * The Accounting for this User monetary movements. diff --git a/src/Entity/User/Person.php b/src/Entity/User/Person.php index ea252c7..a990362 100644 --- a/src/Entity/User/Person.php +++ b/src/Entity/User/Person.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; class Person { #[ORM\Id] - #[ORM\OneToOne(inversedBy: 'personalData', cascade: ['persist', 'remove'])] + #[ORM\OneToOne(inversedBy: 'person', cascade: ['persist', 'remove'])] #[ORM\JoinColumn(nullable: false)] private ?User $user = null; diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index d4f4220..7111c0f 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -76,7 +76,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account private Collection $tokens; #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] - private ?Person $personalData = null; + private ?Person $person = null; /** * @var list The user roles. Admin only property. @@ -114,7 +114,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account $this->projects = new ArrayCollection(); $this->tokens = new ArrayCollection(); - $this->personalData = Person::for($this); + $this->person = Person::for($this); $this->emailConfirmed = false; $this->active = false; @@ -251,19 +251,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function getPersonalData(): ?Person + public function getPerson(): ?Person { - return $this->personalData; + return $this->person; } - public function setPersonalData(Person $personalData): static + public function setPerson(Person $person): static { // set the owning side of the relation if necessary - if ($personalData->getUser() !== $this) { - $personalData->setUser($this); + if ($person->getUser() !== $this) { + $person->setUser($this); } - $this->personalData = $personalData; + $this->person = $person; return $this; } From b10ef0581b072e81cf53fc6c1f3927f507855af6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 02:29:42 +0100 Subject: [PATCH 13/28] Add map provider for person entity --- src/Entity/User/Person.php | 4 ++-- src/Mapping/Provider/PersonMapProvider.php | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/Mapping/Provider/PersonMapProvider.php diff --git a/src/Entity/User/Person.php b/src/Entity/User/Person.php index a990362..0b008fd 100644 --- a/src/Entity/User/Person.php +++ b/src/Entity/User/Person.php @@ -3,7 +3,7 @@ namespace App\Entity\User; use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; -use App\Mapping\Provider\EntityMapProvider; +use App\Mapping\Provider\PersonMapProvider; use App\Repository\User\PersonRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\ORM\Mapping as ORM; @@ -15,7 +15,7 @@ use Doctrine\ORM\Mapping as ORM; * \ * Sensitive personal data is encrypted before being stored on the database. */ -#[MapProvider(EntityMapProvider::class)] +#[MapProvider(PersonMapProvider::class)] #[ORM\Entity(repositoryClass: PersonRepository::class)] class Person { diff --git a/src/Mapping/Provider/PersonMapProvider.php b/src/Mapping/Provider/PersonMapProvider.php new file mode 100644 index 0000000..7a60953 --- /dev/null +++ b/src/Mapping/Provider/PersonMapProvider.php @@ -0,0 +1,22 @@ +personRepository->find($source->user->id); + } +} From cb80a0607e291a068abd7116845cacadc520bd09 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 02:29:53 +0100 Subject: [PATCH 14/28] Bugfix on person api resource security --- src/ApiResource/User/PersonApiResource.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php index a625d83..1da679a 100644 --- a/src/ApiResource/User/PersonApiResource.php +++ b/src/ApiResource/User/PersonApiResource.php @@ -19,7 +19,6 @@ use App\State\ApiResourceStateProvider; #[API\ApiResource( shortName: 'Person', security: 'is_granted("PERSON_VIEW", object)', - securityPostDenormalize: 'is_granted("PERSON_EDIT", object)', stateOptions: new Options(entityClass: Person::class), provider: ApiResourceStateProvider::class, processor: ApiResourceStateProcessor::class, @@ -32,7 +31,9 @@ use App\State\ApiResourceStateProvider; ] )] #[API\Get()] -#[API\Patch()] +#[API\Patch( + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', +)] class PersonApiResource { #[API\ApiProperty(identifier: true, writable: false)] @@ -42,8 +43,7 @@ class PersonApiResource * Personal ID for tax purposes. e.g: NIF, Steuernummer, SSN, etc. */ #[API\ApiProperty( - security: 'is_granted("PERSON_VIEW", object)', - securityPostDenormalize: 'is_granted("PERSON_EDIT", object)', + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', )] public string $taxId; @@ -52,8 +52,7 @@ class PersonApiResource * in most western conventions this is the given name(s). e.g: John, Juan, etc. */ #[API\ApiProperty( - security: 'is_granted("PERSON_VIEW", object)', - securityPostDenormalize: 'is_granted("PERSON_EDIT", object)', + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', )] public string $firstName; @@ -62,8 +61,7 @@ class PersonApiResource * in most western conventions this is the family name(s). e.g: Smith, Herrera García, etc. */ #[API\ApiProperty( - security: 'is_granted("PERSON_VIEW", object)', - securityPostDenormalize: 'is_granted("PERSON_EDIT", object)', + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', )] public string $lastName; } From 34da4eef4f04f17d8e853cadb963773fd5b59e05 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 02:46:02 +0100 Subject: [PATCH 15/28] Add calculated display name to user api resource --- src/ApiResource/User/PersonApiResource.php | 2 +- src/ApiResource/User/UserApiResource.php | 6 ++++ src/Entity/User/Person.php | 4 +-- .../UserDisplayNameMapTransformer.php | 34 +++++++++++++++++++ src/Service/UserService.php | 3 +- 5 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/Mapping/Transformer/UserDisplayNameMapTransformer.php diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php index 1da679a..ec68fe9 100644 --- a/src/ApiResource/User/PersonApiResource.php +++ b/src/ApiResource/User/PersonApiResource.php @@ -27,7 +27,7 @@ use App\State\ApiResourceStateProvider; 'id' => new API\Link( fromClass: UserApiResource::class, fromProperty: 'person' - ) + ), ] )] #[API\Get()] diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 4cb617e..15e32ba 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -10,9 +10,11 @@ use App\Entity\User\User; use App\Entity\User\UserType; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; +use App\Mapping\Transformer\UserDisplayNameMapTransformer; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; use App\State\User\UserSignupProcessor; +use AutoMapper\Attribute\MapFrom; use Symfony\Component\Validator\Constraints as Assert; /** @@ -71,6 +73,10 @@ class UserApiResource )] public array $roles; + #[API\ApiProperty(writable: false)] + #[MapFrom(User::class, transformer: UserDisplayNameMapTransformer::class)] + public string $displayName; + /** * For `individual` User types: personal data about the User themselves, * for `organization` User types: personal data for the organization representative. diff --git a/src/Entity/User/Person.php b/src/Entity/User/Person.php index 0b008fd..00b8b2d 100644 --- a/src/Entity/User/Person.php +++ b/src/Entity/User/Person.php @@ -32,13 +32,13 @@ class Person private ?string $taxId = null; /** - * First-part of the name of the person, usually the given name(s). e.g: John, Juan + * First-part of the name of the person, usually the given name(s). e.g: John, Juan. */ #[ORM\Column(length: 255, nullable: true)] private ?string $firstName = null; /** - * Last-part of the name of the person, usually the family name(s). e.g: Smith, Herrera García + * Last-part of the name of the person, usually the family name(s). e.g: Smith, Herrera García. */ #[ORM\Column(length: 255, nullable: true)] private ?string $lastName = null; diff --git a/src/Mapping/Transformer/UserDisplayNameMapTransformer.php b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php new file mode 100644 index 0000000..53c8546 --- /dev/null +++ b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php @@ -0,0 +1,34 @@ +getType()) { + case UserType::Individual: + return $this->individualDisplayName($source); + } + } + + private function individualDisplayName(User $user): string + { + $person = $user->getPerson(); + + $name = \sprintf( + '%s %s', + $person->getFirstName(), + $person->getLastName() + ); + + return \trim($name); + } +} diff --git a/src/Service/UserService.php b/src/Service/UserService.php index cd37db1..84633f7 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -6,8 +6,9 @@ class UserService { /** * Normalizes a string into a handle-valid string. - * + * * @return string A handle-valid string + * * @throws \Exception If the given value could not be normalized into a compliant handle string */ public static function asHandle(string $value): string From ea4b1aa80b9f78562a49fec2e10698f31ed8a1e7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 13:29:55 +0100 Subject: [PATCH 16/28] Bugfixes --- src/Entity/User/User.php | 4 ++-- tests/Entity/ProjectApiTest.php | 2 +- tests/Gateway/Wallet/WalletServiceTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 7111c0f..2a953fa 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -100,7 +100,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account * Is this user for an individual acting on their own or a larger group of individuals? */ #[ORM\Column(enumType: UserType::class)] - private ?UserType $type = null; + private UserType $type = UserType::Individual; /** * URL to the avatar image of this User. @@ -321,7 +321,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function getType(): ?UserType + public function getType(): UserType { return $this->type; } diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index 9debc21..a89abbb 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -35,7 +35,7 @@ class ProjectApiTest extends ApiTestCase $this->assertJsonContains(['member' => []]); $owner = new User(); - $owner->setUsername('test_user'); + $owner->setHandle('test_user'); $owner->setEmail('testuser@example.com'); $owner->setPassword('projectapitestuserpassword'); diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index daa9126..d7c45c7 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -30,7 +30,7 @@ class WalletServiceTest extends KernelTestCase private function getUser(): User { $user = new User(); - $user->setUsername('wallettestuser'); + $user->setHandle('wallettestuser'); $user->setEmail('testuser@wallet.com'); $user->setPassword('wallettestpassword'); From e4f6601c8f6ca54878ad61a1b6dda2db3ef2e03c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 13:31:59 +0100 Subject: [PATCH 17/28] Bump patch versions --- composer.lock | 329 +++++++++++++++++++++++++------------------------- 1 file changed, 166 insertions(+), 163 deletions(-) diff --git a/composer.lock b/composer.lock index 5d0dfe6..71ed5e0 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "api-platform/doctrine-common", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", @@ -89,13 +89,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.0.17" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.0.18" }, "time": "2024-09-21T11:01:16+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", @@ -173,13 +173,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.0.17" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.0.18" }, "time": "2024-12-13T13:34:25+00:00" }, { "name": "api-platform/documentation", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", @@ -234,13 +234,13 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/documentation/tree/v4.1.0-beta.2" }, "time": "2024-09-21T11:01:16+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", @@ -311,13 +311,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/http-cache/tree/v4.1.0-beta.2" }, "time": "2024-10-28T09:21:17+00:00" }, { "name": "api-platform/hydra", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", @@ -395,13 +395,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.0.17" + "source": "https://github.com/api-platform/hydra/tree/v4.0.18" }, "time": "2024-11-06T10:50:57+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", @@ -479,7 +479,7 @@ }, { "name": "api-platform/jsonld", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", @@ -550,13 +550,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/jsonld/tree/v4.1.0-beta.2" }, "time": "2024-12-19T16:01:35+00:00" }, { "name": "api-platform/metadata", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", @@ -646,22 +646,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.0.17" + "source": "https://github.com/api-platform/metadata/tree/v4.0.18" }, "time": "2025-02-07T15:44:47+00:00" }, { "name": "api-platform/openapi", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "3145c45ad6e668e20beff2653c1f245296c33d1c" + "reference": "9d96b31ad3eb524a2cb6f89f835deca858beacd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/3145c45ad6e668e20beff2653c1f245296c33d1c", - "reference": "3145c45ad6e668e20beff2653c1f245296c33d1c", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/9d96b31ad3eb524a2cb6f89f835deca858beacd0", + "reference": "9d96b31ad3eb524a2cb6f89f835deca858beacd0", "shasum": "" }, "require": { @@ -731,13 +731,13 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.0.17" + "source": "https://github.com/api-platform/openapi/tree/v4.0.18" }, - "time": "2025-01-09T14:23:11+00:00" + "time": "2025-02-21T09:07:11+00:00" }, { "name": "api-platform/serializer", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", @@ -821,13 +821,13 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.0.17" + "source": "https://github.com/api-platform/serializer/tree/v4.1.0-beta.2" }, "time": "2025-02-06T09:32:58+00:00" }, { "name": "api-platform/state", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", @@ -911,22 +911,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.0.17" + "source": "https://github.com/api-platform/state/tree/v4.0.18" }, "time": "2025-01-13T10:20:09+00:00" }, { "name": "api-platform/symfony", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "ea372108b0367ce027d28beef0dbadb454e81ca1" + "reference": "83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/ea372108b0367ce027d28beef0dbadb454e81ca1", - "reference": "ea372108b0367ce027d28beef0dbadb454e81ca1", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d", + "reference": "83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d", "shasum": "" }, "require": { @@ -1034,13 +1034,13 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.0.17" + "source": "https://github.com/api-platform/symfony/tree/v4.0.18" }, - "time": "2025-02-06T09:04:42+00:00" + "time": "2025-02-14T09:53:32+00:00" }, { "name": "api-platform/validator", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", @@ -2872,25 +2872,24 @@ }, { "name": "gedmo/doctrine-extensions", - "version": "v3.17.1", + "version": "v3.19.0", "source": { "type": "git", "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900" + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/5b0b8a442d19e6701ae64535dc08f7944e2895d2", + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2", "shasum": "" }, "require": { "behat/transliterator": "^1.2", "doctrine/collections": "^1.2 || ^2.0", - "doctrine/common": "^2.13 || ^3.0", "doctrine/deprecations": "^1.0", "doctrine/event-manager": "^1.2 || ^2.0", - "doctrine/persistence": "^2.2 || ^3.0", + "doctrine/persistence": "^2.2 || ^3.0 || ^4.0", "php": "^7.4 || ^8.0", "psr/cache": "^1 || ^2 || ^3", "psr/clock": "^1", @@ -2898,24 +2897,26 @@ }, "conflict": { "doctrine/annotations": "<1.13 || >=3.0", + "doctrine/common": "<2.13 || >=4.0", "doctrine/dbal": "<3.7 || >=5.0", "doctrine/mongodb-odm": "<2.3 || >=3.0", - "doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=4.0" + "doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0" }, "require-dev": { "doctrine/annotations": "^1.13 || ^2.0", "doctrine/cache": "^1.11 || ^2.0", + "doctrine/common": "^2.13 || ^3.0", "doctrine/dbal": "^3.7 || ^4.0", "doctrine/doctrine-bundle": "^2.3", "doctrine/mongodb-odm": "^2.3", - "doctrine/orm": "^2.14.0 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.14.0", + "doctrine/orm": "^2.20 || ^3.3", + "friendsofphp/php-cs-fixer": "^3.70", "nesbot/carbon": "^2.71 || ^3.0", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-doctrine": "^1.4", - "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan": "^2.1.1", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.3", "phpunit/phpunit": "^9.6", - "rector/rector": "^1.1", + "rector/rector": "^2.0.6", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/phpunit-bridge": "^6.0 || ^7.0", @@ -2977,7 +2978,7 @@ "support": { "email": "gediminas.morkevicius@gmail.com", "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", - "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.19.0", "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" }, "funding": [ @@ -2998,7 +2999,7 @@ "type": "github" } ], - "time": "2024-10-07T22:30:27+00:00" + "time": "2025-02-24T22:12:57+00:00" }, { "name": "goteo/benzina", @@ -4559,16 +4560,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "70d60e9a3603108563010f8592dff15a6f15dfae" + "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/70d60e9a3603108563010f8592dff15a6f15dfae", - "reference": "70d60e9a3603108563010f8592dff15a6f15dfae", + "url": "https://api.github.com/repos/symfony/cache/zipball/b209751ed25f735ea90ca4c9c969d9413a17dfee", + "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee", "shasum": "" }, "require": { @@ -4635,7 +4636,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.16" + "source": "https://github.com/symfony/cache/tree/v6.4.18" }, "funding": [ { @@ -4651,7 +4652,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T10:10:54+00:00" + "time": "2025-01-22T14:13:52+00:00" }, { "name": "symfony/cache-contracts", @@ -5122,21 +5123,21 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "2ba7e747a944b69f9f583c35173560afebbff995" + "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/2ba7e747a944b69f9f583c35173560afebbff995", - "reference": "2ba7e747a944b69f9f583c35173560afebbff995", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", + "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", "shasum": "" }, "require": { "doctrine/event-manager": "^1.2|^2", - "doctrine/persistence": "^3.1", + "doctrine/persistence": "^2.5|^3.1|^4", "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", @@ -5210,7 +5211,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.17" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.18" }, "funding": [ { @@ -5226,7 +5227,7 @@ "type": "tidelift" } ], - "time": "2024-12-18T10:42:42+00:00" + "time": "2025-01-25T08:04:58+00:00" }, { "name": "symfony/dotenv", @@ -5304,16 +5305,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "37ad2380e8c1a8cf62a1200a5c10080b679b446c" + "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/37ad2380e8c1a8cf62a1200a5c10080b679b446c", - "reference": "37ad2380e8c1a8cf62a1200a5c10080b679b446c", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", + "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", "shasum": "" }, "require": { @@ -5359,7 +5360,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.17" + "source": "https://github.com/symfony/error-handler/tree/v6.4.18" }, "funding": [ { @@ -5375,7 +5376,7 @@ "type": "tidelift" } ], - "time": "2024-12-06T13:30:51+00:00" + "time": "2025-01-06T09:38:16+00:00" }, { "name": "symfony/event-dispatcher", @@ -5797,16 +5798,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "17d8ae2e7aa77154f942e8ac48849ac718b0963f" + "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/17d8ae2e7aa77154f942e8ac48849ac718b0963f", - "reference": "17d8ae2e7aa77154f942e8ac48849ac718b0963f", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", + "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", "shasum": "" }, "require": { @@ -5926,7 +5927,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.17" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.18" }, "funding": [ { @@ -5942,20 +5943,20 @@ "type": "tidelift" } ], - "time": "2024-12-19T14:08:41+00:00" + "time": "2025-01-28T18:47:02+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "88898d842eb29d7e1a903724c94e90a6ca9c0509" + "reference": "394b440934056b8d9d6ba250001458e9d7998b7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/88898d842eb29d7e1a903724c94e90a6ca9c0509", - "reference": "88898d842eb29d7e1a903724c94e90a6ca9c0509", + "url": "https://api.github.com/repos/symfony/http-client/zipball/394b440934056b8d9d6ba250001458e9d7998b7f", + "reference": "394b440934056b8d9d6ba250001458e9d7998b7f", "shasum": "" }, "require": { @@ -6019,7 +6020,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.17" + "source": "https://github.com/symfony/http-client/tree/v6.4.18" }, "funding": [ { @@ -6035,7 +6036,7 @@ "type": "tidelift" } ], - "time": "2024-12-18T12:18:31+00:00" + "time": "2025-01-28T15:49:13+00:00" }, { "name": "symfony/http-client-contracts", @@ -6117,16 +6118,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57" + "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/431771b7a6f662f1575b3cfc8fd7617aa9864d57", - "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0492d6217e5ab48f51fca76f64cf8e78919d0db", + "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db", "shasum": "" }, "require": { @@ -6174,7 +6175,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.16" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.18" }, "funding": [ { @@ -6190,20 +6191,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T18:58:10+00:00" + "time": "2025-01-09T15:48:56+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "c5647393c5ce11833d13e4b70fff4b571d4ac710" + "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c5647393c5ce11833d13e4b70fff4b571d4ac710", - "reference": "c5647393c5ce11833d13e4b70fff4b571d4ac710", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", + "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", "shasum": "" }, "require": { @@ -6288,7 +6289,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.17" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.18" }, "funding": [ { @@ -6304,7 +6305,7 @@ "type": "tidelift" } ], - "time": "2024-12-31T14:49:31+00:00" + "time": "2025-01-29T07:25:58+00:00" }, { "name": "symfony/intl", @@ -7096,16 +7097,16 @@ }, { "name": "symfony/property-access", - "version": "v6.4.13", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "8cc779d88d12e440adaa26387bcfc25744064afe" + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/8cc779d88d12e440adaa26387bcfc25744064afe", - "reference": "8cc779d88d12e440adaa26387bcfc25744064afe", + "url": "https://api.github.com/repos/symfony/property-access/zipball/80e0378f2f058b60d87dedc3c760caec882e992c", + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c", "shasum": "" }, "require": { @@ -7153,7 +7154,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.13" + "source": "https://github.com/symfony/property-access/tree/v6.4.18" }, "funding": [ { @@ -7169,20 +7170,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2024-12-16T14:42:05+00:00" }, { "name": "symfony/property-info", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "38b125d78e67668159f75383a293ec0c5d3f2963" + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/38b125d78e67668159f75383a293ec0c5d3f2963", - "reference": "38b125d78e67668159f75383a293ec0c5d3f2963", + "url": "https://api.github.com/repos/symfony/property-info/zipball/94d18e5cc11a37fd92856d38b61d9cdf72536a1e", + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e", "shasum": "" }, "require": { @@ -7193,7 +7194,9 @@ "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<5.4|>=6.0,<6.4" + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", @@ -7237,7 +7240,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.4.17" + "source": "https://github.com/symfony/property-info/tree/v6.4.18" }, "funding": [ { @@ -7253,20 +7256,20 @@ "type": "tidelift" } ], - "time": "2024-12-26T19:01:29+00:00" + "time": "2025-01-21T10:52:27+00:00" }, { "name": "symfony/routing", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220" + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/91e02e606b4b705c2f4fb42f7e7708b7923a3220", - "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220", + "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", "shasum": "" }, "require": { @@ -7320,7 +7323,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.16" + "source": "https://github.com/symfony/routing/tree/v6.4.18" }, "funding": [ { @@ -7336,7 +7339,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T15:31:34+00:00" + "time": "2025-01-09T08:51:02+00:00" }, { "name": "symfony/runtime", @@ -7531,16 +7534,16 @@ }, { "name": "symfony/security-core", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "19cdb7de86e556202ab16e0cffd1a97348231bc0" + "reference": "0ae7ae716968e00287ab9b7768405e0dc9cad109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/19cdb7de86e556202ab16e0cffd1a97348231bc0", - "reference": "19cdb7de86e556202ab16e0cffd1a97348231bc0", + "url": "https://api.github.com/repos/symfony/security-core/zipball/0ae7ae716968e00287ab9b7768405e0dc9cad109", + "reference": "0ae7ae716968e00287ab9b7768405e0dc9cad109", "shasum": "" }, "require": { @@ -7597,7 +7600,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.16" + "source": "https://github.com/symfony/security-core/tree/v6.4.18" }, "funding": [ { @@ -7613,7 +7616,7 @@ "type": "tidelift" } ], - "time": "2024-11-27T09:48:51+00:00" + "time": "2025-01-22T20:59:03+00:00" }, { "name": "symfony/security-csrf", @@ -7685,16 +7688,16 @@ }, { "name": "symfony/security-http", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "ded1e078f952e686b058d9eac98e497bea47b308" + "reference": "54f2ccce1f3822eee3fb3a85debd9a67d12762b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/ded1e078f952e686b058d9eac98e497bea47b308", - "reference": "ded1e078f952e686b058d9eac98e497bea47b308", + "url": "https://api.github.com/repos/symfony/security-http/zipball/54f2ccce1f3822eee3fb3a85debd9a67d12762b8", + "reference": "54f2ccce1f3822eee3fb3a85debd9a67d12762b8", "shasum": "" }, "require": { @@ -7753,7 +7756,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.15" + "source": "https://github.com/symfony/security-http/tree/v6.4.18" }, "funding": [ { @@ -7769,20 +7772,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:18+00:00" + "time": "2025-01-28T14:53:52+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "9d862d66198f3c2e30404228629ef4c18d5d608e" + "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/9d862d66198f3c2e30404228629ef4c18d5d608e", - "reference": "9d862d66198f3c2e30404228629ef4c18d5d608e", + "url": "https://api.github.com/repos/symfony/serializer/zipball/6ad986f62276da4c8c69754decfaa445a89cb6e3", + "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3", "shasum": "" }, "require": { @@ -7851,7 +7854,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.15" + "source": "https://github.com/symfony/serializer/tree/v6.4.18" }, "funding": [ { @@ -7867,7 +7870,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T13:25:59+00:00" + "time": "2025-01-28T18:47:02+00:00" }, { "name": "symfony/service-contracts", @@ -8522,16 +8525,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "a3c19a0e542d427c207e22242043ef35b5b99a2c" + "reference": "ce20367d07b2592202e9c266b16a93fa50145207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/a3c19a0e542d427c207e22242043ef35b5b99a2c", - "reference": "a3c19a0e542d427c207e22242043ef35b5b99a2c", + "url": "https://api.github.com/repos/symfony/validator/zipball/ce20367d07b2592202e9c266b16a93fa50145207", + "reference": "ce20367d07b2592202e9c266b16a93fa50145207", "shasum": "" }, "require": { @@ -8599,7 +8602,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.17" + "source": "https://github.com/symfony/validator/tree/v6.4.18" }, "funding": [ { @@ -8615,20 +8618,20 @@ "type": "tidelift" } ], - "time": "2024-12-29T12:50:19+00:00" + "time": "2025-01-27T16:05:44+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80" + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80", - "reference": "38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", "shasum": "" }, "require": { @@ -8684,7 +8687,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.15" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" }, "funding": [ { @@ -8700,7 +8703,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T15:28:48+00:00" + "time": "2025-01-17T11:26:11+00:00" }, { "name": "symfony/var-exporter", @@ -8864,16 +8867,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.13", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9" + "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", + "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", "shasum": "" }, "require": { @@ -8916,7 +8919,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.13" + "source": "https://github.com/symfony/yaml/tree/v6.4.18" }, "funding": [ { @@ -8932,7 +8935,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-01-07T09:44:41+00:00" }, { "name": "twig/twig", @@ -9511,16 +9514,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.69.1", + "version": "v3.70.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "13b0c0eede38c11cd674b080f2b485d0f14ffa9f" + "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/13b0c0eede38c11cd674b080f2b485d0f14ffa9f", - "reference": "13b0c0eede38c11cd674b080f2b485d0f14ffa9f", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", + "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", "shasum": "" }, "require": { @@ -9602,7 +9605,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.69.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.70.0" }, "funding": [ { @@ -9610,7 +9613,7 @@ "type": "github" } ], - "time": "2025-02-18T23:57:43+00:00" + "time": "2025-02-22T23:30:51+00:00" }, { "name": "masterminds/html5", @@ -11903,16 +11906,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4304e6ad5c894a9c72831ad459f627bfd35d766d" + "reference": "fd07959d3e8992795029bdab3605c2e8e895034e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4304e6ad5c894a9c72831ad459f627bfd35d766d", - "reference": "4304e6ad5c894a9c72831ad459f627bfd35d766d", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/fd07959d3e8992795029bdab3605c2e8e895034e", + "reference": "fd07959d3e8992795029bdab3605c2e8e895034e", "shasum": "" }, "require": { @@ -11950,7 +11953,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.16" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.18" }, "funding": [ { @@ -11966,7 +11969,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T15:06:22+00:00" + "time": "2025-01-09T15:35:00+00:00" }, { "name": "symfony/maker-bundle", @@ -12381,16 +12384,16 @@ }, { "name": "zenstruck/foundry", - "version": "v2.3.4", + "version": "v2.3.5", "source": { "type": "git", "url": "https://github.com/zenstruck/foundry.git", - "reference": "19d4f79767fa6505ad4092cdb9542de0a747cd85" + "reference": "acd5dfc270bb77d48d62090d27a63d085fddf813" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/19d4f79767fa6505ad4092cdb9542de0a747cd85", - "reference": "19d4f79767fa6505ad4092cdb9542de0a747cd85", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/acd5dfc270bb77d48d62090d27a63d085fddf813", + "reference": "acd5dfc270bb77d48d62090d27a63d085fddf813", "shasum": "" }, "require": { @@ -12474,7 +12477,7 @@ ], "support": { "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v2.3.4" + "source": "https://github.com/zenstruck/foundry/tree/v2.3.5" }, "funding": [ { @@ -12482,7 +12485,7 @@ "type": "github" } ], - "time": "2025-02-14T11:35:54+00:00" + "time": "2025-02-24T13:07:31+00:00" } ], "aliases": [], From b810b80a36ad4a45b13fced237eb64e4380ffc0d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:07:27 +0100 Subject: [PATCH 18/28] Update person tax id examples --- src/ApiResource/User/PersonApiResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php index ec68fe9..ffcc344 100644 --- a/src/ApiResource/User/PersonApiResource.php +++ b/src/ApiResource/User/PersonApiResource.php @@ -40,7 +40,7 @@ class PersonApiResource public UserApiResource $user; /** - * Personal ID for tax purposes. e.g: NIF, Steuernummer, SSN, etc. + * Personal ID for tax purposes. e.g: NIF, Steuer-ID, SSN, ITIN, etc. */ #[API\ApiProperty( securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', From 00a886679f9b45959930ab1b28f06aa82166380e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:07:46 +0100 Subject: [PATCH 19/28] Add Organization api resource --- .../User/OrganizationApiResource.php | 69 +++++++++++++++++++ src/OpenApi/RedocExtensionsTrait.php | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/ApiResource/User/OrganizationApiResource.php diff --git a/src/ApiResource/User/OrganizationApiResource.php b/src/ApiResource/User/OrganizationApiResource.php new file mode 100644 index 0000000..fe32eb9 --- /dev/null +++ b/src/ApiResource/User/OrganizationApiResource.php @@ -0,0 +1,69 @@ + new API\Link( + fromClass: UserApiResource::class, + fromProperty: 'Organization' + ), + ] +)] +#[API\Get()] +#[API\Patch( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', +)] +class OrganizationApiResource +{ + #[API\ApiProperty(identifier: true, writable: false)] + public UserApiResource $user; + + /** + * ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + #[Assert\NotBlank()] + public string $taxId; + + /** + * Organization legal name before government, + * as it appears on legal documents issued by or for this organization.\ + * Will be used as last option for the display name of the User. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + #[Assert\NotBlank()] + public string $legalName; + + /** + * The name under which the organization presents itself.\ + * Might be similar to the legal name or completely different.\ + * Will be used as display name for the User when present. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + public string $businessName; +} diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index 74cf150..ab60a4c 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -21,7 +21,7 @@ trait RedocExtensionsTrait $groups = [ [ 'name' => 'Users', - 'tags' => ['User', 'UserToken', 'Person'], + 'tags' => ['User', 'UserToken', 'Person', 'Organization'], ], [ 'name' => 'Projects', From 03350f74b0f7102feca7cc89e44dbe736d9cc9f0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:18:04 +0100 Subject: [PATCH 20/28] Add organization entity to db --- .../User/OrganizationApiResource.php | 5 +- src/ApiResource/User/UserApiResource.php | 12 ++- src/Entity/User/Organization.php | 84 +++++++++++++++++++ src/Entity/User/User.php | 20 +++++ .../User/OrganizationRepository.php | 43 ++++++++++ 5 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/Entity/User/Organization.php create mode 100644 src/Repository/User/OrganizationRepository.php diff --git a/src/ApiResource/User/OrganizationApiResource.php b/src/ApiResource/User/OrganizationApiResource.php index fe32eb9..7eb17db 100644 --- a/src/ApiResource/User/OrganizationApiResource.php +++ b/src/ApiResource/User/OrganizationApiResource.php @@ -2,7 +2,9 @@ namespace App\ApiResource\User; +use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; +use App\Entity\User\Organization; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; use Symfony\Component\Validator\Constraints as Assert; @@ -18,13 +20,14 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\ApiResource( shortName: 'Organization', security: 'is_granted("ORGANIZATION_VIEW", object)', + stateOptions: new Options(entityClass: Organization::class), provider: ApiResourceStateProvider::class, processor: ApiResourceStateProcessor::class, uriTemplate: '/users/{id}/organization', uriVariables: [ 'id' => new API\Link( fromClass: UserApiResource::class, - fromProperty: 'Organization' + fromProperty: 'organization' ), ] )] diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 15e32ba..a16940c 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -78,11 +78,17 @@ class UserApiResource public string $displayName; /** - * For `individual` User types: personal data about the User themselves, - * for `organization` User types: personal data for the organization representative. + * For `individual` User types: personal data about the User themselves.\ + * For `organization` User types: data for the organization representative or person managing the User. */ #[API\ApiProperty(writable: false)] - public ?PersonApiResource $person = null; + public PersonApiResource $person; + + /** + * For `organization` User types only. Legal entity data. + */ + #[API\ApiProperty(writable: false)] + public ?OrganizationApiResource $organization = null; /** * The Accounting for this User monetary movements. diff --git a/src/Entity/User/Organization.php b/src/Entity/User/Organization.php new file mode 100644 index 0000000..76cbf2b --- /dev/null +++ b/src/Entity/User/Organization.php @@ -0,0 +1,84 @@ +user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getTaxId(): ?string + { + return $this->taxId; + } + + public function setTaxId(string $taxId): static + { + $this->taxId = $taxId; + + return $this; + } + + public function getLegalName(): ?string + { + return $this->legalName; + } + + public function setLegalName(string $legalName): static + { + $this->legalName = $legalName; + + return $this; + } + + public function getBusinessName(): ?string + { + return $this->businessName; + } + + public function setBusinessName(?string $businessName): static + { + $this->businessName = $businessName; + + return $this; + } +} diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 2a953fa..6ba6692 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -78,6 +78,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] private ?Person $person = null; + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?Organization $organization = null; + /** * @var list The user roles. Admin only property. */ @@ -268,6 +271,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + public function getOrganization(): ?Organization + { + return $this->organization; + } + + public function setOrganization(Organization $organization): static + { + // set the owning side of the relation if necessary + if ($organization->getUser() !== $this) { + $organization->setUser($this); + } + + $this->organization = $organization; + + return $this; + } + /** * @see UserInterface * diff --git a/src/Repository/User/OrganizationRepository.php b/src/Repository/User/OrganizationRepository.php new file mode 100644 index 0000000..3215719 --- /dev/null +++ b/src/Repository/User/OrganizationRepository.php @@ -0,0 +1,43 @@ + + */ +class OrganizationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Organization::class); + } + + // /** + // * @return Organization[] Returns an array of Organization objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('o') + // ->andWhere('o.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('o.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Organization + // { + // return $this->createQueryBuilder('o') + // ->andWhere('o.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} From 82b946be8bbe1fe08371021b151ba975cf0a9698 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:19:51 +0100 Subject: [PATCH 21/28] Add organization voter --- src/Security/Voter/OrganizationVoter.php | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Security/Voter/OrganizationVoter.php diff --git a/src/Security/Voter/OrganizationVoter.php b/src/Security/Voter/OrganizationVoter.php new file mode 100644 index 0000000..f618f89 --- /dev/null +++ b/src/Security/Voter/OrganizationVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::EDIT: + return $subject->user->id === $user->getId(); + case self::VIEW: + return $user->hasRoles(['ROLE_ADMIN']) + || $subject->user->id === $user->getId(); + } + + return false; + } +} From 4ba6040c3824aef99972b06344c731c435de91eb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:43:49 +0100 Subject: [PATCH 22/28] Handle display name of organization type users --- .../Transformer/UserDisplayNameMapTransformer.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Mapping/Transformer/UserDisplayNameMapTransformer.php b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php index 53c8546..187c505 100644 --- a/src/Mapping/Transformer/UserDisplayNameMapTransformer.php +++ b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php @@ -16,6 +16,8 @@ class UserDisplayNameMapTransformer implements PropertyTransformerInterface switch ($source->getType()) { case UserType::Individual: return $this->individualDisplayName($source); + case UserType::Organization: + return $this->organizationDisplayName($source); } } @@ -31,4 +33,17 @@ class UserDisplayNameMapTransformer implements PropertyTransformerInterface return \trim($name); } + + private function organizationDisplayName(User $user): string + { + $org = $user->getOrganization(); + + $name = $org->getBusinessName(); + + if (empty($name)) { + $name = $org->getBusinessName(); + } + + return $name ?? ''; + } } From 665c5501d3028b098f9f901f3b166ec57fc8c249 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:44:22 +0100 Subject: [PATCH 23/28] Add handy isType user type method check --- src/Entity/User/User.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 6ba6692..3e12ccc 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -346,6 +346,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this->type; } + public function isType(UserType $type): bool + { + return $type === $this->getType(); + } + public function setType(UserType $type): static { $this->type = $type; From 455f5c64a65c59d6c1756ab022e5e234a379fa76 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:44:46 +0100 Subject: [PATCH 24/28] Add map provider for org entity --- .../Provider/OrganizationMapProvider.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Mapping/Provider/OrganizationMapProvider.php diff --git a/src/Mapping/Provider/OrganizationMapProvider.php b/src/Mapping/Provider/OrganizationMapProvider.php new file mode 100644 index 0000000..44cab35 --- /dev/null +++ b/src/Mapping/Provider/OrganizationMapProvider.php @@ -0,0 +1,22 @@ +orgRepository->find($source->user->id); + } +} From bd649ec903657501727389e8ead704df1239ac4f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:45:02 +0100 Subject: [PATCH 25/28] Update organization entity for signup cases --- src/ApiResource/User/OrganizationApiResource.php | 2 +- src/Entity/User/Organization.php | 15 +++++++++++++-- src/State/User/UserSignupProcessor.php | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ApiResource/User/OrganizationApiResource.php b/src/ApiResource/User/OrganizationApiResource.php index 7eb17db..ac5a11f 100644 --- a/src/ApiResource/User/OrganizationApiResource.php +++ b/src/ApiResource/User/OrganizationApiResource.php @@ -52,7 +52,7 @@ class OrganizationApiResource /** * Organization legal name before government, * as it appears on legal documents issued by or for this organization.\ - * Will be used as last option for the display name of the User. + * Will be used as last option for the display name of the User. */ #[API\ApiProperty( securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', diff --git a/src/Entity/User/Organization.php b/src/Entity/User/Organization.php index 76cbf2b..c28e88d 100644 --- a/src/Entity/User/Organization.php +++ b/src/Entity/User/Organization.php @@ -3,9 +3,12 @@ namespace App\Entity\User; use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; +use App\Mapping\Provider\OrganizationMapProvider; use App\Repository\User\OrganizationRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\ORM\Mapping as ORM; +#[MapProvider(OrganizationMapProvider::class)] #[ORM\Entity(repositoryClass: OrganizationRepository::class)] class Organization { @@ -17,7 +20,7 @@ class Organization /** * ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc. */ - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] #[Encrypted()] private ?string $taxId = null; @@ -25,7 +28,7 @@ class Organization * Organization legal name before government, * as it appears on legal documents issued by or for this organization. */ - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $legalName = null; /** @@ -34,6 +37,14 @@ class Organization #[ORM\Column(length: 255, nullable: true)] private ?string $businessName = null; + public static function for(User $user): Organization + { + $org = new Organization(); + $org->setUser($user); + + return $org; + } + public function getUser(): ?User { return $this->user; diff --git a/src/State/User/UserSignupProcessor.php b/src/State/User/UserSignupProcessor.php index dd8c773..91a63bd 100644 --- a/src/State/User/UserSignupProcessor.php +++ b/src/State/User/UserSignupProcessor.php @@ -6,7 +6,9 @@ use ApiPlatform\Metadata as API; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\User\UserApiResource; use App\Dto\UserSignupDto; +use App\Entity\User\Organization; use App\Entity\User\User; +use App\Entity\User\UserType; use App\Mapping\AutoMapper; use App\Repository\User\UserRepository; use App\Service\UserService; @@ -35,6 +37,10 @@ final class UserSignupProcessor implements ProcessorInterface $user->setHandle($this->buildHandle($data)); $user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password)); + if ($user->isType(UserType::Organization)) { + $user->setOrganization(Organization::for($user)); + } + $user = $this->entityStateProcessor->process($user, $operation, $uriVariables, $context); return $this->autoMapper->map($user, UserApiResource::class); From 01f35a4cc7cbe7136cbfdd379171b033dab6d1be Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 14:49:09 +0100 Subject: [PATCH 26/28] Fix user api resource edit security --- src/ApiResource/User/UserApiResource.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index a16940c..5e4798d 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -29,8 +29,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\GetCollection()] #[API\Post(input: UserSignupDto::class, processor: UserSignupProcessor::class)] #[API\Get()] -#[API\Patch(securityPostDenormalize: 'is_granted("USER_EDIT", object)')] -#[API\Delete(securityPostDenormalize: 'is_granted("USER_EDIT", object)')] +#[API\Patch(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] +#[API\Delete(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] #[API\ApiFilter(filterClass: UserQueryFilter::class, properties: ['query'])] class UserApiResource { @@ -59,7 +59,7 @@ class UserApiResource /** * Is this User for an individual acting on their own or a group of individuals? */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("USER_EDIT")')] + #[API\ApiProperty(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] public UserType $type; /** From 475ae469ad2b24c8949e172a397dc64d6c6b11a5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 15:03:47 +0100 Subject: [PATCH 27/28] Update user state processing --- src/ApiResource/User/UserApiResource.php | 4 ++-- src/State/User/UserStateProcessor.php | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 5e4798d..4833f00 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -11,9 +11,9 @@ use App\Entity\User\UserType; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; use App\Mapping\Transformer\UserDisplayNameMapTransformer; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; use App\State\User\UserSignupProcessor; +use App\State\User\UserStateProcessor; use AutoMapper\Attribute\MapFrom; use Symfony\Component\Validator\Constraints as Assert; @@ -24,7 +24,7 @@ use Symfony\Component\Validator\Constraints as Assert; shortName: 'User', stateOptions: new Options(entityClass: User::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class, + processor: UserStateProcessor::class, )] #[API\GetCollection()] #[API\Post(input: UserSignupDto::class, processor: UserSignupProcessor::class)] diff --git a/src/State/User/UserStateProcessor.php b/src/State/User/UserStateProcessor.php index 968063c..f0619f1 100644 --- a/src/State/User/UserStateProcessor.php +++ b/src/State/User/UserStateProcessor.php @@ -5,15 +5,18 @@ namespace App\State\User; use ApiPlatform\Doctrine\Common\State\PersistProcessor; use ApiPlatform\Metadata as API; use ApiPlatform\State\ProcessorInterface; +use App\Entity\User\Organization; +use App\Entity\User\User; +use App\Entity\User\UserType; +use App\Mapping\AutoMapper; use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserStateProcessor implements ProcessorInterface { public function __construct( + private AutoMapper $autoMapper, #[Autowire(service: PersistProcessor::class)] - private ProcessorInterface $innerProcessor, - private UserPasswordHasherInterface $userPasswordHasher, + private ProcessorInterface $persistProcessor, ) {} /** @@ -23,6 +26,15 @@ class UserStateProcessor implements ProcessorInterface */ public function process(mixed $data, API\Operation $operation, array $uriVariables = [], array $context = []) { - return $data; + /** @var User */ + $user = $this->autoMapper->map($data, User::class); + + if ($user->isType(UserType::Organization) && $user->getOrganization() === null) { + $user->setOrganization(Organization::for($user)); + } + + $user = $this->persistProcessor->process($user, $operation, $uriVariables, $context); + + return $this->autoMapper->map($user, $data); } } From d3f80bc4be0de287d0211b8ed265d1784d43c000 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 25 Feb 2025 15:09:10 +0100 Subject: [PATCH 28/28] Update user id link in org and person subresources --- src/ApiResource/User/OrganizationApiResource.php | 3 ++- src/ApiResource/User/PersonApiResource.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ApiResource/User/OrganizationApiResource.php b/src/ApiResource/User/OrganizationApiResource.php index ac5a11f..2b9e95c 100644 --- a/src/ApiResource/User/OrganizationApiResource.php +++ b/src/ApiResource/User/OrganizationApiResource.php @@ -27,7 +27,8 @@ use Symfony\Component\Validator\Constraints as Assert; uriVariables: [ 'id' => new API\Link( fromClass: UserApiResource::class, - fromProperty: 'organization' + fromProperty: 'organization', + description: 'User identifier' ), ] )] diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php index ffcc344..4ebdddf 100644 --- a/src/ApiResource/User/PersonApiResource.php +++ b/src/ApiResource/User/PersonApiResource.php @@ -26,7 +26,8 @@ use App\State\ApiResourceStateProvider; uriVariables: [ 'id' => new API\Link( fromClass: UserApiResource::class, - fromProperty: 'person' + fromProperty: 'person', + description: 'User identifier' ), ] )]