aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/RateLimiter.php
blob: 07b46bac2a7d461046289427723e20152673b718 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php

declare(strict_types=1);
// Rate limiter: prevents brute-force attacks by tracking how many times an IP
// performs a given action (e.g. login, password reset) within a sliding time window.

namespace App;

class RateLimiter
{
    private \PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::getInstance()->getPdo();
    }

    /**
     * Check whether the given IP has exceeded the allowed number of attempts
     * for an action within the time window.
     */
    public function isLimited(string $ip, string $action, int $maxAttempts, int $windowSeconds): bool
    {
        // Clean up old entries first so the table doesn't grow forever
        $this->purge($action, $windowSeconds);

        $stmt = $this->pdo->prepare(
            'SELECT COUNT(*) FROM rate_limits
             WHERE ip = :ip AND action = :action
             AND attempted_at > DATE_SUB(NOW(), INTERVAL :window SECOND)'
        );
        $stmt->execute(['ip' => $ip, 'action' => $action, 'window' => $windowSeconds]);

        return (int) $stmt->fetchColumn() >= $maxAttempts;
    }

    /**
     * Record an attempt for the given IP and action.
     */
    public function record(string $ip, string $action): void
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO rate_limits (ip, action) VALUES (:ip, :action)'
        );
        $stmt->execute(['ip' => $ip, 'action' => $action]);
    }

    /**
     * Remove entries older than the window so the table stays small.
     */
    private function purge(string $action, int $windowSeconds): void
    {
        $stmt = $this->pdo->prepare(
            'DELETE FROM rate_limits
             WHERE action = :action
             AND attempted_at <= DATE_SUB(NOW(), INTERVAL :window SECOND)'
        );
        $stmt->execute(['action' => $action, 'window' => $windowSeconds]);
    }
}