fix: standaryzacja i bezpieczeństwo formularzy

- Ujednolicono kontakt/actions.ts (rate limit, escapeHtml, getMailClient, usunięto bezpośrednie Resend)
- Usunięto nieużywany plik lib/recaptcha.ts
- Naprawiono brak obsługi błędów API (Resend error) w zgloszenie-awarii, zamowienie i zapytanie-multisplit
- Dodano logowanie błędów w catch dla tych formularzy
- Posprzątano stare skrypty robocze z roota
This commit is contained in:
MichalC
2026-08-02 21:10:24 +02:00
parent c9e4283789
commit d11808b471
11 changed files with 46 additions and 6650 deletions
+23 -12
View File
@@ -1,10 +1,9 @@
"use server";
import { z } from "zod";
import { Resend } from "resend";
import { verifyRecaptcha } from "@/lib/recaptcha";
const resend = new Resend(process.env.RESEND_API_KEY);
import { escapeHtml, rateLimit, verifyRecaptcha } from "@/lib/security";
import { getMailClient } from "@/lib/mail";
import { headers } from "next/headers";
const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
@@ -18,12 +17,16 @@ const contactFormSchema = z.object({
export async function submitContactForm(prevState: unknown, formData: FormData) {
try {
const ip = (await headers()).get('x-forwarded-for') || 'unknown';
if (!rateLimit(ip)) {
return { success: false, message: 'Wysłano zbyt wiele zapytań. Spróbuj ponownie później.' };
}
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
const token = formData.get('recaptchaToken') as string;
if (!token) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
const { success } = await verifyRecaptcha(token);
const success = await verifyRecaptcha(token);
if (!success) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
@@ -42,8 +45,16 @@ export async function submitContactForm(prevState: unknown, formData: FormData)
const validatedData = contactFormSchema.parse(rawData);
const mailClient = getMailClient();
if (!mailClient) {
return {
success: false,
message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna.",
};
}
// 2. Wysłanie e-maila przez Resend
const { data, error } = await resend.emails.send({
const { data, error } = await mailClient.resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || "[email protected]",
to: process.env.RESEND_TO_EMAIL || "[email protected]", // Docelowy e-mail (odbiorca)
replyTo: validatedData.email,
@@ -59,29 +70,29 @@ export async function submitContactForm(prevState: unknown, formData: FormData)
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; width: 120px;"><strong>Imię:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.name}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.name)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Telefon:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="tel:${validatedData.phone}" style="color: #0F2A47; text-decoration: none;">${validatedData.phone}</a></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="tel:${escapeHtml(validatedData.phone)}" style="color: #0F2A47; text-decoration: none;">${escapeHtml(validatedData.phone)}</a></td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>E-mail:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="mailto:${validatedData.email}" style="color: #0F2A47; text-decoration: none;">${validatedData.email}</a></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="mailto:${escapeHtml(validatedData.email || '')}" style="color: #0F2A47; text-decoration: none;">${escapeHtml(validatedData.email || '')}</a></td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Miasto:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.city}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.city)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Temat:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.subject}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.subject || '')}</td>
</tr>
</table>
<h2 style="font-size: 18px; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Treść wiadomości:</h2>
<div style="background-color: #f8fafc; padding: 15px; border-radius: 6px; font-size: 15px; line-height: 1.6; white-space: pre-wrap;">
${validatedData.message}
${escapeHtml(validatedData.message || '')}
</div>
<p style="margin-top: 24px; font-size: 12px; color: #64748b; text-align: center;">
+9 -2
View File
@@ -85,7 +85,7 @@ export async function submitOrderForm(prevState: unknown, formData: FormData) {
`<tr><td style="padding:8px;border-bottom:1px solid #f1f5f9;">${escapeHtml(p.name)}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:center;">${p.quantity}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:right;">${(p.price * p.quantity).toLocaleString('pl-PL')} zł</td></tr>`
).join('');
await mailClient.resend.emails.send({
const { data: emailData, error } = await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
replyTo: data.email,
@@ -120,9 +120,16 @@ export async function submitOrderForm(prevState: unknown, formData: FormData) {
</div>
`,
});
if (error) {
console.error("Resend Error:", error);
return { success: false, message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego." };
}
console.log("Wysłano e-mail (ID):", emailData?.id);
return { success: true, message: 'Dziękujemy! Skontaktujemy się z Tobą w ciągu 24 godzin, aby potwierdzić zamówienie i ustalić termin montażu.' };
} catch {
} catch (error) {
console.error("Błąd formularza:", error);
return { success: false, message: 'Wystąpił błąd podczas wysyłania zamówienia. Spróbuj ponownie.' };
}
}
+5 -2
View File
@@ -44,7 +44,7 @@ export async function submitMultisplitForm(data: Record<string, unknown>) {
const validatedData = multisplitFormSchema.parse(processData);
const { error } = await mailClient.resend.emails.send({
const { data: emailData, error } = await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
replyTo: validatedData.email || undefined,
@@ -92,11 +92,14 @@ export async function submitMultisplitForm(data: Record<string, unknown>) {
});
if (error) {
return { success: false, message: "Wystąpił problem z wysłaniem. Spróbuj ponownie." };
console.error("Resend Error:", error);
return { success: false, message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego." };
}
console.log("Wysłano e-mail (ID):", emailData?.id);
return { success: true, message: "Dziękujemy za zapytanie! Skontaktujemy się z Tobą najszybciej jak to możliwe." };
} catch (error) {
console.error("Błąd formularza:", error);
return { success: false, message: "Błąd walidacji lub błąd serwera. Spróbuj ponownie później." };
}
}
+9 -2
View File
@@ -48,7 +48,7 @@ export async function submitAwariaForm(data: Record<string, unknown>) {
email: data.email || '[email protected]',
});
await mailClient.resend.emails.send({
const { data: emailData, error } = await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
subject: `${parsedData.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${escapeHtml(parsedData.brand)} — ${escapeHtml(parsedData.city)}`,
@@ -80,8 +80,15 @@ export async function submitAwariaForm(data: Record<string, unknown>) {
</div>
`,
});
if (error) {
console.error("Resend Error:", error);
return { success: false, message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego." };
}
console.log("Wysłano e-mail (ID):", emailData?.id);
return { success: true };
} catch {
} catch (error) {
console.error("Błąd formularza:", error);
return { success: false, message: 'Wystąpił błąd podczas wysyłania.' };
}
}