diff --git a/routes/actions.php b/routes/actions.php index 9c2af7d..91836e3 100644 --- a/routes/actions.php +++ b/routes/actions.php @@ -4,7 +4,7 @@ use DuncanMcClean\GuestEntries\Http\Middleware\EnsureFormParametersArriveIntact; use Illuminate\Support\Facades\Route; -Route::name('guest-entries.')->middleware([EnsureFormParametersArriveIntact::class])->group(function () { +Route::name('guest-entries.')->middleware([EnsureFormParametersArriveIntact::class, 'throttle:guest-entries'])->group(function () { Route::post('/create', [GuestEntryController::class, 'store'])->name('store'); Route::post('/update', [GuestEntryController::class, 'update'])->name('update'); Route::delete('/delete', [GuestEntryController::class, 'destroy'])->name('destroy'); diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index f25cae2..1ccd087 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,6 +2,9 @@ namespace DuncanMcClean\GuestEntries; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; use Statamic\Providers\AddonServiceProvider; class ServiceProvider extends AddonServiceProvider @@ -23,5 +26,9 @@ public function boot() $this->publishes([ __DIR__.'/../config/guest-entries.php' => config_path('guest-entries.php'), ], 'guest-entries-config'); + + RateLimiter::for('guest-entries', function (Request $request) { + return Limit::perMinute(10)->by($request->ip()); + }); } } diff --git a/tests/RateLimitingTest.php b/tests/RateLimitingTest.php new file mode 100644 index 0000000..62d363f --- /dev/null +++ b/tests/RateLimitingTest.php @@ -0,0 +1,32 @@ +each(fn () => expect($this->post(route('statamic.guest-entries.store'))->getStatusCode())->not->toBe(429)); + $this->post(route('statamic.guest-entries.store'))->assertStatus(429); +}); + +it('rate limits the update endpoint', function () { + collect(range(1, 10))->each(fn () => expect($this->post(route('statamic.guest-entries.update'))->getStatusCode())->not->toBe(429)); + $this->post(route('statamic.guest-entries.update'))->assertStatus(429); +}); + +it('rate limits the delete endpoint', function () { + collect(range(1, 10))->each(fn () => expect($this->delete(route('statamic.guest-entries.destroy'))->getStatusCode())->not->toBe(429)); + $this->delete(route('statamic.guest-entries.destroy'))->assertStatus(429); +}); + +it('allows the rate limiter to be overridden', function () { + RateLimiter::for('guest-entries', fn ($request) => Limit::perMinute(2)->by($request->ip())); + + $this->post(route('statamic.guest-entries.store')); + $this->post(route('statamic.guest-entries.store')); + $this->post(route('statamic.guest-entries.store'))->assertStatus(429); +});