mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
175 lines
4.9 KiB
TypeScript
175 lines
4.9 KiB
TypeScript
import { Ratelimit } from "@upstash/ratelimit";
|
|
import { Redis } from "@upstash/redis";
|
|
import { NextResponse } from "next/server";
|
|
import { Resend } from "resend";
|
|
import { getClientIP } from "@/lib/ai-chat/rate-limit";
|
|
import { contactSchema, isFreeEmail } from "@/lib/enterprise-contact";
|
|
|
|
let _ratelimit: Ratelimit | null = null;
|
|
function getRatelimit(): Ratelimit {
|
|
if (!_ratelimit) {
|
|
const redis = new Redis({
|
|
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
});
|
|
_ratelimit = new Ratelimit({
|
|
redis,
|
|
limiter: Ratelimit.slidingWindow(5, "1 h"),
|
|
prefix: "enterprise-contact",
|
|
});
|
|
}
|
|
return _ratelimit;
|
|
}
|
|
|
|
function escapeHtml(text: string): string {
|
|
const map: Record<string, string> = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return text.replace(/[&<>"']/g, (m) => map[m] ?? m);
|
|
}
|
|
|
|
function getFirstName(fullName: string): string {
|
|
const firstName = fullName.trim().split(/\s+/)[0];
|
|
return firstName || "there";
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
// honeypot - bots fill hidden fields
|
|
const raw =
|
|
typeof body === "object" && body !== null
|
|
? (body as Record<string, unknown>)
|
|
: {};
|
|
if (typeof raw._hp === "string" && raw._hp) {
|
|
return NextResponse.json({});
|
|
}
|
|
|
|
if (process.env.NODE_ENV === "production") {
|
|
const { success } = await getRatelimit().limit(getClientIP(request));
|
|
if (!success) {
|
|
return NextResponse.json(
|
|
{ message: "Too many requests. Please try again later." },
|
|
{ status: 429 },
|
|
);
|
|
}
|
|
}
|
|
|
|
const parsed = contactSchema.safeParse(body);
|
|
|
|
if (!parsed.success) {
|
|
const firstError = parsed.error.issues[0];
|
|
const field = firstError?.path[0];
|
|
if (field === "email") {
|
|
return NextResponse.json(
|
|
{ message: "Please enter a valid email address" },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
return NextResponse.json(
|
|
{ message: "Missing required fields" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const { fullName, email, company, companySize, description } = parsed.data;
|
|
|
|
if (isFreeEmail(email)) {
|
|
return NextResponse.json(
|
|
{ message: "Please use a company email address" },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
|
|
const toEmail = process.env.SUPPORT_EMAIL;
|
|
const resendApiKey = process.env.RESEND_API_KEY;
|
|
if (!toEmail || !resendApiKey) {
|
|
console.error("Missing SUPPORT_EMAIL or RESEND_API_KEY");
|
|
return NextResponse.json(
|
|
{ message: "Server configuration error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
const resend = new Resend(resendApiKey);
|
|
const firstName = getFirstName(fullName);
|
|
|
|
const [internalResult, acknowledgementResult] = await Promise.all([
|
|
resend.emails.send({
|
|
from: "Enterprise Support <enterprise@better-auth.com>",
|
|
to: toEmail,
|
|
subject: `Enterprise Inquiry from ${fullName}`,
|
|
html: `
|
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: #18181b;">Enterprise Inquiry</h2>
|
|
<div style="background: #f4f4f5; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
<p><strong>Name:</strong> ${escapeHtml(fullName)}</p>
|
|
<p><strong>Email:</strong> ${escapeHtml(email)}</p>
|
|
<p><strong>Company:</strong> ${escapeHtml(company)}</p>
|
|
${companySize ? `<p><strong>Company Size:</strong> ${escapeHtml(companySize)}</p>` : ""}
|
|
${description ? `<p><strong>Message:</strong><br/>${escapeHtml(description).replace(/\n/g, "<br/>")}</p>` : ""}
|
|
</div>
|
|
<p style="color: #71717a; font-size: 12px;">
|
|
Submitted: ${new Date().toLocaleString()}<br/>
|
|
User Agent: ${escapeHtml(request.headers.get("user-agent") || "N/A")}<br/>
|
|
Referer: ${escapeHtml(request.headers.get("referer") || "N/A")}
|
|
</p>
|
|
</div>
|
|
`,
|
|
}),
|
|
resend.emails.send({
|
|
from: "Ravi <ravi@better-auth.com>",
|
|
to: email,
|
|
cc: toEmail,
|
|
subject: "Re: Enterprise Inquiry",
|
|
text: `Hi ${firstName},
|
|
|
|
Thanks for reaching out. We've received your inquiry and someone from our team will follow up shortly.
|
|
|
|
In the meantime, if there are any timelines, technical requirements, or procurement details we should be aware of, feel free to reply here.
|
|
|
|
Best,
|
|
Ravi`,
|
|
}),
|
|
]);
|
|
|
|
if (internalResult.error) {
|
|
console.error(
|
|
"Resend internal email failed (email=%s)",
|
|
email,
|
|
internalResult.error,
|
|
);
|
|
return NextResponse.json(
|
|
{ message: "Something went wrong. Please try again." },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
if (acknowledgementResult.error) {
|
|
console.error(
|
|
"Resend acknowledgement email failed (email=%s)",
|
|
email,
|
|
acknowledgementResult.error,
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({});
|
|
} catch (e) {
|
|
console.error("Enterprise contact form error", e);
|
|
return NextResponse.json(
|
|
{ message: "Something went wrong. Please try again." },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|