148 lines
4.0 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|