update projects and contact form
This commit is contained in:
109
app/api/contact/route.ts
Normal file
109
app/api/contact/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user