- Primary sender: tia@tia.manohargupta.com (verified subdomain in Resend) - sendWithFallback() retries with onboarding@resend.dev if Resend rejects the primary domain (covers the window while SPF is still propagating) - Both sendFamilyInviteEmail() and sendVerificationEmail() use the fallback Update EMAIL_FROM in Dokploy to: Tia <tia@tia.manohargupta.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
4.3 KiB
TypeScript
100 lines
4.3 KiB
TypeScript
import { Resend } from "resend";
|
|
|
|
const RESEND_API_KEY = process.env.RESEND_API_KEY;
|
|
// Primary: verified subdomain (set EMAIL_FROM=Tia <tia@tia.manohargupta.com> in Dokploy)
|
|
// Fallback: Resend shared domain — works without domain verification
|
|
const EMAIL_FROM = process.env.EMAIL_FROM || "Tia <tia@tia.manohargupta.com>";
|
|
const EMAIL_FROM_FALLBACK = "Tia <onboarding@resend.dev>";
|
|
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com";
|
|
|
|
/** Send email with automatic fallback to shared domain if primary domain isn't verified yet */
|
|
async function sendWithFallback(resend: Resend, payload: Parameters<Resend["emails"]["send"]>[0]) {
|
|
const result = await resend.emails.send(payload);
|
|
// Resend returns { error } object rather than throwing for domain issues
|
|
const err = (result as { error?: { message?: string; name?: string } }).error;
|
|
if (err) {
|
|
const msg = (err.message || err.name || "").toLowerCase();
|
|
if (msg.includes("domain") || msg.includes("verif") || msg.includes("sender")) {
|
|
console.warn("[EMAIL-FALLBACK] Primary domain rejected, retrying with shared domain:", err.message);
|
|
return await resend.emails.send({ ...payload, from: EMAIL_FROM_FALLBACK });
|
|
}
|
|
throw new Error(err.message || "Resend error");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Sends a family invite email, or logs the link in dev. */
|
|
export async function sendFamilyInviteEmail(opts: {
|
|
to: string;
|
|
inviterName: string;
|
|
familyName: string;
|
|
token: string;
|
|
role: string;
|
|
}) {
|
|
const { to, inviterName, familyName, token, role } = opts;
|
|
const link = `${APP_URL}/invite/${token}`;
|
|
const roleLabel = role === "admin" ? "an admin" : "a caregiver";
|
|
|
|
if (!RESEND_API_KEY) {
|
|
console.log(`[INVITE-LINK] to=${to} family=${familyName} link=${link}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resend = new Resend(RESEND_API_KEY);
|
|
await sendWithFallback(resend, {
|
|
from: EMAIL_FROM,
|
|
to,
|
|
subject: `${inviterName} invited you to join their family on Tia 🍼`,
|
|
html: `
|
|
<div style="font-family:system-ui,sans-serif;max-width:500px;margin:0 auto;">
|
|
<h2 style="color:#1f2937;">You're invited! 🎉</h2>
|
|
<p style="color:#374151;font-size:16px;">
|
|
<strong>${inviterName}</strong> has invited you to join <strong>${familyName}</strong> on Tia as ${roleLabel}.
|
|
</p>
|
|
<p style="color:#374151;font-size:15px;">
|
|
Tia helps families track feeds, sleep, growth, memories, and more — all in one place.
|
|
</p>
|
|
<a href="${link}"
|
|
style="display:inline-block;background:#fb7185;color:#fff;padding:14px 28px;text-decoration:none;border-radius:8px;margin:20px 0;font-size:16px;font-weight:600;">
|
|
Accept Invitation
|
|
</a>
|
|
<p style="color:#9ca3af;font-size:13px;">This link expires in 7 days. If you didn't expect this, you can safely ignore it.</p>
|
|
</div>`,
|
|
});
|
|
console.log(`[INVITE-EMAIL-SENT] to=${to} family=${familyName}`);
|
|
} catch (e) {
|
|
console.error("[INVITE-EMAIL-FAILED]", e);
|
|
}
|
|
}
|
|
|
|
/** Sends an account-verification email, or logs the link in dev. */
|
|
export async function sendVerificationEmail(email: string, token: string, userId: string) {
|
|
const link = `${APP_URL}/api/auth/verify-email?token=${token}`;
|
|
|
|
if (!RESEND_API_KEY) {
|
|
// Dev fallback: no Resend key -> print the link so test accounts
|
|
// can be verified straight from the server log.
|
|
console.log(`[VERIFY-LINK] user=${userId} email=${email} link=${link}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resend = new Resend(RESEND_API_KEY);
|
|
await sendWithFallback(resend, {
|
|
from: EMAIL_FROM,
|
|
to: email,
|
|
subject: "Verify your Tia email",
|
|
html: `
|
|
<div style="font-family:system-ui,sans-serif;max-width:500px;margin:0 auto;">
|
|
<h2>Welcome to Tia</h2>
|
|
<p>Confirm this email to activate your account. This link expires in 24 hours.</p>
|
|
<a href="${link}" style="display:inline-block;background:#e11d48;color:#fff;padding:12px 24px;text-decoration:none;border-radius:6px;margin:16px 0;">Verify Email</a>
|
|
<p style="color:#6b7280;font-size:14px;">If you didn't create a Tia account, ignore this email.</p>
|
|
</div>`,
|
|
});
|
|
console.log(`[VERIFY-EMAIL-SENT] user=${userId} email=${email}`);
|
|
} catch (e) {
|
|
console.error("[VERIFY-EMAIL-FAILED]", e);
|
|
}
|
|
}
|