feat(giris): adres başına 60 sn soğuma
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, number> {
|
||||
const g = globalThis as { __girisEpostaSon?: Map<string, number> };
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user