110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import nodemailer from 'nodemailer';
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
sendmail: true,
|
|
newline: 'unix',
|
|
path: '/usr/sbin/sendmail',
|
|
});
|
|
|
|
export async function POST(req: Request) {
|
|
try {
|
|
const {
|
|
name,
|
|
email,
|
|
message,
|
|
company, // honeypot
|
|
turnstileToken,
|
|
formStartedAt,
|
|
} = await req.json();
|
|
|
|
// Basic validation
|
|
if (!name || !email || !message) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required fields.' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Honeypot: bots often fill this
|
|
if (company && String(company).trim().length > 0) {
|
|
return NextResponse.json({ error: 'Spam detected.' }, { status: 400 });
|
|
}
|
|
|
|
// Time-based spam check
|
|
const started = Number(formStartedAt);
|
|
if (!started || Date.now() - started < 3000) {
|
|
return NextResponse.json(
|
|
{ error: 'Submission too fast.' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Turnstile validation
|
|
if (!turnstileToken) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing captcha token.' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const secret = process.env.TURNSTILE_SECRET_KEY;
|
|
if (!secret) {
|
|
return NextResponse.json(
|
|
{ error: 'Turnstile secret not configured.' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
const ip = req.headers.get('cf-connecting-ip') || '';
|
|
|
|
const verifyRes = await fetch(
|
|
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
secret,
|
|
response: turnstileToken,
|
|
remoteip: ip,
|
|
}),
|
|
},
|
|
);
|
|
|
|
const verifyData = (await verifyRes.json()) as {
|
|
success: boolean;
|
|
'error-codes'?: string[];
|
|
};
|
|
|
|
if (!verifyData.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Captcha verification failed.',
|
|
details: verifyData['error-codes'] || [],
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Send email via local sendmail (Postfix/Sendmail/Exim)
|
|
await transporter.sendMail({
|
|
from: 'William March <contact@williammarch.xyz>',
|
|
to: 'your-real-inbox@example.com', // TODO: change to your inbox
|
|
replyTo: email,
|
|
subject: `New message from ${name} via williammarch.xyz`,
|
|
text: `From: ${name} <${email}>\n\n${message}`,
|
|
html: `
|
|
<p><strong>From:</strong> ${name} <${email}></p>
|
|
<p><strong>Message:</strong></p>
|
|
<p>${message.replace(/\n/g, '<br/>')}</p>
|
|
`,
|
|
});
|
|
|
|
return NextResponse.json({ ok: true });
|
|
} catch (error) {
|
|
const msg =
|
|
error instanceof Error ? error.message : 'Failed to send email.';
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|