import { NextResponse } from "next/server"; import { buildInviteEmail } from "@/lib/invite-email"; // Sends a team-invitation email via the Twilio Emails API. Server-only: the Twilio // Account SID + Auth Token come from Vercel env (TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN) // and never reach the browser. From EMAIL_FROM_ADDRESS (default support@lynkedup.dev). // // POST /api/email/invite { to, token, roleNames?[] } → { ok } // The email body is a fixed invite template (only `to` + `token` + role names are // caller-supplied), so this can't be used to send arbitrary content. export const runtime = "nodejs"; const TWILIO_EMAILS_URL = "https://comms.twilio.com/v1/Emails"; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const SESSION_COOKIE = "__Host-insignia_session"; export async function POST(request: Request) { // Light guard: only a signed-in session (which the invite UI has) may trigger sends, // so this isn't an open email relay. Presence check — the cookie is HttpOnly + __Host-. const cookies = request.headers.get("cookie") ?? ""; if (!cookies.includes(`${SESSION_COOKIE}=`)) { return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 }); } const sid = process.env.TWILIO_ACCOUNT_SID ?? ""; const authToken = process.env.TWILIO_AUTH_TOKEN ?? ""; const from = process.env.EMAIL_FROM_ADDRESS ?? "support@lynkedup.dev"; const fromName = process.env.EMAIL_FROM_NAME ?? "LynkedUp Pro"; if (!sid || !authToken) { return NextResponse.json({ ok: false, error: "email_not_configured" }, { status: 503 }); } let body: { to?: string; token?: string; roleNames?: string[] }; try { body = await request.json(); } catch { return NextResponse.json({ ok: false, error: "invalid_json" }, { status: 400 }); } const to = (body.to ?? "").trim().toLowerCase(); const token = (body.token ?? "").trim(); const roleNames = Array.isArray(body.roleNames) ? body.roleNames.filter((r) => typeof r === "string").slice(0, 10) : []; if (!EMAIL_RE.test(to)) return NextResponse.json({ ok: false, error: "invalid_email" }, { status: 400 }); if (token.length < 16 || token.length > 256) return NextResponse.json({ ok: false, error: "invalid_token" }, { status: 400 }); const origin = (process.env.PORTAL_BASE_URL || new URL(request.url).origin).replace(/\/$/, ""); const inviteUrl = `${origin}/portal/invite?token=${encodeURIComponent(token)}`; const { subject, html, text } = buildInviteEmail({ inviteUrl, roleNames }); const payload = { from: { address: from, name: fromName }, to: [{ address: to }], content: { subject, html, text }, }; try { const res = await fetch(TWILIO_EMAILS_URL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Basic ${Buffer.from(`${sid}:${authToken}`).toString("base64")}`, }, body: JSON.stringify(payload), }); if (!res.ok) { const detail = await res.text().catch(() => ""); return NextResponse.json({ ok: false, error: "send_failed", status: res.status, detail: detail.slice(0, 300) }, { status: 502 }); } return NextResponse.json({ ok: true }); } catch (e) { return NextResponse.json({ ok: false, error: "send_error", detail: (e as Error).message }, { status: 502 }); } }