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,
|
"when": 1748307600000,
|
||||||
"tag": "0005_email_verification",
|
"tag": "0005_email_verification",
|
||||||
"breakpoints": true
|
"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 { sql } from "@/db";
|
||||||
import { requireFamily } from "@/lib/auth";
|
import { requireFamily } from "@/lib/auth";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
|
import { sendFamilyInviteEmail } from "@/lib/email";
|
||||||
|
|
||||||
// GET - list invites for a family
|
// GET - list invites for a family
|
||||||
export async function GET(request: Request) {
|
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)
|
`INSERT INTO family_invites (family_id, email, role, display_name, token, expires_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id, email, role, display_name as "displayName", expires_at as "expiresAt"`,
|
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}` });
|
return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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 EMAIL_FROM = process.env.EMAIL_FROM || "Tia <noreply@tia.manohargupta.com>";
|
||||||
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://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. */
|
/** Sends an account-verification email, or logs the link in dev. */
|
||||||
export async function sendVerificationEmail(email: string, token: string, userId: string) {
|
export async function sendVerificationEmail(email: string, token: string, userId: string) {
|
||||||
const link = `${APP_URL}/api/auth/verify-email?token=${token}`;
|
const link = `${APP_URL}/api/auth/verify-email?token=${token}`;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue