Hiz siniri: giris, kayit ve arama uclari
Uygulamada hicbir hiz siniri yoktu. /api/auth/login sinirsiz sifre denemesine, /api/auth/register sinirsiz sahte hesap acmaya aciкti (her hesap FREE plan kotasi alir = dogrudan maliyet). - src/lib/rate-limit.ts: IP basina sabit pencere sayaci, surec belleginde (ek altyapi yok). Cok instance'a cikilirsa Redis/Postgres'e tasinmali. - giris 10/15dk — sayac IP+eposta bazli: tek IP'den farkli hesaplara saldiri da yavaslar - kayit 5/saat - ads/search 60/dk — kota kontrolu zaten var, bu limit agir sorgu seliyle veritabanini bogmayi engeller - 429 + Retry-After + Turkce mesaj - IP: x-forwarded-for'un SON elemani (ilk eleman istemci tarafindan uydurulabilir; son elemani bizim proxy ekler) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Hız sınırı — IP başına sabit pencere sayacı.
|
||||
*
|
||||
* Neden: yayına giren her uç saatler içinde otomatik botlarca taranıyor.
|
||||
* Sınırsız giriş ucu şifre kaba kuvvetine, sınırsız kayıt ucu sahte hesap
|
||||
* seline (her hesap ücretsiz plan kotası = maliyet) açık.
|
||||
*
|
||||
* Depo süreç belleğinde: ek altyapı (Redis) gerektirmez, tek konteyner
|
||||
* kurulumunda yeterlidir. Uygulama birden fazla instance'a çıkarılırsa
|
||||
* sayaç paylaşılmaz — o noktada Redis'e ya da Postgres tablosuna taşınmalı.
|
||||
*/
|
||||
|
||||
type Kova = { sayac: number; bitis: number };
|
||||
|
||||
const kovalar = new Map<string, Kova>();
|
||||
|
||||
export const KURALLAR = {
|
||||
giris: { limit: 10, sureMs: 15 * 60 * 1000 },
|
||||
kayit: { limit: 5, sureMs: 60 * 60 * 1000 },
|
||||
arama: { limit: 60, sureMs: 60 * 1000 },
|
||||
} as const;
|
||||
|
||||
export type KuralAdi = keyof typeof KURALLAR;
|
||||
|
||||
/**
|
||||
* Gerçek istemci IP'si. Coolify/Traefik arkasında x-forwarded-for gelir;
|
||||
* ilk eleman istemci tarafından uydurulabildiği için SON eleman (bizim
|
||||
* proxy'mizin eklediği) tercih edilir.
|
||||
*/
|
||||
export function istemciIp(req: Request): string {
|
||||
const gercek = req.headers.get("x-real-ip");
|
||||
if (gercek) return gercek.trim();
|
||||
const iletilen = req.headers.get("x-forwarded-for");
|
||||
if (iletilen) {
|
||||
const parcalar = iletilen.split(",").map((p) => p.trim()).filter(Boolean);
|
||||
if (parcalar.length) return parcalar[parcalar.length - 1]!;
|
||||
}
|
||||
return "bilinmiyor";
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit aşıldıysa hazır 429 yanıtı döndürür; aşılmadıysa null.
|
||||
*
|
||||
* const sinir = hizSiniriAsimi(req, "giris");
|
||||
* if (sinir) return sinir;
|
||||
*
|
||||
* `ekAnahtar` verilirse sayaç IP + o anahtar bazında tutulur (ör. e-posta):
|
||||
* tek IP'den farklı hesaplara saldırıyı da yavaşlatır.
|
||||
*/
|
||||
export function hizSiniriAsimi(
|
||||
req: Request,
|
||||
kuralAdi: KuralAdi,
|
||||
ekAnahtar?: string,
|
||||
): Response | null {
|
||||
const kural = KURALLAR[kuralAdi];
|
||||
const simdi = Date.now();
|
||||
const anahtar = `${kuralAdi}|${istemciIp(req)}|${ekAnahtar ?? ""}`;
|
||||
|
||||
let kova = kovalar.get(anahtar);
|
||||
if (!kova || simdi > kova.bitis) {
|
||||
kova = { sayac: 0, bitis: simdi + kural.sureMs };
|
||||
kovalar.set(anahtar, kova);
|
||||
}
|
||||
kova.sayac += 1;
|
||||
|
||||
if (kova.sayac > kural.limit) {
|
||||
const kalanSn = Math.max(1, Math.ceil((kova.bitis - simdi) / 1000));
|
||||
const sure = kalanSn > 90 ? `${Math.ceil(kalanSn / 60)} dakika` : `${kalanSn} saniye`;
|
||||
return NextResponseBenzeri(
|
||||
{ error: "RATE_LIMITED", message: `Çok fazla deneme yaptınız. ${sure} sonra tekrar deneyin.` },
|
||||
429,
|
||||
{ "Retry-After": String(kalanSn) },
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function NextResponseBenzeri(
|
||||
govde: unknown,
|
||||
durum: number,
|
||||
basliklar: Record<string, string>,
|
||||
) {
|
||||
return new Response(JSON.stringify(govde), {
|
||||
status: durum,
|
||||
headers: { "Content-Type": "application/json", ...basliklar },
|
||||
});
|
||||
}
|
||||
|
||||
// Bellek şişmesini önle: süresi geçmiş kovaları periyodik temizle.
|
||||
if (typeof setInterval !== "undefined") {
|
||||
const zamanlayici = setInterval(() => {
|
||||
const simdi = Date.now();
|
||||
for (const [k, v] of kovalar) if (simdi > v.bitis) kovalar.delete(k);
|
||||
}, 5 * 60 * 1000);
|
||||
zamanlayici.unref?.();
|
||||
}
|
||||
Reference in New Issue
Block a user