diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 784b314..b452467 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1748307600000, "tag": "0005_email_verification", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1748394000000, + "tag": "0006_family_invites_missing_cols", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/api/invites/route.ts b/src/app/api/invites/route.ts index d7bcc44..dfbc526 100644 --- a/src/app/api/invites/route.ts +++ b/src/app/api/invites/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { sql } from "@/db"; import { requireFamily } from "@/lib/auth"; import { randomBytes } from "crypto"; +import { sendFamilyInviteEmail } from "@/lib/email"; // GET - list invites for a family export async function GET(request: Request) { @@ -61,9 +62,32 @@ export async function POST(request: Request) { `INSERT INTO family_invites (family_id, email, role, display_name, token, expires_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, role, display_name as "displayName", expires_at as "expiresAt"`, - [auth.session!.familyId, email, role || "caregiver", displayName, token, expiresAt.toISOString()] + [auth.session!.familyId, email, role || "caregiver", displayName || null, token, expiresAt.toISOString()] ); + // Fetch inviter name + family name for the email + try { + const [meta] = await sql.unsafe( + `SELECT u.name as inviter_name, f.name as family_name + FROM family_members fm + JOIN users u ON u.id = fm.user_id + JOIN families f ON f.id = fm.family_id + WHERE fm.family_id = $1 AND fm.user_id = $2 + LIMIT 1`, + [auth.session!.familyId, auth.session!.userId] + ); + await sendFamilyInviteEmail({ + to: email, + inviterName: meta?.inviter_name || "Someone", + familyName: meta?.family_name || "your family", + token, + role: role || "caregiver", + }); + } catch (emailErr) { + console.error("[INVITE-EMAIL-ERROR]", emailErr); + // non-fatal — invite was created, email just didn't send + } + return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}` }); } catch (error) { console.error(error); diff --git a/src/lib/email.ts b/src/lib/email.ts index 2b1c6f0..2547125 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -4,6 +4,51 @@ const RESEND_API_KEY = process.env.RESEND_API_KEY; const EMAIL_FROM = process.env.EMAIL_FROM || "Tia "; const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com"; +/** 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 resend.emails.send({ + from: EMAIL_FROM, + to, + subject: `${inviterName} invited you to join their family on Tia 🍼`, + html: ` +
+

You're invited! 🎉

+

+ ${inviterName} has invited you to join ${familyName} on Tia as ${roleLabel}. +

+

+ Tia helps families track feeds, sleep, growth, memories, and more — all in one place. +

+ + Accept Invitation + +

This link expires in 7 days. If you didn't expect this, you can safely ignore it.

+
`, + }); + 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}`;