php-commerce/Boot.php
Snider c169f4161f security(webhooks): add per-IP rate limiting for webhook endpoints (P2-075)
Add WebhookRateLimiter service with IP-based rate limiting for webhook
endpoints to prevent rate limit exhaustion attacks against legitimate
payment webhooks.

Changes:
- Add WebhookRateLimiter service with per-IP tracking
- Default: 60 req/min for unknown IPs, 300 req/min for trusted gateway IPs
- Support CIDR ranges for IP allowlisting
- Configure via commerce.webhooks.rate_limits and trusted_ips
- Update BTCPayWebhookController and StripeWebhookController
- Return proper 429 responses with Retry-After headers
- Replace global throttle:120,1 middleware with granular controls
- Add comprehensive tests for rate limiting behaviour

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:11:02 +00:00

163 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce;
use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
use Core\Events\ConsoleBooting;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Core\Mod\Commerce\Listeners\ProvisionSocialHostSubscription;
use Core\Mod\Commerce\Listeners\RewardAgentReferralOnSubscription;
use Core\Mod\Commerce\Services\PaymentGateway\BTCPayGateway;
use Core\Mod\Commerce\Services\PaymentGateway\PaymentGatewayContract;
use Core\Mod\Commerce\Services\PaymentGateway\StripeGateway;
/**
* Commerce Module Boot
*
* Orders, subscriptions, and billing engine.
*
* Service layer: Service\Commerce\Boot
*/
class Boot extends ServiceProvider
{
protected string $moduleName = 'commerce';
/**
* Events this module listens to for lazy loading.
*
* @var array<class-string, string>
*/
public static array $listens = [
AdminPanelBooting::class => 'onAdminPanel',
ApiRoutesRegistering::class => 'onApiRoutes',
WebRoutesRegistering::class => 'onWebRoutes',
ConsoleBooting::class => 'onConsole',
];
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__.'/Migrations');
// Laravel event listeners (not lifecycle events)
Event::subscribe(ProvisionSocialHostSubscription::class);
Event::listen(\Core\Mod\Commerce\Events\SubscriptionCreated::class, RewardAgentReferralOnSubscription::class);
Event::listen(\Core\Mod\Commerce\Events\SubscriptionRenewed::class, Listeners\ResetUsageOnRenewal::class);
Event::listen(\Core\Mod\Commerce\Events\OrderPaid::class, Listeners\CreateReferralCommission::class);
}
public function register(): void
{
$this->mergeConfigFrom(
__DIR__.'/config.php',
$this->moduleName
);
// Core Services
$this->app->singleton(\Core\Mod\Commerce\Services\CommerceService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SubscriptionService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\InvoiceService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\PermissionMatrixService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CouponService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\TaxService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CurrencyService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\ContentOverrideService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\DunningService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SkuParserService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SkuBuilderService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CreditNoteService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\PaymentMethodService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\UsageBillingService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\ReferralService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\FraudService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CheckoutRateLimiter::class);
$this->app->singleton(\Core\Mod\Commerce\Services\WebhookRateLimiter::class);
// Payment Gateways
$this->app->singleton('commerce.gateway.btcpay', function ($app) {
return new BTCPayGateway;
});
$this->app->singleton('commerce.gateway.stripe', function ($app) {
return new StripeGateway;
});
$this->app->bind(PaymentGatewayContract::class, function ($app) {
$defaultGateway = config('commerce.gateways.btcpay.enabled')
? 'btcpay'
: 'stripe';
return $app->make("commerce.gateway.{$defaultGateway}");
});
}
// -------------------------------------------------------------------------
// Event-driven handlers
// -------------------------------------------------------------------------
public function onAdminPanel(AdminPanelBooting $event): void
{
$event->views($this->moduleName, __DIR__.'/View/Blade');
if (file_exists(__DIR__.'/Routes/admin.php')) {
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
}
// Admin Livewire components
$event->livewire('commerce.admin.subscription-manager', View\Modal\Admin\SubscriptionManager::class);
$event->livewire('commerce.admin.order-manager', View\Modal\Admin\OrderManager::class);
$event->livewire('commerce.admin.coupon-manager', View\Modal\Admin\CouponManager::class);
$event->livewire('commerce.admin.dashboard', View\Modal\Admin\Dashboard::class);
$event->livewire('commerce.admin.entity-manager', View\Modal\Admin\EntityManager::class);
$event->livewire('commerce.admin.permission-matrix-manager', View\Modal\Admin\PermissionMatrixManager::class);
$event->livewire('commerce.admin.product-manager', View\Modal\Admin\ProductManager::class);
$event->livewire('commerce.admin.credit-note-manager', View\Modal\Admin\CreditNoteManager::class);
$event->livewire('commerce.admin.referral-manager', View\Modal\Admin\ReferralManager::class);
}
public function onApiRoutes(ApiRoutesRegistering $event): void
{
if (file_exists(__DIR__.'/Routes/api.php')) {
$event->routes(fn () => Route::middleware('api')->group(__DIR__.'/Routes/api.php'));
}
}
public function onWebRoutes(WebRoutesRegistering $event): void
{
if (file_exists(__DIR__.'/Routes/web.php')) {
$event->routes(fn () => Route::middleware(['web', 'auth'])->group(__DIR__.'/Routes/web.php'));
}
// Note: Checkout routes are provided by each frontage (lt.hn, Hub, etc.)
// Commerce module provides the backend services only
// Web/User facing Livewire components (for Hub integration)
$event->livewire('commerce.web.subscription', View\Modal\Web\Subscription::class);
$event->livewire('commerce.web.invoices', View\Modal\Web\Invoices::class);
$event->livewire('commerce.web.dashboard', View\Modal\Web\Dashboard::class);
$event->livewire('commerce.web.payment-methods', View\Modal\Web\PaymentMethods::class);
$event->livewire('commerce.web.change-plan', View\Modal\Web\ChangePlan::class);
$event->livewire('commerce.web.checkout-page', View\Modal\Web\CheckoutPage::class);
$event->livewire('commerce.web.checkout-success', View\Modal\Web\CheckoutSuccess::class);
$event->livewire('commerce.web.checkout-cancel', View\Modal\Web\CheckoutCancel::class);
$event->livewire('commerce.web.currency-selector', View\Modal\Web\CurrencySelector::class);
$event->livewire('commerce.web.usage-dashboard', View\Modal\Web\UsageDashboard::class);
$event->livewire('commerce.web.referral-dashboard', View\Modal\Web\ReferralDashboard::class);
}
public function onConsole(ConsoleBooting $event): void
{
$event->command(Console\ProcessDunning::class);
$event->command(Console\SendRenewalReminders::class);
$event->command(Console\PlantSubscriberTrees::class);
$event->command(Console\CleanupExpiredOrders::class);
$event->command(Console\RefreshExchangeRates::class);
$event->command(Console\SyncUsageToStripe::class);
$event->command(Console\MatureReferralCommissions::class);
}
}