Merge pull request #167 from goteo/develop

Merge develop into main
This commit is contained in:
2025-10-06 11:44:24 +02:00
committed by GitHub
15 changed files with 95 additions and 16 deletions
+1 -1
View File
@@ -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 ###
+1 -1
View File
@@ -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'@'%';
```
@@ -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 = [];
@@ -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;
}
@@ -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;
}
@@ -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<int, MatchCallSubmissionApiResource>
*/
#[API\ApiProperty(writable: false)]
public array $matchCallSubmissions;
}
+3 -3
View File
@@ -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,
+5
View File
@@ -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;
+8
View File
@@ -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);
+5
View File
@@ -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.
*
+1 -1
View File
@@ -28,7 +28,7 @@ class GatewayLocator
}
/**
* @return <string, GatewayInterface>
* @return array<string, GatewayInterface>
*/
public function getAll(): array
{
+5
View File
@@ -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([
+20 -9
View File
@@ -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(),
]);
}
+5
View File
@@ -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,
@@ -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');