Ask organizer modal implemented
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sendOrganizerInquiryNotification } from "../../../lib/server/mail";
|
||||
import { rateLimitKey } from "../../../lib/server/rateLimit";
|
||||
import {
|
||||
errorJson,
|
||||
rateLimited,
|
||||
serverMisconfigured,
|
||||
} from "../../../lib/server/responses";
|
||||
import { logRouteError } from "../../../lib/server/requestId";
|
||||
import { apiRoute } from "../../../lib/server/apiRoute";
|
||||
import { ORGANIZER_INQUIRY_HONEYPOT_FIELD } from "../../../lib/organizerInquiryConstants";
|
||||
import { organizerInquiryBodySchema } from "../../../lib/server/validation/organizerInquirySchemas";
|
||||
import { readLimitedJson } from "../../../lib/server/validation/requestBody";
|
||||
import { jsonFromZodError } from "../../../lib/server/validation/zodHttp";
|
||||
|
||||
const SCOPE = "organizer-inquiry.submit";
|
||||
const EMAIL_MIN_INTERVAL_MS = 60 * 1000;
|
||||
const IP_MIN_INTERVAL_MS = 20 * 1000;
|
||||
|
||||
function clientIp(request: NextRequest): string {
|
||||
return (
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
||||
request.headers.get("x-real-ip") ??
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function organizerInquiryTo(): string | null {
|
||||
const raw = process.env.ORGANIZER_INQUIRY_TO?.trim();
|
||||
return raw && raw.length > 0 ? raw : null;
|
||||
}
|
||||
|
||||
export const POST = apiRoute(SCOPE, async (request: NextRequest, _ctx, { requestId }) => {
|
||||
const parsedBody = await readLimitedJson(request);
|
||||
if (parsedBody.ok === false) {
|
||||
return parsedBody.response;
|
||||
}
|
||||
|
||||
const validated = organizerInquiryBodySchema.safeParse(parsedBody.value);
|
||||
if (!validated.success) {
|
||||
return jsonFromZodError(validated.error);
|
||||
}
|
||||
|
||||
const { email, message } = validated.data;
|
||||
const honeypot = validated.data[ORGANIZER_INQUIRY_HONEYPOT_FIELD];
|
||||
|
||||
if (honeypot.length > 0) {
|
||||
// Silent success for bots — do not send mail or reveal rejection.
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
const ip = clientIp(request);
|
||||
|
||||
const rlEmail = rateLimitKey(`organizer-inquiry-email:${email}`, EMAIL_MIN_INTERVAL_MS);
|
||||
if (rlEmail.ok === false) {
|
||||
return rateLimited(rlEmail.retryAfterMs);
|
||||
}
|
||||
|
||||
const rlIp = rateLimitKey(`organizer-inquiry-ip:${ip}`, IP_MIN_INTERVAL_MS);
|
||||
if (rlIp.ok === false) {
|
||||
return rateLimited(rlIp.retryAfterMs);
|
||||
}
|
||||
|
||||
const to = organizerInquiryTo();
|
||||
if (!to) {
|
||||
return serverMisconfigured("ORGANIZER_INQUIRY_TO is not configured");
|
||||
}
|
||||
|
||||
const from = process.env.SMTP_FROM ?? "noreply@localhost";
|
||||
|
||||
try {
|
||||
await sendOrganizerInquiryNotification({
|
||||
to,
|
||||
fromEmail: from,
|
||||
visitorEmail: email,
|
||||
message,
|
||||
requestId,
|
||||
});
|
||||
} catch (err) {
|
||||
logRouteError(SCOPE, requestId, err, { phase: "sendOrganizerInquiryNotification" });
|
||||
return errorJson(
|
||||
"mail_failed",
|
||||
"We could not send your message. Please try again later.",
|
||||
502,
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
});
|
||||
Reference in New Issue
Block a user