- 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
136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
'use server';
|
|
|
|
import { z } from 'zod';
|
|
import { headers } from 'next/headers';
|
|
import { getProductBySlug } from '@/content/products';
|
|
import { escapeHtml, rateLimit, verifyRecaptcha, baseContactSchema } from '@/lib/security';
|
|
import { getMailClient } from '@/lib/mail';
|
|
|
|
const orderSchema = baseContactSchema.extend({
|
|
address: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
products: z.string(),
|
|
total: z.string(), // We don't trust this total, just checking if it exists
|
|
});
|
|
|
|
const productItemSchema = z.array(z.object({
|
|
slug: z.string(),
|
|
quantity: z.number().int().min(1).max(100),
|
|
}));
|
|
|
|
export async function submitOrderForm(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);
|
|
if (!success) {
|
|
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
|
}
|
|
}
|
|
|
|
const mailClient = getMailClient();
|
|
if (!mailClient) {
|
|
return { success: false, message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna." };
|
|
}
|
|
|
|
const data = orderSchema.parse({
|
|
name: formData.get('name'),
|
|
phone: formData.get('phone'),
|
|
email: formData.get('email'),
|
|
address: formData.get('address') || '',
|
|
notes: formData.get('notes') || '',
|
|
consent: formData.get('consent'),
|
|
products: formData.get('products'),
|
|
total: formData.get('total'),
|
|
});
|
|
|
|
let rawProducts;
|
|
try {
|
|
rawProducts = JSON.parse(data.products);
|
|
} catch {
|
|
return { success: false, message: "Błąd przetwarzania koszyka." };
|
|
}
|
|
|
|
const parsedProducts = productItemSchema.parse(rawProducts);
|
|
|
|
if (parsedProducts.length === 0) {
|
|
return { success: false, message: "Koszyk jest pusty." };
|
|
}
|
|
|
|
let calculatedTotal = 0;
|
|
const validatedProducts = [];
|
|
|
|
for (const p of parsedProducts) {
|
|
const dbProduct = getProductBySlug(p.slug);
|
|
if (!dbProduct || !dbProduct.price) {
|
|
return { success: false, message: "Jeden z produktów w koszyku jest niedostępny." };
|
|
}
|
|
validatedProducts.push({
|
|
name: dbProduct.name,
|
|
quantity: p.quantity,
|
|
price: dbProduct.price
|
|
});
|
|
calculatedTotal += dbProduct.price * p.quantity;
|
|
}
|
|
|
|
const productRows = validatedProducts.map(p =>
|
|
`<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('');
|
|
|
|
const { data: emailData, error } = await mailClient.resend.emails.send({
|
|
from: mailClient.fromEmail,
|
|
to: mailClient.toEmail,
|
|
replyTo: data.email,
|
|
subject: `Nowe zamówienie od ${escapeHtml(data.name)} — ${calculatedTotal.toLocaleString('pl-PL')} zł`,
|
|
html: `
|
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
|
|
<div style="background:#0F2A47;padding:20px;text-align:center;">
|
|
<h1 style="color:#fff;margin:0;font-size:20px;">Nowe zamówienie — ThermCool</h1>
|
|
</div>
|
|
<div style="padding:24px;">
|
|
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
|
|
<p><strong>Imię:</strong> ${escapeHtml(data.name)}</p>
|
|
<p><strong>Telefon:</strong> <a href="tel:${escapeHtml(data.phone)}">${escapeHtml(data.phone)}</a></p>
|
|
<p><strong>Email:</strong> ${escapeHtml(data.email)}</p>
|
|
${data.address ? `<p><strong>Adres:</strong> ${escapeHtml(data.address)}</p>` : ''}
|
|
${data.notes ? `<p><strong>Uwagi:</strong> ${escapeHtml(data.notes)}</p>` : ''}
|
|
<h2 style="font-size:16px;">Zamówione produkty</h2>
|
|
<table style="width:100%;border-collapse:collapse;">
|
|
<thead><tr style="background:#f8fafc;">
|
|
<th style="padding:8px;text-align:left;">Produkt</th>
|
|
<th style="padding:8px;text-align:center;">Ilość</th>
|
|
<th style="padding:8px;text-align:right;">Wartość</th>
|
|
</tr></thead>
|
|
<tbody>${productRows}</tbody>
|
|
<tfoot><tr>
|
|
<td colspan="2" style="padding:8px;font-weight:bold;">Łącznie (urządzenia)</td>
|
|
<td style="padding:8px;font-weight:bold;text-align:right;">${calculatedTotal.toLocaleString('pl-PL')} zł</td>
|
|
</tr></tfoot>
|
|
</table>
|
|
<p style="margin-top:16px;font-size:12px;color:#64748b;">+ koszt montażu do ustalenia telefonicznie</p>
|
|
</div>
|
|
</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 (error) {
|
|
console.error("Błąd formularza:", error);
|
|
return { success: false, message: 'Wystąpił błąd podczas wysyłania zamówienia. Spróbuj ponownie.' };
|
|
}
|
|
}
|