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 ### 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'@'%'; ``` 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 = []; 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; } 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; } 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; } 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, 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/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. * 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 { 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..36eada8 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( @@ -47,6 +50,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([ @@ -170,16 +178,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 +202,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(), ]); } 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, 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');