Initial Kelola Bumi website
This commit is contained in:
147
app/api/contact/route.ts
Normal file
147
app/api/contact/route.ts
Normal file
@ -0,0 +1,147 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user