diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 121c003..c95bc3e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -5,7 +5,7 @@ import { magicLink } from "better-auth/plugins"; import { appDb, schema } from "./appdb"; import { grantCredits, DENEME_KREDISI } from "./credits"; import { magicLinkEpostasi } from "./eposta"; -import { epostaGonder } from "./eposta-gonder"; +import { epostaGonder, epostaKonsolModu } from "./eposta-gonder"; /** * Verify URL'ini maile konacak onay sayfası URL'ine çevirir. Doğrulama linki @@ -30,13 +30,15 @@ async function sendMagicLinkEmail(email: string, url: string) { (globalThis as { __sonMagicLink?: string }).__sonMagicLink = url; } const mailLinki = onaySayfasiUrl(url); - if (!process.env.RESEND_API_KEY) { - // Dev fallback: Resend anahtarı yoksa linki terminale yaz + if (epostaKonsolModu()) { + // Konsol modu (dev ya da EPOSTA_KONSOL_FALLBACK=1): linki terminale yaz. + // Prod'da anahtar yoksa buraya girilmez; epostaGonder hata fırlatır — + // token'lı link prod loglarına düşmez. console.log(`\n[giris] Magic link for ${email}:\n${mailLinki}\n`); return; } - // Gönderim hatası better-auth'a fırlatılır — aksi halde kullanıcı - // "gönderildi" ekranını görür ama mail hiç çıkmaz. + // Gönderim/yapılandırma hatası better-auth'a fırlatılır — aksi halde + // kullanıcı "gönderildi" ekranını görür ama mail hiç çıkmaz. await epostaGonder(email, magicLinkEpostasi(mailLinki)); } diff --git a/src/lib/eposta-gonder.ts b/src/lib/eposta-gonder.ts index a4e6050..aa38b3a 100644 --- a/src/lib/eposta-gonder.ts +++ b/src/lib/eposta-gonder.ts @@ -4,23 +4,62 @@ import type { EpostaIcerik } from "./eposta"; /** * Ortak e-posta göndericisi (Resend). Şablonlar eposta.ts'te, gönderim * burada — magic link (auth.ts) ve kredi hatırlatması aynı yoldan çıkar. - * RESEND_API_KEY yoksa dev fallback: içerik terminale yazılır. + * + * Env okumalarının tamamı ÇAĞRI ANINDA yapılır, modül seviyesinde değil: + * `next build` NODE_ENV=production + env'siz koşar; modül yüklenirken hata + * fırlatmak build'i kırar. */ + +/** Maile yanıt verilirse düşeceği adres — mail.kolaytercih.com'un MX'i yok. */ +const VARSAYILAN_YANIT_ADRESI = "destek@kolaytercih.com"; + +/** Prod'da konsol fallback'ini bilinçli açan kaçış bayrağı (lokal docker testi). */ +function kacisBayragi(): boolean { + return process.env.EPOSTA_KONSOL_FALLBACK === "1"; +} + +/** + * Konsol modu: gönderim yerine içerik terminale yazılır. Yalnızca Resend + * anahtarı yokken ve (dev'de ya da EPOSTA_KONSOL_FALLBACK=1 ile) geçerlidir. + * Production'da anahtar yoksa konsol moduna DÜŞÜLMEZ — giriş token'ı taşıyan + * link prod loglarına yazılmasın, kullanıcı da "gönderildi" sanmasın diye + * epostaGonder net hata fırlatır. + */ +export function epostaKonsolModu(): boolean { + return ( + !process.env.RESEND_API_KEY && + (process.env.NODE_ENV !== "production" || kacisBayragi()) + ); +} + export async function epostaGonder( to: string, icerik: EpostaIcerik, ): Promise { - if (!process.env.RESEND_API_KEY) { + if (epostaKonsolModu()) { console.log( - `\n[eposta] (dev) ${to} adresine gönderilecekti — ${icerik.subject}\n${icerik.text}\n`, + `\n[eposta] (konsol modu) ${to} adresine gönderilecekti — ${icerik.subject}\n${icerik.text}\n`, ); return; } + const anahtar = process.env.RESEND_API_KEY; + if (!anahtar) { + throw new Error("[eposta] RESEND_API_KEY tanımlı değil"); + } + // EMAIL_FROM yoksa resend.dev adresi yalnızca Resend hesap sahibine teslim + // eder — prod'da sessizce buna düşmek yerine yapılandırma hatasını göster. + const gonderen = process.env.EMAIL_FROM; + if (!gonderen && process.env.NODE_ENV === "production" && !kacisBayragi()) { + throw new Error("[eposta] EMAIL_FROM tanımlı değil"); + } const { Resend } = await import("resend"); - const resend = new Resend(process.env.RESEND_API_KEY); + const resend = new Resend(anahtar); const { error } = await resend.emails.send({ - from: process.env.EMAIL_FROM ?? "KolayTercih ", + from: gonderen || "KolayTercih ", to, + // `||`: CI'da secret tanımsızsa env boş string gelir; boş Reply-To ile + // Resend'e gitmek yerine varsayılana düş. + replyTo: process.env.EMAIL_REPLY_TO || VARSAYILAN_YANIT_ADRESI, subject: icerik.subject, html: icerik.html, // Düz metin alternatifi spam puanını düşürür ve HTML'i engelleyen