fix(eposta): Reply-To + prod'da sessiz fallback yok

- Resend gönderimine replyTo eklendi (EMAIL_REPLY_TO, yoksa
  destek@kolaytercih.com) — mail.kolaytercih.com'un MX'i olmadığı için
  yanıtlar bounce oluyordu.
- epostaKonsolModu(): konsola yazma yalnızca anahtar yokken VE (dev'de ya
  da EPOSTA_KONSOL_FALLBACK=1 ile) geçerli. Production'da anahtar yoksa
  artık "gönderildi" dönmüyor, token'lı link loga yazılmıyor; gönderim
  anında net hata fırlatılıyor (better-auth 500 → formda hata toast'ı).
- Production'da EMAIL_FROM yoksa sessizce onboarding@resend.dev'e düşmek
  yerine hata. Env okumaları çağrı anında — next build etkilenmez.

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:
bilalgursen
2026-09-21 04:49:08 +03:00
parent aac33e7533
commit 5992cf3637
2 changed files with 51 additions and 10 deletions

View File

@@ -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<void> {
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 <onboarding@resend.dev>",
from: gonderen || "KolayTercih <onboarding@resend.dev>",
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