diff --git a/composer.json b/composer.json index 05c02fc..e4f05f5 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "nelmio/cors-bundle": "^2.3", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", + "stof/doctrine-extensions-bundle": "^1.11", "subiabre/doctrine-snowflakes": "^2.0", "symfony/asset": "6.4.*", "symfony/console": "6.4.*", diff --git a/composer.lock b/composer.lock index ba88be0..9e9a0c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b558cc67d1647e5cfb49487057a3af3d", + "content-hash": "e65a6d86be5982961b544a7bfa7704c9", "packages": [ { "name": "api-platform/core", @@ -172,6 +172,55 @@ }, "time": "2024-02-20T09:52:06+00:00" }, + { + "name": "behat/transliterator", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0", + "phpunit/phpunit": "^8.5.25 || ^9.5.19" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Transliterator\\": "src/Behat/Transliterator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "support": { + "issues": "https://github.com/Behat/Transliterator/issues", + "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" + }, + "time": "2022-03-30T09:27:43+00:00" + }, { "name": "brick/math", "version": "0.11.0", @@ -1607,6 +1656,134 @@ }, "time": "2022-05-23T21:33:49+00:00" }, + { + "name": "gedmo/doctrine-extensions", + "version": "v3.15.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", + "reference": "2a89103f4984d8970f3855284c8c04e6e6a63c0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/2a89103f4984d8970f3855284c8c04e6e6a63c0f", + "reference": "2a89103f4984d8970f3855284c8c04e6e6a63c0f", + "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", + "php": "^7.4 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "psr/clock": "^1", + "symfony/cache": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "doctrine/annotations": "<1.13 || >=3.0", + "doctrine/dbal": "<3.2 || >=4.0", + "doctrine/mongodb-odm": "<2.3 || >=3.0", + "doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13 || ^2.0", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^3.2", + "doctrine/doctrine-bundle": "^2.3", + "doctrine/mongodb-odm": "^2.3", + "doctrine/orm": "^2.14.0", + "friendsofphp/php-cs-fixer": "^3.14.0", + "nesbot/carbon": "^2.71 || ^3.0", + "phpstan/phpstan": "^1.10.2", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.6", + "rector/rector": "^0.19", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", + "doctrine/orm": "to use the extensions with the ORM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.13-dev" + } + }, + "autoload": { + "psr-4": { + "Gedmo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gediminas Morkevicius", + "email": "gediminas.morkevicius@gmail.com" + }, + { + "name": "Gustavo Falco", + "email": "comfortablynumb84@gmail.com" + }, + { + "name": "David Buchmann", + "email": "david@liip.ch" + } + ], + "description": "Doctrine behavioral extensions", + "homepage": "http://gediminasm.org/", + "keywords": [ + "Blameable", + "behaviors", + "doctrine", + "extensions", + "gedmo", + "loggable", + "nestedset", + "odm", + "orm", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree", + "uploadable" + ], + "support": { + "email": "gediminas.morkevicius@gmail.com", + "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.15.0", + "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" + }, + "funding": [ + { + "url": "https://github.com/l3pp4rd", + "type": "github" + }, + { + "url": "https://github.com/mbabker", + "type": "github" + }, + { + "url": "https://github.com/phansys", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2024-02-12T15:17:22+00:00" + }, { "name": "godruoyi/php-snowflake", "version": "3.0.0", @@ -2354,6 +2531,86 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "stof/doctrine-extensions-bundle", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git", + "reference": "9f7023e4c8a1c00a5627d41c1027a3f89e477630" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/9f7023e4c8a1c00a5627d41c1027a3f89e477630", + "reference": "9f7023e4c8a1c00a5627d41c1027a3f89e477630", + "shasum": "" + }, + "require": { + "gedmo/doctrine-extensions": "^3.15.0", + "php": "^7.4 || ^8.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", + "symfony/mime": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^v6.4.1 || ^7.0.1", + "symfony/security-core": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "to use the ORM extensions", + "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions", + "symfony/mime": "To use the Mime component integration for Uploadable" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stof\\DoctrineExtensionsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integration of the gedmo/doctrine-extensions with Symfony", + "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle", + "keywords": [ + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree" + ], + "support": { + "issues": "https://github.com/stof/StofDoctrineExtensionsBundle/issues", + "source": "https://github.com/stof/StofDoctrineExtensionsBundle/tree/v1.11.0" + }, + "time": "2024-02-13T14:43:20+00:00" + }, { "name": "subiabre/doctrine-snowflakes", "version": "2.0", diff --git a/config/bundles.php b/config/bundles.php index 16b2903..af8eb8c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,4 +11,5 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 559641f..a10c213 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -21,6 +21,12 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + + gedmo_loggable: + type: attribute + prefix: Gedmo\Loggable\Entity + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity" + is_bundle: false when@test: doctrine: diff --git a/config/packages/stof_doctrine_extensions.yaml b/config/packages/stof_doctrine_extensions.yaml new file mode 100644 index 0000000..e069aaa --- /dev/null +++ b/config/packages/stof_doctrine_extensions.yaml @@ -0,0 +1,8 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html +# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc +stof_doctrine_extensions: + default_locale: en_US + + orm: + default: + loggable: true diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 7fe4809..ee2ed10 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -32,6 +32,12 @@ RUN apt install -y \ && docker-php-ext-install \ zip +# Intl +RUN apt install -y \ + libicu-dev \ +&& docker-php-ext-install \ + intl + # Composer COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php new file mode 100644 index 0000000..1ee3550 --- /dev/null +++ b/src/ApiResource/Version.php @@ -0,0 +1,101 @@ +log->getId(); + } + + /** + * The ID of the version for this specific resource. + */ + public function getVersion(): ?int + { + return $this->log->getVersion(); + } + + /** + * The type of action that performed the recorded changes. + */ + public function getAction(): ?string + { + return $this->log->getAction(); + } + + /** + * The type of the recorded resource. + */ + public function getResource(): string + { + $classPieces = explode('\\', $this->log->getObjectClass()); + + return end($classPieces); + } + + /** + * The ID of the recorded resource. + */ + public function getResouceId(): int + { + return $this->log->getObjectId(); + } + + /** + * The full resource data, reconstructed from the current resource data merged with versioned data. + */ + public function getResourceData() + { + $entity = $this->entity; + + foreach ($this->log->getData() as $changedProperty => $changedValue) { + $setter = sprintf("set%s", ucfirst($changedProperty)); + $entity->$setter($changedValue); + } + + return $entity; + } + + /** + * The changed resource data, i.e the new values of the changed properties. + */ + public function getResourceChanges() + { + return $this->log->getData(); + } + + /** + * The date at which this version was created. + */ + public function getDateCreated(): ?\DateTimeInterface + { + return $this->log->getLoggedAt(); + } +} diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php new file mode 100644 index 0000000..7ae246a --- /dev/null +++ b/src/Entity/GatewayCharge.php @@ -0,0 +1,97 @@ +id; + } + + public function getMoney(): ?Money + { + return $this->money; + } + + public function setMoney(Money $money): static + { + $this->money = $money; + + return $this; + } + + public function getTarget(): ?Accounting + { + return $this->target; + } + + public function setTarget(?Accounting $target): static + { + $this->target = $target; + + return $this; + } + + public function getExtradata(): ?array + { + return $this->extradata; + } + + public function setExtradata(?array $extradata): static + { + $this->extradata = $extradata; + + return $this; + } + + public function getCheckout(): ?GatewayCheckout + { + return $this->checkout; + } + + public function setCheckout(?GatewayCheckout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } +} diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php new file mode 100644 index 0000000..4b7a073 --- /dev/null +++ b/src/Entity/GatewayCheckout.php @@ -0,0 +1,121 @@ +charges = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getGateway(): ?string + { + return $this->gateway; + } + + public function setGateway(string $gateway): static + { + $this->gateway = $gateway; + + return $this; + } + + public function getOrigin(): ?Accounting + { + return $this->origin; + } + + public function setOrigin(?Accounting $origin): static + { + $this->origin = $origin; + + return $this; + } + + /** + * @return Collection + */ + public function getCharges(): Collection + { + return $this->charges; + } + + public function addCharge(GatewayCharge $charge): static + { + if (!$this->charges->contains($charge)) { + $this->charges->add($charge); + $charge->setCheckout($this); + } + + return $this; + } + + public function removeCharge(GatewayCharge $charge): static + { + if ($this->charges->removeElement($charge)) { + // set the owning side to null (unless already changed) + if ($charge->getCheckout() === $this) { + $charge->setCheckout(null); + } + } + + return $this; + } +} diff --git a/src/Entity/Interface/UserOwnedInterface.php b/src/Entity/Interface/UserOwnedInterface.php new file mode 100644 index 0000000..076b4b3 --- /dev/null +++ b/src/Entity/Interface/UserOwnedInterface.php @@ -0,0 +1,12 @@ + 'partial'])] +#[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project { #[ORM\Id] diff --git a/src/Entity/Transaction.php b/src/Entity/Transaction.php index 605c850..810b97a 100644 --- a/src/Entity/Transaction.php +++ b/src/Entity/Transaction.php @@ -4,8 +4,6 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Repository\TransactionRepository; -use App\State\TransactionStateProcessor; -use App\Validator\GatewayName; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -17,7 +15,6 @@ use Symfony\Component\Validator\Constraints as Assert; */ #[ORM\Entity(repositoryClass: TransactionRepository::class)] #[API\GetCollection()] -#[API\Post(processor: TransactionStateProcessor::class)] #[API\Get()] class Transaction { @@ -27,7 +24,7 @@ class Transaction private ?int $id = null; /** - * The monetary value to be received at target and issued at origin. + * The monetary value received at target and issued at origin. */ #[Assert\Valid()] #[Assert\NotBlank()] @@ -51,12 +48,18 @@ class Transaction private ?Accounting $target = null; /** - * The Gateway processing this Transaction. + * The name of the Gateway implementation that secured the money in this Transaction. */ - #[Assert\Valid()] #[Assert\NotBlank()] - #[ORM\Embedded(class: TransactionGateway::class)] - private ?TransactionGateway $gateway = null; + #[ORM\Column(length: 255)] + private ?string $gateway = null; + + /** + * An external identifier provided by the Gateway for this Transaction. + */ + #[Assert\NotBlank()] + #[ORM\Column(length: 255)] + private ?string $gatewayReference = null; public function getId(): ?int { @@ -99,15 +102,27 @@ class Transaction return $this; } - public function getGateway(): ?TransactionGateway + public function getGateway(): ?string { return $this->gateway; } - public function setGateway(TransactionGateway $gateway): static + public function setGateway(string $gateway): static { $this->gateway = $gateway; return $this; } + + public function getGatewayReference(): ?string + { + return $this->gatewayReference; + } + + public function setGatewayReference(string $gatewayReference): static + { + $this->gatewayReference = $gatewayReference; + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index 459dc89..b89128d 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,11 +2,14 @@ namespace App\Entity; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; +use App\Entity\Interface\UserOwnedInterface; use App\Repository\UserRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -19,16 +22,21 @@ use Symfony\Component\Validator\Constraints as Assert; * Users are the usual issuers of funding, however an User's Accounting can still be a Transaction recipient. * This allows to keep an User's "wallet", witholding their non-raised fundings into their Accounting. */ +#[Gedmo\Loggable()] #[API\GetCollection()] -#[API\Post()] +#[API\Post(validationContext: ['groups' => ['default', 'postValidation']])] #[API\Get()] -#[API\Put(security: 'is_granted("AUTH_USER_EDIT")')] -#[API\Delete(security: 'is_granted("AUTH_USER_EDIT")')] -#[API\Patch(security: 'is_granted("AUTH_USER_EDIT")')] +#[API\Put(security: 'is_granted("USER_EDIT", object)')] +#[API\Delete(security: 'is_granted("USER_EDIT", object)')] +#[API\Patch(security: 'is_granted("USER_EDIT", object)')] +#[API\ApiFilter(filterClass: SearchFilter::class, properties: [ + 'username' => 'partial', + 'name' => 'partial' +])] #[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] -class User implements UserInterface, PasswordAuthenticatedUserInterface +class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] @@ -38,6 +46,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * Human readable, non white space, unique string. */ + #[Gedmo\Versioned] #[Assert\NotBlank()] #[Assert\Length(min: 4, max: 30)] #[Assert\Regex('/^[a-z0-9_]+$/')] @@ -61,12 +70,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * Plain-text, will be hashed by the platform. */ - #[Assert\NotBlank()] + #[Assert\NotBlank(['groups' => ['postValidation']])] #[Assert\Length(min: 12)] - #[API\ApiProperty(writable: true, readable: false)] + #[API\ApiProperty(writable: true, readable: false, required: true)] #[SerializedName('password')] private ?string $plainPassword = null; + #[Gedmo\Versioned] #[Assert\NotBlank()] #[Assert\Email()] #[ORM\Column(length: 255)] @@ -80,8 +90,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * The UserTokens owned by this user. Owner only property. */ - #[API\ApiProperty(writable: false, readableLink: true, security: 'is_granted("AUTH_OWNER", object)')] - #[ORM\OneToMany(mappedBy: 'ownedBy', targetEntity: UserToken::class, orphanRemoval: true)] + #[API\ApiProperty(writable: false, security: 'is_granted("USER_OWNED", object)')] + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: UserToken::class, orphanRemoval: true)] private Collection $tokens; #[ORM\Column] @@ -152,6 +162,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return (string) $this->username; } + public function getOwner(): ?User + { + return $this; + } + public function isOwnedBy(User $user): bool { return $this->getUserIdentifier() === $user->getUserIdentifier(); @@ -201,7 +216,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - public function getPlainPassword(): string + public function getPlainPassword(): ?string { return $this->plainPassword; } @@ -258,7 +273,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { if (!$this->tokens->contains($token)) { $this->tokens->add($token); - $token->setOwnedBy($this); + $token->setOwner($this); } return $this; @@ -268,8 +283,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { if ($this->tokens->removeElement($token)) { // set the owning side to null (unless already changed) - if ($token->getOwnedBy() === $this) { - $token->setOwnedBy(null); + if ($token->getOwner() === $this) { + $token->setOwner(null); } } diff --git a/src/Entity/UserToken.php b/src/Entity/UserToken.php index 1f4a4a0..03dfd21 100644 --- a/src/Entity/UserToken.php +++ b/src/Entity/UserToken.php @@ -4,6 +4,7 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Dto\UserTokenLoginDto; +use App\Entity\Interface\UserOwnedInterface; use App\Repository\UserTokenRepository; use App\State\UserTokenLoginProcessor; use Doctrine\ORM\Mapping as ORM; @@ -18,9 +19,10 @@ use Doctrine\ORM\Mapping as ORM; * `pat_` means the token was created via a login flow. */ #[API\Post(input: UserTokenLoginDto::class, processor: UserTokenLoginProcessor::class)] -#[API\Delete(security: 'is_granted("AUTH_OWNER")')] +#[API\Get(security: 'is_granted("USER_OWNED", object)')] +#[API\Delete(security: 'is_granted("USER_OWNED", object)')] #[ORM\Entity(repositoryClass: UserTokenRepository::class)] -class UserToken +class UserToken implements UserOwnedInterface { #[ORM\Id] #[ORM\GeneratedValue] @@ -38,7 +40,7 @@ class UserToken */ #[ORM\ManyToOne(inversedBy: 'tokens')] #[ORM\JoinColumn(nullable: false)] - private ?User $ownedBy = null; + private ?User $owner = null; public function getId(): ?int { @@ -57,20 +59,20 @@ class UserToken return $this; } - public function getOwnedBy(): ?User + public function getOwner(): ?User { - return $this->ownedBy; + return $this->owner; } - public function setOwnedBy(?User $ownedBy): static + public function setOwner(?User $owner): static { - $this->ownedBy = $ownedBy; + $this->owner = $owner; return $this; } public function isOwnedBy(User $user): bool { - return $this->ownedBy->getUserIdentifier() === $user->getUserIdentifier(); + return $this->owner->getUserIdentifier() === $user->getUserIdentifier(); } } diff --git a/src/Filter/VersionResourceFilter.php b/src/Filter/VersionResourceFilter.php new file mode 100644 index 0000000..fe7b33d --- /dev/null +++ b/src/Filter/VersionResourceFilter.php @@ -0,0 +1,58 @@ +properties as $property => $strategy) { + $description["$property"] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => true, + 'description' => 'The name of the resource.', + 'schema' => [ + 'type' => Type::BUILTIN_TYPE_STRING, + 'enum' => ['User'] + ], + 'openapi' => [ + 'allowEmptyValue' => false, + ], + ]; + } + + return $description; + } +} diff --git a/src/Filter/VersionResourceIdFilter.php b/src/Filter/VersionResourceIdFilter.php new file mode 100644 index 0000000..0b68986 --- /dev/null +++ b/src/Filter/VersionResourceIdFilter.php @@ -0,0 +1,42 @@ +properties as $property => $strategy) { + $description["$property"] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_INT, + 'required' => true, + 'description' => 'The ID of the named resource.', + 'openapi' => [ + 'allowEmptyValue' => false, + ], + ]; + } + + return $description; + } +} diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 9517ddc..bca058f 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -30,7 +30,7 @@ class UsersPump implements PumpInterface foreach ($data as $key => $userData) { $username = $this->normalizeUsername($userData['id']); if (!$username) { - continue; + $username = $this->normalizeUsername($userData['email']); } $user = new User; diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index 23ff31e..8e26a34 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -2,7 +2,7 @@ namespace App\Library\Economy\Payment; -use App\Entity\Transaction; +use App\Entity\GatewayCheckout; interface GatewayInterface { @@ -12,11 +12,11 @@ interface GatewayInterface public static function getName(): string; /** - * When processing a Transaction a Gateway must validate it using it's own means. + * When processing a GatewayCheckout a Gateway must validate it using it's own means. * - * Gateways are trusted to have secured the funds in the Transaction. - * @param Transaction - * @return Transaction + * Gateways are trusted to have secured the funds in the GatewayCheckout. + * @param GatewayCheckout $checkout + * @return GatewayCheckout */ - public function process(Transaction $transaction): Transaction; + public function process(GatewayCheckout $checkout): GatewayCheckout; } diff --git a/src/Library/Economy/Payment/GatewayLocator.php b/src/Library/Economy/Payment/GatewayLocator.php index 7713fe4..f7fd094 100644 --- a/src/Library/Economy/Payment/GatewayLocator.php +++ b/src/Library/Economy/Payment/GatewayLocator.php @@ -2,7 +2,7 @@ namespace App\Library\Economy\Payment; -use App\Entity\Transaction; +use App\Entity\GatewayCheckout; class GatewayLocator { @@ -135,12 +135,17 @@ class GatewayLocator } /** - * @param Transaction $transaction + * @param GatewayCheckout $checkout * @return GatewayInterface - * @throws \Exception When the $transaction::gateway::name does not match to that of an implemented Gateway + * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway */ - public function getGatewayOf(Transaction $transaction): GatewayInterface + public function getGatewayOf(GatewayCheckout $checkout): GatewayInterface { - return $this->getGateway($transaction->getGateway()->getName()); + $gateway = $checkout->getGateway(); + if (!$gateway) { + throw new \Exception("The given GatewayCheckout does not specify a Gateway"); + } + + return $this->getGateway($gateway); } } diff --git a/src/Repository/GatewayChargeRepository.php b/src/Repository/GatewayChargeRepository.php new file mode 100644 index 0000000..9748d28 --- /dev/null +++ b/src/Repository/GatewayChargeRepository.php @@ -0,0 +1,48 @@ + + * + * @method GatewayCharge|null find($id, $lockMode = null, $lockVersion = null) + * @method GatewayCharge|null findOneBy(array $criteria, array $orderBy = null) + * @method GatewayCharge[] findAll() + * @method GatewayCharge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GatewayChargeRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GatewayCharge::class); + } + + // /** + // * @return GatewayCharge[] Returns an array of GatewayCharge objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('g.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?GatewayCharge + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/GatewayCheckoutRepository.php b/src/Repository/GatewayCheckoutRepository.php new file mode 100644 index 0000000..5f17bfd --- /dev/null +++ b/src/Repository/GatewayCheckoutRepository.php @@ -0,0 +1,48 @@ + + * + * @method GatewayCheckout|null find($id, $lockMode = null, $lockVersion = null) + * @method GatewayCheckout|null findOneBy(array $criteria, array $orderBy = null) + * @method GatewayCheckout[] findAll() + * @method GatewayCheckout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GatewayCheckoutRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GatewayCheckout::class); + } + +// /** +// * @return GatewayCheckout[] Returns an array of GatewayCheckout objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('g') +// ->andWhere('g.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('g.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?GatewayCheckout +// { +// return $this->createQueryBuilder('g') +// ->andWhere('g.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Security/AccessTokenHandler.php b/src/Security/AccessTokenHandler.php index c2467de..bc9488c 100644 --- a/src/Security/AccessTokenHandler.php +++ b/src/Security/AccessTokenHandler.php @@ -21,6 +21,6 @@ class AccessTokenHandler implements AccessTokenHandlerInterface throw new BadCredentialsException(); } - return new UserBadge($token->getOwnedBy()->getUserIdentifier()); + return new UserBadge($token->getOwner()->getUserIdentifier()); } } diff --git a/src/Security/Voter/AuthOwnerVoter.php b/src/Security/Voter/UserOwnedVoter.php similarity index 65% rename from src/Security/Voter/AuthOwnerVoter.php rename to src/Security/Voter/UserOwnedVoter.php index 83964cf..f8f6225 100644 --- a/src/Security/Voter/AuthOwnerVoter.php +++ b/src/Security/Voter/UserOwnedVoter.php @@ -2,21 +2,28 @@ namespace App\Security\Voter; +use App\Entity\Interface\UserOwnedInterface; +use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\User\UserInterface; -class AuthOwnerVoter extends Voter +class UserOwnedVoter extends Voter { - public const OWNER = 'AUTH_OWNER'; + public const OWNED = 'USER_OWNED'; protected function supports(string $attribute, mixed $subject): bool { - return in_array($attribute, [self::OWNER]) - && $subject !== null - && method_exists($subject, 'isOwnedBy'); + if (!in_array($attribute, [self::OWNED])) { + return false; + } + + return $subject instanceof UserOwnedInterface; } + /** + * @param User|UserOwnedInterface $subject + */ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); @@ -27,7 +34,5 @@ class AuthOwnerVoter extends Voter } return $subject->isOwnedBy($user); - - return false; } } diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php new file mode 100644 index 0000000..5757ced --- /dev/null +++ b/src/Security/Voter/UserVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof UserInterface) { + return false; + } + + switch ($attribute) { + case self::EDIT: + return $user->getUserIdentifier() === $subject->getUserIdentifier() + || \in_array('ROLE_ADMIN', $user->getRoles()); + case self::VIEW: + return true; + } + + return false; + } +} diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index fd7da70..2971905 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -28,7 +28,7 @@ class AuthService { $token = new UserToken; - $token->setOwnedBy($user); + $token->setOwner($user); $token->setToken(sprintf('%s%s', $type->value, hash( self::TOKEN_HASH_ALGO, join('', [ diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php new file mode 100644 index 0000000..006c97a --- /dev/null +++ b/src/State/GatewayCheckoutProcessor.php @@ -0,0 +1,33 @@ +gatewayLocator->getGatewayOf($data); + + $this->entityManager->persist($data); + $this->entityManager->flush(); + + $gateway->process($data); + + return $data; + } +} diff --git a/src/State/VersionStateProvider.php b/src/State/VersionStateProvider.php new file mode 100644 index 0000000..859e58c --- /dev/null +++ b/src/State/VersionStateProvider.php @@ -0,0 +1,73 @@ +versionRepository = $this->entityManager->getRepository(LogEntry::class); + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + switch ($operation::class) { + case API\Get::class: + return $this->getVersion($uriVariables['id']); + case API\GetCollection::class: + return $this->getVersions( + $context['request']->query->get('resource'), + $context['request']->query->get('resourceId') + ); + } + } + + /** + * @throws NotFoundHttpException + */ + private function getVersion(int $id): Version + { + $log = $this->versionRepository->find($id); + + if (!$log) { + throw new NotFoundHttpException("Not Found"); + } + + $entity = $this->entityManager->find($log->getObjectClass(), $log->getObjectId()); + + return new Version($log, $entity); + } + + /** + * @return Version[] + */ + private function getVersions(string $resource, int $resourceId): array + { + $entity = $this->entityManager->find($this->resourceToEntity($resource), $resourceId); + $logs = $this->versionRepository->getLogEntries($entity); + + $versions = []; + foreach ($logs as $log) { + $versions[] = new Version($log, $entity); + } + + return $versions; + } + + private function resourceToEntity(string $resource): string + { + return sprintf("App\\Entity\\%s", ucfirst(strtolower($resource))); + } +} diff --git a/symfony.lock b/symfony.lock index be69af2..c52da5e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -66,6 +66,18 @@ "tests/bootstrap.php" ] }, + "stof/doctrine-extensions-bundle": { + "version": "1.11", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.2", + "ref": "e805aba9eff5372e2d149a9ff56566769e22819d" + }, + "files": [ + "config/packages/stof_doctrine_extensions.yaml" + ] + }, "symfony/console": { "version": "6.3", "recipe": {