Files
kelolabumi-web/app/api/contact/route.ts
2026-04-23 01:43:48 +07:00

148 lines
4.0 KiB
TypeScript

import nodemailer from "nodemailer";
import { NextRequest, NextResponse } from "next/server";
import { verifyCaptchaToken } from "../../../lib/contact-captcha";
const WINDOW_MS = 30 * 60 * 1000;
const MAX_REQUESTS = 5;
const MIN_FILL_MS = 3000;
type RateLimitEntry = {
count: number;
resetAt: number;
};
const rateLimitStore = globalThis as typeof globalThis & {
__contactRateLimit?: Map<string, RateLimitEntry>;
};
const requests = rateLimitStore.__contactRateLimit ?? new Map<string, RateLimitEntry>();
rateLimitStore.__contactRateLimit = requests;
function getClientIp(request: NextRequest) {
const forwardedFor = request.headers.get("x-forwarded-for");
if (forwardedFor) {
return forwardedFor.split(",")[0]?.trim() ?? "unknown";
}
return request.headers.get("x-real-ip") ?? "unknown";
}
function isRateLimited(ip: string) {
const now = Date.now();
const existing = requests.get(ip);
if (!existing || existing.resetAt <= now) {
requests.set(ip, { count: 1, resetAt: now + WINDOW_MS });
return false;
}
if (existing.count >= MAX_REQUESTS) {
return true;
}
existing.count += 1;
requests.set(ip, existing);
return false;
}
export async function POST(request: NextRequest) {
const ip = getClientIp(request);
if (isRateLimited(ip)) {
return NextResponse.json(
{ message: "Terlalu banyak percobaan. Silakan coba lagi beberapa saat." },
{ status: 429 }
);
}
const body = (await request.json()) as {
fullName?: string;
email?: string;
subject?: string;
message?: string;
website?: string;
startedAt?: string;
captchaAnswer?: string;
captchaToken?: string;
};
if (body.website) {
return NextResponse.json({ message: "Permintaan ditolak." }, { status: 400 });
}
const startedAt = Number(body.startedAt ?? 0);
if (!startedAt || Date.now() - startedAt < MIN_FILL_MS) {
return NextResponse.json(
{ message: "Form dikirim terlalu cepat. Silakan isi kembali dengan benar." },
{ status: 400 }
);
}
const fullName = body.fullName?.trim();
const email = body.email?.trim();
const subject = body.subject?.trim();
const message = body.message?.trim();
const captchaAnswer = body.captchaAnswer?.trim();
const captchaToken = body.captchaToken?.trim();
if (!fullName || !email || !subject || !message) {
return NextResponse.json(
{ message: "Semua field wajib diisi sebelum mengirim." },
{ status: 400 }
);
}
if (!captchaAnswer || !captchaToken || !verifyCaptchaToken(captchaToken, captchaAnswer)) {
return NextResponse.json(
{ message: "Captcha tidak valid atau sudah kedaluwarsa." },
{ status: 400 }
);
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT ?? 465),
secure: process.env.SMTP_SECURE !== "false",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
try {
await transporter.sendMail({
from: `"Kelola Bumi Contact Form" <${process.env.SMTP_USER}>`,
to: process.env.CONTACT_TO_EMAIL,
replyTo: email,
subject: `[Kelola Bumi] ${subject}`,
text: [
`Nama: ${fullName}`,
`Email: ${email}`,
`Subjek: ${subject}`,
"",
"Pesan:",
message
].join("\n"),
html: `
<div style="font-family: Arial, sans-serif; color: #111d23; line-height: 1.6;">
<h2>Pesan Baru dari Form Kontak Kelola Bumi</h2>
<p><strong>Nama:</strong> ${fullName}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Subjek:</strong> ${subject}</p>
<p><strong>Pesan:</strong></p>
<p>${message.replace(/\n/g, "<br />")}</p>
</div>
`
});
return NextResponse.json({
message: "Pesan berhasil dikirim. Tim kami akan menghubungi Anda."
});
} catch {
return NextResponse.json(
{ message: "Email gagal dikirim. Periksa konfigurasi mailer." },
{ status: 500 }
);
}
}