Using rate limiting abstraction with October CMS version 2

I’m wanting to add some rate limiting to certain frontend actions on my site.

My site has several forms, and I’d like to limit the number of times these can be submitted within a certain amount of time.

I’ve searched the October docs and didn’t find much info on this. So I looked at the Laravel docs and found this page that details how to use the rate limiting abstraction; but it seems this is only available in Laravel 10. The only thing I can see in the Laravel 6 docs (Which I believe is the Laravel version that OctoberV2 uses) is to use rate limiting middleware.

Ideally id like to use the rate limiting abstraction in frontend components somehow. So was wondering if anyone out there has done something similar, and if so, point me in the right direction so I can set this up myself.

Thanks

Hey @neil-impelling

You do need the \Illuminate\Cache\RateLimiter class. Here is the October CMS implementation we’re using in v4. Hopefully it helps.

You create an instance with a unique key.

$limiter = new \System\Classes\RateLimiter('foobar');
$limiter->increment();

And check if its been too many tries.

if ($limiter->tooManyAttempts()) { /* ... */ }

Here is the class definition:

<?php namespace System\Classes;

use Str;
use App;
use Request;

/**
 * RateLimiter prevents too many attempts at logging in
 */
class RateLimiter
{
    /**
     * @var string throttleKey
     */
    protected $throttleKey;

    /**
     * @var \Illuminate\Cache\RateLimiter limiter instance
     */
    protected $limiter;

    /**
     * @var \Illuminate\Http\Request request instance
     */
    protected $request;

    /**
     * __construct a new login rate limiter instance.
     */
    public function __construct(string $throttleKey)
    {
        $this->throttleKey = $throttleKey;
        $this->limiter = App::make(\Illuminate\Cache\RateLimiter::class);
        $this->request = Request::instance();
    }

    /**
     * attempts gets the number of attempts for the given key.
     */
    public function attempts()
    {
        return $this->limiter->attempts($this->throttleKey($this->request));
    }

    /**
     * tooManyAttempts determines if the user has too many failed login attempts.
     */
    public function tooManyAttempts($maxAttempts = 5)
    {
        return $this->limiter->tooManyAttempts($this->throttleKey($this->request), $maxAttempts);
    }

    /**
     * increment the login attempts for the user.
     */
    public function increment($decaySeconds = 60)
    {
        $this->limiter->hit($this->throttleKey($this->request), $decaySeconds);
    }

    /**
     * availableIn determines the number of seconds until logging in is available again.
     */
    public function availableIn()
    {
        return $this->limiter->availableIn($this->throttleKey($this->request));
    }

    /**
     * clear the login locks for the given user credentials.
     */
    public function clear()
    {
        $this->limiter->clear($this->throttleKey($this->request));
    }

    /**
     * throttleKey gets the throttle key for the given request.
     * @return string
     */
    protected function throttleKey()
    {
        return Str::transliterate(Str::lower($this->throttleKey)).'|'.$this->request->ip();
    }
}

Thanks for the response @daft

I ended up creating a class (I called it Mesh) - with a single static allow function that uses the app() helper to create a new instance off \Illuminate\Cache\RateLimiter

public static function allow($key)
{
    $limiter = app(\Illuminate\Cache\RateLimiter::class);

    if ($limiter->tooManyAttempts($key, 5)) {
        abort(429);
    }

    $limiter->hit($key, 60);

    return true;
}

Now after my forms are submitted, I call the method like so…

public function onSubmitContactForm()
{
    try {
         Mesh::allow('contact-form:'.request()->ip());
        // handle the form submission / send emails etc....
    } catch (Exception $e) {
        // handle exception
    }
}

Can you see anything wrong with doing it this way?

1 Like

Looks great

There may be some cases where you need to hit and check in different parts of the logic, but cross that bridge when you come to it.

1 Like

Nice one. Thanks man.