diff --git a/composer.json b/composer.json index c53d007..0018ef2 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "illuminate/support": "^9.0|^10.0" }, "require-dev": { - "livewire/livewire": "^2.3", + "livewire/livewire": "^3.0", + "livewire/volt": "^1.3", "orchestra/testbench": "^7.0|^8.0", "phpunit/phpunit": "^9.0|^10.0" }, diff --git a/src/Exceptions/TooManyRequestsException.php b/src/Exceptions/TooManyRequestsException.php index 2394fd7..a78a320 100644 --- a/src/Exceptions/TooManyRequestsException.php +++ b/src/Exceptions/TooManyRequestsException.php @@ -12,7 +12,7 @@ public function __construct( public $component, public $method, public $ip, - public $secondsUntilAvailable + public $secondsUntilAvailable, ) { $this->minutesUntilAvailable = ceil($this->secondsUntilAvailable / 60); diff --git a/src/WithRateLimiting.php b/src/WithRateLimiting.php index d743980..400c3ff 100644 --- a/src/WithRateLimiting.php +++ b/src/WithRateLimiting.php @@ -7,45 +7,52 @@ trait WithRateLimiting { - protected function clearRateLimiter($method = null) + protected function clearRateLimiter($method = null, $component = null) { $method ??= debug_backtrace(limit: 2)[1]['function']; - $key = $this->getRateLimitKey($method); + $component ??= static::class; + + $key = $this->getRateLimitKey($method, $component); RateLimiter::clear($key); } - protected function getRateLimitKey($method) + protected function getRateLimitKey($method, $component) { $method ??= debug_backtrace(limit: 2)[1]['function']; - return sha1(static::class.'|'.$method.'|'.request()->ip()); + $component ??= static::class; + + return sha1($component.'|'.$method.'|'.request()->ip()); } - protected function hitRateLimiter($method = null, $decaySeconds = 60) + protected function hitRateLimiter($method = null, $decaySeconds = 60, $component = null) { $method ??= debug_backtrace(limit: 2)[1]['function']; - $key = $this->getRateLimitKey($method); + $component ??= static::class; + + $key = $this->getRateLimitKey($method, $component); RateLimiter::hit($key, $decaySeconds); } - protected function rateLimit($maxAttempts, $decaySeconds = 60, $method = null) + protected function rateLimit($maxAttempts, $decaySeconds = 60, $method = null, $component = null) { $method ??= debug_backtrace(limit: 2)[1]['function']; - $key = $this->getRateLimitKey($method); + $component ??= static::class; + + $key = $this->getRateLimitKey($method, $component); if (RateLimiter::tooManyAttempts($key, $maxAttempts)) { - $component = static::class; $ip = request()->ip(); $secondsUntilAvailable = RateLimiter::availableIn($key); throw new TooManyRequestsException($component, $method, $ip, $secondsUntilAvailable); } - $this->hitRateLimiter($method, $decaySeconds); + $this->hitRateLimiter($method, $decaySeconds, $component); } } diff --git a/tests/RateLimitingTest.php b/tests/RateLimitingTest.php index da88889..cefe524 100644 --- a/tests/RateLimitingTest.php +++ b/tests/RateLimitingTest.php @@ -4,6 +4,7 @@ use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use Livewire\Livewire; +use Livewire\Volt\Volt; class RateLimitingTest extends TestCase { @@ -42,6 +43,51 @@ public function can_hit_and_clear_rate_limiter() ->call('limit') ->assertSet('secondsUntilAvailable', 0); } + + /** @test */ + public function can_rate_limit_volt() + { + $this->mountVolt(); + $component = Volt::test('volt-component'); + + $component + ->call('limit') + ->assertSet('secondsUntilAvailable', 0) + ->call('limit') + ->assertSet('secondsUntilAvailable', 0) + ->call('limit') + ->assertSet('secondsUntilAvailable', 0) + ->call('limit') + ->assertNotSet('secondsUntilAvailable', 0); + + sleep(1); + + $component + ->call('limit') + ->assertSet('secondsUntilAvailable', 0); + } + + /** @test */ + public function can_hit_and_clear_rate_limiter_volt() + { + $this->mountVolt(); + Volt::test('volt-component') + ->call('hit') + ->call('hit') + ->call('hit') + ->call('limit') + ->assertNotSet('secondsUntilAvailable', 0) + ->call('clear') + ->call('limit') + ->assertSet('secondsUntilAvailable', 0); + } + + protected function mountVolt() + { + Volt::mount([ + __DIR__ . '/views', + ]); + } } class Component extends \Livewire\Component @@ -75,4 +121,4 @@ public function render() { return view('component'); } -} \ No newline at end of file +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 387ca38..244f04d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,8 @@ namespace DanHarrin\LivewireRateLimiting\Tests; use Livewire\LivewireServiceProvider; +use Livewire\Volt\Volt; +use Livewire\Volt\VoltServiceProvider; class TestCase extends \Orchestra\Testbench\TestCase { @@ -20,6 +22,7 @@ protected function getPackageProviders($app) { return [ LivewireServiceProvider::class, + VoltServiceProvider::class, ]; } -} \ No newline at end of file +} diff --git a/tests/views/volt-component.blade.php b/tests/views/volt-component.blade.php new file mode 100644 index 0000000..867a06d --- /dev/null +++ b/tests/views/volt-component.blade.php @@ -0,0 +1,36 @@ +clearRateLimiter('limit', component: 'VoltComponent'); + } + + public function hit() + { + $this->hitRateLimiter('limit', 1, component: 'VoltComponent'); + } + + public function limit() + { + try { + $this->rateLimit(3, 1, component: 'VoltComponent'); + } catch (TooManyRequestsException $exception) { + return $this->secondsUntilAvailable = $exception->secondsUntilAvailable; + } + + $this->secondsUntilAvailable = 0; + } + +}; ?> + +
+ // +