From 2af4a9f7813ee0ca8c7b5adce2ac0517e002ced1 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:55:10 +0300 Subject: [PATCH] =?UTF-8?q?feat(giris):=20adres=20ba=C5=9F=C4=B1na=2060=20?= =?UTF-8?q?sn=20so=C4=9Fuma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aynı e-posta adresine 60 sn içinde ikinci giriş linki gönderilmez: istek yine 200 döner (form "gönderildi" gösterir), mail çıkmaz. Üçüncü kişiye mail bombardımanını ve Resend kotasının yakılmasını engeller; bugüne dek tek sınır better-auth'un IP limitiydi (5 istek/60 sn). - Kaynak süreç içi Map (globalThis.__girisEpostaSon), anahtar trim+lowercase adres. verification tablosundan türetilmez: orada identifier token'dır ve satır sendMagicLink'ten önce yazılır. - Pencere kaymaz (düşen istek süreyi uzatmaz); her çağrıda eski kayıtlar budanır. Gönderim hata verirse kayıt silinir, kullanıcı hemen yeniden deneyebilir. - Yalnızca NODE_ENV=production: dev paneli "anında giriş" art arda istek atıyor. Konsol modunda da işler (lokal docker testi). Canlı giriş yolunu değiştirir; merge öncesi lokal docker testi şart. Co-Authored-By: Claude Fable 5.1 --- src/lib/auth.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index c95bc3e..1a1df6b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -22,7 +22,67 @@ function onaySayfasiUrl(verifyUrl: string): string { return hedef.toString(); } +/** Aynı adrese bu süre içinde en çok bir giriş e-postası gider. */ +const GIRIS_SOGUMA_MS = 60_000; + +/** + * Adres → son gönderim zamanı. Süreç içi Map: tek konteyner çalışıyoruz, + * şema/DB yazımı gerektirmez; yeniden başlatmada sıfırlanır (ikinci katman + * better-auth'un IP limiti). `verification` tablosundan türetilmez — orada + * identifier token'dır ve satır sendMagicLink'ten ÖNCE yazılır. globalThis: + * modül birden çok kez yüklense de tek kayıt (appdb/index.ts deseni). + */ +function girisSogumaKaydi(): Map { + const g = globalThis as { __girisEpostaSon?: Map }; + return (g.__girisEpostaSon ??= new Map()); +} + +function sogumaAnahtari(email: string): string { + return email.trim().toLowerCase(); +} + +/** + * Adres soğumada değilse pencereyi başlatır ve `true` döner; soğumadaysa + * `false` (pencere kaymaz — süre ilk gönderimden sayılır). Her çağrıda süresi + * dolmuş kayıtlar budanır, Map büyümez. + */ +export function girisSogumasiAl(email: string, simdi = Date.now()): boolean { + const kayit = girisSogumaKaydi(); + for (const [adres, zaman] of kayit) { + if (simdi - zaman >= GIRIS_SOGUMA_MS) kayit.delete(adres); + } + const anahtar = sogumaAnahtari(email); + if (kayit.has(anahtar)) return false; + kayit.set(anahtar, simdi); + return true; +} + +/** Gönderim başarısızsa pencereyi geri verir — kullanıcı hemen yeniden deneyebilsin. */ +export function girisSogumasiBirak(email: string): void { + girisSogumaKaydi().delete(sogumaAnahtari(email)); +} + async function sendMagicLinkEmail(email: string, url: string) { + // Soğuma yalnızca prod'da: dev paneli "anında giriş" aynı adrese art arda + // istek atıyor. Konsol modunda da işler (lokal docker testi görsün). + const sogumaAcik = process.env.NODE_ENV === "production"; + if (sogumaAcik && !girisSogumasiAl(email)) { + // Sessizce çık: istek 200 döner, form "gönderildi" gösterir, mail çıkmaz — + // üçüncü kişiye mail bombardımanı ve Resend kotası yakılması engellenir. + console.info( + "[giris] Soğuma penceresinde tekrar istek — e-posta gönderilmedi.", + ); + return; + } + try { + await girisLinkiniIlet(email, url); + } catch (err) { + if (sogumaAcik) girisSogumasiBirak(email); + throw err; + } +} + +async function girisLinkiniIlet(email: string, url: string) { if (process.env.NODE_ENV !== "production") { // Dev paneli "anında giriş" için son üretilen linki yakalar (bkz. // src/lib/dev/actions.ts) — panel onay adımını atlayıp doğrudan verify