Add contact email confirmation and abuse protection
This commit is contained in:
+59
-11
@@ -1,20 +1,52 @@
|
||||
"use server";
|
||||
|
||||
import { createElement } from "react";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { ContactConfirmationEmail } from "@/emails/ContactConfirmationEmail";
|
||||
import { ContactRequestEmail } from "@/emails/ContactRequestEmail";
|
||||
import {
|
||||
contactRequestSchema,
|
||||
contactRequestTypeLabels,
|
||||
type ContactRequestInput,
|
||||
type ContactSubmissionPayload,
|
||||
} from "@/lib/contact-schema";
|
||||
import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
const DEFAULT_ADMIN_EMAIL = "kontakt@kita-planer.local";
|
||||
const RATE_LIMIT_MAX = 5;
|
||||
const RATE_LIMIT_WINDOW_SECONDS = 60 * 60;
|
||||
|
||||
async function resolveClientIp(): Promise<string> {
|
||||
const h = await headers();
|
||||
const forwarded = h.get("x-forwarded-for");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0]?.trim() || "unknown";
|
||||
}
|
||||
return h.get("x-real-ip") ?? "unknown";
|
||||
}
|
||||
|
||||
export async function submitContactRequest(data: ContactSubmissionPayload) {
|
||||
// Honeypot: silently absorb bot submissions without informing them.
|
||||
if (data.website && data.website.trim().length > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const ip = await resolveClientIp();
|
||||
const limit = rateLimit(
|
||||
`contact:${ip}`,
|
||||
RATE_LIMIT_MAX,
|
||||
RATE_LIMIT_WINDOW_SECONDS,
|
||||
);
|
||||
if (!limit.allowed) {
|
||||
const minutes = Math.max(1, Math.ceil(limit.retryAfterSeconds / 60));
|
||||
return {
|
||||
success: false,
|
||||
error: `Zu viele Anfragen. Bitte versuche es in ${minutes} Minuten erneut.`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function submitContactRequest(data: ContactRequestInput) {
|
||||
const parsed = contactRequestSchema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -32,19 +64,35 @@ export async function submitContactRequest(data: ContactRequestInput) {
|
||||
|
||||
const adminEmail = process.env.ADMIN_EMAIL || DEFAULT_ADMIN_EMAIL;
|
||||
const inquiryLabel = contactRequestTypeLabels[parsed.data.requestType];
|
||||
const result = await sendAppEmail({
|
||||
to: adminEmail,
|
||||
subject: `Kontaktanfrage: ${inquiryLabel} von ${parsed.data.name}`,
|
||||
replyTo: parsed.data.email,
|
||||
react: createElement(ContactRequestEmail, parsed.data),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const [adminResult, confirmationResult] = await Promise.all([
|
||||
sendAppEmail({
|
||||
to: adminEmail,
|
||||
subject: `Kontaktanfrage: ${inquiryLabel} von ${parsed.data.name}`,
|
||||
replyTo: parsed.data.email,
|
||||
react: createElement(ContactRequestEmail, parsed.data),
|
||||
}),
|
||||
sendAppEmail({
|
||||
to: parsed.data.email,
|
||||
subject: "Wir haben deine Anfrage erhalten — Kita-Planer",
|
||||
replyTo: adminEmail,
|
||||
react: createElement(ContactConfirmationEmail, parsed.data),
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!adminResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
error: adminResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
if (!confirmationResult.success) {
|
||||
console.error(
|
||||
"Bestätigungs-E-Mail konnte nicht zugestellt werden:",
|
||||
confirmationResult.error,
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user