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