feat(invites): auto-send invite email via Resend + register migration in journal
- Add sendFamilyInviteEmail() to email.ts using existing Resend setup
- Wire into POST /api/invites — fetches inviter name + family name, sends
warm invite email with Accept Invitation button linking to /invite/{token}
- Email is non-fatal: invite is created even if email send fails
- Register 0006_family_invites_missing_cols in _journal.json so Dokploy
auto-applies the migration on next deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b67631fda
commit
b01e0596c1
3 changed files with 77 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,51 @@ const RESEND_API_KEY = process.env.RESEND_API_KEY;
|
|||
const EMAIL_FROM = process.env.EMAIL_FROM || "Tia <noreply@tia.manohargupta.com>";
|
||||
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: `
|
||||
<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}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue