refactor(email): remove frontend invite email; be-crm sends it now

Per AppShell, email delivery lives in the domain API. Delete the frontend
/api/email/invite route + invite-email template, and the team-management email
wiring. be-crm now emails invitees on create/resend. Copy link stays as a
fallback (invite token still returned in the pending-invite list).
This commit is contained in:
tanweer919
2026-07-14 15:38:53 +05:30
parent 1a2043eaae
commit 9eb234935d
4 changed files with 11 additions and 175 deletions
-74
View File
@@ -1,74 +0,0 @@
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 });
}
}