Pembatas Percobaan Akses & Tangkal Serangan Spam
Menahan serangan tebak password massal dan tombol yang diklik berkali-kali secara membabi-buta, sehingga website tetap stabil dan aman.
Menahan serangan tebak password massal dan tombol yang diklik berkali-kali secara membabi-buta, sehingga website tetap stabil dan aman.
Tantangan & Kebutuhan Pengguna
Menyimpan penghitung rate limit hanya di dalam memori proses Node.js tidak efektif ketika aplikasi dijalankan pada beberapa container atau instance paralel, karena setiap instance memiliki memori terpisah. Di sisi lain, menambahkan infrastruktur tambahan seperti cluster Redis khusus sering kali membebani biaya operasional proyek skala menengah, dan ketergantungan mutlak pada database untuk rate limit dapat melumpuhkan aplikasi saat koneksi database sedang padat.
Pembatas laju yang tangguh harus konsisten lintas instance melalui penyimpanan terpusat, namun tidak boleh ikut mematikan layanan ketika penyimpanan utama sedang mengalami gangguan sesaat.
Solusi & Cara Kerja
Modul `common/lib/rate-limit.ts` menggunakan tabel PostgreSQL `rate_limit_buckets` sebagai sumber kebenaran utama yang dibaca dan diperbarui melalui `prisma.$transaction`. Setiap aksi sensitif—mulai dari percobaan login, aktivasi MFA, hingga pemanggilan `requirePermission` pada Server Action (120 request/menit untuk `read` dan 30 request/menit untuk mutasi)—divalidasi terhadap jendela waktunya. Apabila kueri ke PostgreSQL gagal karena kendala jaringan atau pool koneksi, blok `catch` secara transparan mengalihkan pengecekan ke `assertMemoryRateLimit` yang dibatasi hingga `MAX_BUCKETS = 10_000` entri.
Detail & Hal yang Diperhatikan
- Pembersihan Tanpa Cron Eksternal: Untuk mencegah tabel `rate_limit_buckets` membengkak tanpa perlu menjalankan worker terpisah, setiap request memiliki peluang 2% (`Math.random() < 0.02`) untuk memicu `pruneExpiredRateLimits()` secara asinkron di latar belakang.
- Perlindungan Kebocoran Memori pada Fallback: Struktur `Map` di memori memiliki batas keras `MAX_BUCKETS = 10_000`. Saat batas tercapai, fungsi `cleanupMemory` menyapu seluruh kunci yang waktu `resetAt`-nya sudah lewat.
- Informasi Waktu Tunggu Presisi: Saat batas terlampaui, `RateLimitError` menghitung sisa detik menuju `resetAt` (`retryAfterSec`) sehingga pengguna mendapat umpan balik yang jelas berapa detik lagi ia dapat mencoba kembali.
Fungsi `requirePermission` secara otomatis menerapkan rate limit berlapis dengan pola kunci `permission:${userUuid}:${feature}:${action}`, sehingga setiap akun memiliki kuota aksi mandiri yang tidak saling mengganggu pengguna lain.