99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
"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 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.`,
|
|
};
|
|
}
|
|
|
|
const parsed = contactRequestSchema.safeParse(data);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
error: "Bitte prüfe deine Eingaben und versuche es erneut.",
|
|
};
|
|
}
|
|
|
|
const configError = getAppEmailConfigError();
|
|
if (configError) {
|
|
return {
|
|
success: false,
|
|
error: configError,
|
|
};
|
|
}
|
|
|
|
const adminEmail = process.env.ADMIN_EMAIL || DEFAULT_ADMIN_EMAIL;
|
|
const inquiryLabel = contactRequestTypeLabels[parsed.data.requestType];
|
|
|
|
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: adminResult.error,
|
|
};
|
|
}
|
|
|
|
if (!confirmationResult.success) {
|
|
console.error(
|
|
"Bestätigungs-E-Mail konnte nicht zugestellt werden:",
|
|
confirmationResult.error,
|
|
);
|
|
}
|
|
|
|
return { success: true };
|
|
}
|