From 87229ad04f0bb520aa79fd69d57782cac7ab84eb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 17:35:16 +0200 Subject: [PATCH 01/12] Update example STRIPE_API_KEY value --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index ce2bdc4..07500aa 100644 --- a/.env +++ b/.env @@ -39,7 +39,7 @@ CRYPTO_SECRET="" ###< doctrineencryptbundle/doctrine-encrypt-bundle ### ###> App\Gateway\Stripe\StripeGateway ### -STRIPE_API_KEY="" +STRIPE_API_KEY="!ChangeMe!" STRIPE_WEBHOOK_SECRET="" ###< App\Gateway\Stripe\StripeGateway ### From 22724b68e7a6af702f02554ddbfbeb606f9fce04 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 17:38:34 +0200 Subject: [PATCH 02/12] Add semicolon at end of SQL command example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fd6c7c..5efdf35 100644 --- a/README.md +++ b/README.md @@ -111,5 +111,5 @@ bin/docker mariadb mysql -u root -pgoteo goteo 2. Grant *goteo* all privileges. ```mysql -GRANT ALL PRIVILEGES ON *.* TO 'goteo'@'%' +GRANT ALL PRIVILEGES ON *.* TO 'goteo'@'%'; ``` From 77626a858bdfb089ceba6ff5f4809eb82839783f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 17:47:51 +0200 Subject: [PATCH 03/12] Force checkout charges as embedded objects in api payload --- src/ApiResource/Gateway/CheckoutApiResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiResource/Gateway/CheckoutApiResource.php b/src/ApiResource/Gateway/CheckoutApiResource.php index 45c73b8..bbd0684 100644 --- a/src/ApiResource/Gateway/CheckoutApiResource.php +++ b/src/ApiResource/Gateway/CheckoutApiResource.php @@ -60,7 +60,7 @@ class CheckoutApiResource * * @var ChargeApiResource[] */ - #[API\ApiProperty(readableLink: true)] + #[API\ApiProperty(readableLink: true, writableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] public array $charges = []; From ce52273f51601f1fdd54a17cb25193a20eaa2056 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 17:48:43 +0200 Subject: [PATCH 04/12] Fix Doctrine access to property of embeddable object --- src/Entity/EmbeddableMoney.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Entity/EmbeddableMoney.php b/src/Entity/EmbeddableMoney.php index 353981c..e75073f 100644 --- a/src/Entity/EmbeddableMoney.php +++ b/src/Entity/EmbeddableMoney.php @@ -20,16 +20,16 @@ class EmbeddableMoney implements MoneyInterface * Expressed as the minor unit, e.g: cents, pennies, etc. */ #[ORM\Column(type: 'integer', nullable: true)] - private ?int $amount; + public ?int $amount; /** * 3-letter ISO 4217 currency code. */ #[ORM\Column(type: 'string', nullable: true)] - private ?string $currency; + public ?string $currency; #[ORM\Column(type: 'json', nullable: true)] - private ?array $conversion; + public ?array $conversion; public function __construct( ?int $amount = null, From 1b46b0538ead3f91d761bcecef2be7bb39f3dc93 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 18:29:30 +0200 Subject: [PATCH 05/12] Extend support of charge targets in Stripe processed checkouts --- src/Gateway/Stripe/StripeGateway.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 96a7198..d133039 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -6,6 +6,7 @@ use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Gateway\Tracking; use App\Entity\Project\Project; +use App\Entity\Tipjar; use App\Entity\User\User; use App\Gateway\AbstractGateway; use App\Gateway\ChargeType; @@ -25,6 +26,8 @@ class StripeGateway extends AbstractGateway { public const TRACKING_TITLE_CHECKOUT = 'Stripe Checkout Session ID'; + public const SUPPORTED_TARGETS = [Project::class, Tipjar::class]; + private StripeClient $stripe; public function __construct( @@ -170,16 +173,22 @@ class StripeGateway extends AbstractGateway private function getStripeProduct(Charge $charge) { + /** @var Project|Tipjar */ $target = $charge->getTarget()->getOwner(); - if (!$target instanceof Project) { + if (!\in_array($target::class, self::SUPPORTED_TARGETS)) { throw new \Exception(\sprintf( - "Charges with Stripe must be to Projects, instance of '%s' supplied", - $target::class + "Charges instance of class '%s' not in supported Stripe targets: %s", + $target::class, + join(',', self::SUPPORTED_TARGETS) )); } - $id = \sprintf('P%d', $target->getId()); + $id = \sprintf( + '%s%d', + (new \ReflectionClass($target::class))->getShortName(), + $target->getId() + ); try { $product = $this->stripe->products->retrieve($id); @@ -188,13 +197,10 @@ class StripeGateway extends AbstractGateway throw $e; } - $name = $target->getTitle(); - $description = $target->getSubtitle(); - $product = $this->stripe->products->create([ 'id' => $id, - 'name' => $name, - 'description' => $description, + 'name' => $charge->getTitle(), + 'description' => $charge->getDescription(), ]); } From d3d67342c991083156fe690fae97d1239c5bcc52 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 18:39:50 +0200 Subject: [PATCH 06/12] Add getAllowedRoles static method to GatewayInterface --- src/Gateway/GatewayInterface.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Gateway/GatewayInterface.php b/src/Gateway/GatewayInterface.php index d73541b..7a22b23 100644 --- a/src/Gateway/GatewayInterface.php +++ b/src/Gateway/GatewayInterface.php @@ -34,6 +34,11 @@ interface GatewayInterface */ public static function getSupportedChargeTypes(): array; + /** + * @return string[] The roles that are allowed to process Checkouts with this Gateway + */ + public static function getAllowedRoles(): array; + /** * Connects with the payment gateway and creates a checkout session so the gateway can process the payment. * From bceed55f85b7ff41bdf80a7c07dbefd6ec321fe5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 18:54:33 +0200 Subject: [PATCH 07/12] Fix return type of GatewayLocator::getAll annotation --- src/Gateway/GatewayLocator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateway/GatewayLocator.php b/src/Gateway/GatewayLocator.php index 688ffd3..6720961 100644 --- a/src/Gateway/GatewayLocator.php +++ b/src/Gateway/GatewayLocator.php @@ -28,7 +28,7 @@ class GatewayLocator } /** - * @return + * @return array */ public function getAll(): array { From 0f99ec8c92a3442dd86457732fd8dbb6ad73e5ac Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 18:56:29 +0200 Subject: [PATCH 08/12] Filter provided Gateways by allowed roles --- src/State/Gateway/GatewayStateProvider.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/State/Gateway/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php index 3397d2a..8ef5f31 100644 --- a/src/State/Gateway/GatewayStateProvider.php +++ b/src/State/Gateway/GatewayStateProvider.php @@ -8,12 +8,14 @@ use ApiPlatform\State\ProviderInterface; use App\ApiResource\Gateway\GatewayApiResource; use App\Gateway\GatewayInterface; use App\Gateway\GatewayLocator; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class GatewayStateProvider implements ProviderInterface { public function __construct( private GatewayLocator $gateways, + private Security $security, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null @@ -23,6 +25,8 @@ class GatewayStateProvider implements ProviderInterface return $this->getGateways(); case API\Get::class: return $this->getGateway($uriVariables['name']); + default: + return $this->getGateways(); } } @@ -30,6 +34,14 @@ class GatewayStateProvider implements ProviderInterface { $gateways = []; foreach ($this->gateways->getAll() as $gateway) { + foreach ($gateway::getAllowedRoles() as $role) { + $isGranted = $this->security->isGranted($role, $this->security->getUser()); + + if (!$isGranted) { + continue 2; + } + } + $gateways[] = $this->toResource($gateway); } @@ -41,6 +53,14 @@ class GatewayStateProvider implements ProviderInterface try { $gateway = $this->gateways->get($name); + foreach ($gateway::getAllowedRoles() as $role) { + $isGranted = $this->security->isGranted($role, $this->security->getUser()); + + if (!$isGranted) { + throw new \Exception('Unauthorized Gateway role'); + } + } + return $this->toResource($gateway); } catch (\Exception $e) { throw new NotFoundHttpException('Not Found'); From 27f10a53446e4562afcbc5daf9cfec72b101208e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 29 Sep 2025 18:57:45 +0200 Subject: [PATCH 09/12] Set allowed roles of Gateways --- src/Gateway/Gateway/CashGateway.php | 5 +++++ src/Gateway/Gateway/CecaGateway.php | 8 ++++++++ src/Gateway/Paypal/PaypalGateway.php | 5 +++++ src/Gateway/Stripe/StripeGateway.php | 5 +++++ src/Gateway/Wallet/WalletGateway.php | 5 +++++ 5 files changed, 28 insertions(+) diff --git a/src/Gateway/Gateway/CashGateway.php b/src/Gateway/Gateway/CashGateway.php index 31ac495..9de3ac2 100644 --- a/src/Gateway/Gateway/CashGateway.php +++ b/src/Gateway/Gateway/CashGateway.php @@ -21,6 +21,11 @@ class CashGateway implements GatewayInterface return []; } + public static function getAllowedRoles(): array + { + return ['ROLE_ADMIN']; + } + public function process(Checkout $checkout): Checkout { return $checkout; diff --git a/src/Gateway/Gateway/CecaGateway.php b/src/Gateway/Gateway/CecaGateway.php index 63d7d00..5d22d8f 100644 --- a/src/Gateway/Gateway/CecaGateway.php +++ b/src/Gateway/Gateway/CecaGateway.php @@ -26,6 +26,14 @@ class CecaGateway implements GatewayInterface ]; } + public static function getAllowedRoles(): array + { + // This Gateway only exists for historical purposes + // DO NOT USE + // DO NOT IMPLEMENT + return ['']; + } + public function process(Checkout $checkout): Checkout { throw new \Exception(self::LEGACY_MESSAGE); diff --git a/src/Gateway/Paypal/PaypalGateway.php b/src/Gateway/Paypal/PaypalGateway.php index 3d44132..522704b 100644 --- a/src/Gateway/Paypal/PaypalGateway.php +++ b/src/Gateway/Paypal/PaypalGateway.php @@ -56,6 +56,11 @@ class PaypalGateway extends AbstractGateway ]; } + public static function getAllowedRoles(): array + { + return ['PUBLIC_ACCESS']; + } + public function process(Checkout $checkout): Checkout { $order = $this->paypal->postOrder([ diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 96a7198..34a4f56 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -47,6 +47,11 @@ class StripeGateway extends AbstractGateway ]; } + public static function getAllowedRoles(): array + { + return ['PUBLIC_ACCESS']; + } + public function process(Checkout $checkout): Checkout { $session = $this->stripe->checkout->sessions->create([ diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index d0bcbde..8df5b29 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -30,6 +30,11 @@ class WalletGateway implements GatewayInterface ]; } + public static function getAllowedRoles(): array + { + return ['ROLE_USER']; + } + public function __construct( private WalletService $wallet, private MoneyService $money, From 599a915df54e682243ba7c8b1892cd563f060fee Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 30 Sep 2025 15:18:21 +0200 Subject: [PATCH 10/12] Expose project matchCallSubmissions property --- src/ApiResource/Project/ProjectApiResource.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ApiResource/Project/ProjectApiResource.php b/src/ApiResource/Project/ProjectApiResource.php index 72650c5..af64ffb 100644 --- a/src/ApiResource/Project/ProjectApiResource.php +++ b/src/ApiResource/Project/ProjectApiResource.php @@ -7,6 +7,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\LocalizedApiResourceTrait; +use App\ApiResource\Matchfunding\MatchCallSubmissionApiResource; use App\ApiResource\User\UserApiResource; use App\Dto\ProjectCreationDto; use App\Dto\ProjectUpdationDto; @@ -172,4 +173,10 @@ class ProjectApiResource */ #[API\ApiProperty(writable: false)] public array $updates; + + /** + * @var array + */ + #[API\ApiProperty(writable: false)] + public array $matchCallSubmissions; } From 4f612bc83c1c242aad3f827c91369a4e3260f1d4 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 30 Sep 2025 15:22:24 +0200 Subject: [PATCH 11/12] Allow to filter MatchCallSubmissions by call, projects and status --- .../Matchfunding/MatchCallSubmissionApiResource.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 10bcfbc..87f4b87 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Matchfunding; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Project\ProjectApiResource; @@ -40,17 +41,20 @@ class MatchCallSubmissionApiResource * The MatchCall to which this MatchCallSubmission belongs to. */ #[API\ApiProperty(writable: false)] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public MatchCallApiResource $call; /** * The Project that applied for the MatchCall. */ #[API\ApiProperty(writable: false)] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public ProjectApiResource $project; /** * The status of the Project's application for the MatchCall.\ * Only MatchCallSubmissions with an status `accepted` will receive matchfunding. */ + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::DEFAULT; } From cf43519c40974f1e11a9f7010cecd6c3e673bbea Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 30 Sep 2025 15:30:53 +0200 Subject: [PATCH 12/12] Allow to filter MatchCall collections --- src/ApiResource/Matchfunding/MatchCallApiResource.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 1048333..e8e2576 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Matchfunding; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; @@ -72,11 +73,13 @@ class MatchCallApiResource * Main display title. */ #[Assert\NotBlank()] + #[API\ApiFilter(SearchFilter::class, strategy: 'partial')] public string $title; /** * Long-form secondary display text. */ + #[API\ApiFilter(SearchFilter::class, strategy: 'partial')] public string $description; /** @@ -84,10 +87,16 @@ class MatchCallApiResource */ #[Assert\NotBlank()] #[Assert\Valid] + #[API\ApiFilter( + SearchFilter::class, + strategy: 'exact', + properties: ['territory.country', 'territory.subLvl1', 'territory.subLvl2'], + )] public Territory $territory; /** * The current status of the MatchCall. */ + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public MatchCallStatus $status = MatchCallStatus::InEditing; }