feat: security audit, rate limiting, server-side pricing, html escaping and security headers

This commit is contained in:
MichalC
2026-08-01 13:24:41 +02:00
parent f07f2c2407
commit 47a9ac07fb
24 changed files with 292 additions and 151 deletions
+55 -39
View File
@@ -1,71 +1,87 @@
'use server';
import { Resend } from 'resend';
import { verifyRecaptcha } from '@/lib/recaptcha';
import { z } from 'zod';
import { headers } from "next/headers";
import { escapeHtml, rateLimit, verifyRecaptcha, baseContactSchema } from "@/lib/security";
import { getMailClient } from "@/lib/mail";
const resend = new Resend(process.env.RESEND_API_KEY);
const awariaFormSchema = baseContactSchema.omit({ consent: true }).extend({
isUrgent: z.boolean().optional(),
brand: z.string().min(1, "Wymagana nazwa urządzenia"),
model: z.string().optional(),
errorCode: z.string().optional(),
problemType: z.string().optional(),
description: z.string().optional(),
warranty: z.string().optional(),
lastService: z.string().optional(),
city: z.string().min(1, "Wymagane miasto"),
preferredDate: z.string().optional(),
preferredTime: z.string().optional(),
recaptchaToken: z.string().optional(),
});
export async function submitAwariaForm(data: {
isUrgent: boolean;
brand: string;
model: string;
errorCode: string;
problemType: string;
description: string;
warranty: string;
lastService: string;
name: string;
phone: string;
city: string;
preferredDate: string;
preferredTime: string;
recaptchaToken?: string;
}) {
export async function submitAwariaForm(data: Record<string, unknown>) {
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) {
if (!data.recaptchaToken) {
if (!data.recaptchaToken || typeof data.recaptchaToken !== 'string') {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
const { success } = await verifyRecaptcha(data.recaptchaToken);
const success = await verifyRecaptcha(data.recaptchaToken);
if (!success) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
}
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
to: process.env.RESEND_TO_EMAIL || '[email protected]',
subject: `${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${data.brand} — ${data.city}`,
const mailClient = getMailClient();
if (!mailClient) {
return { success: false, message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna." };
}
// Default missing fields for zod validation
const parsedData = awariaFormSchema.parse({
...data,
email: data.email || '[email protected]',
});
await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
subject: `${parsedData.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${escapeHtml(parsedData.brand)} — ${escapeHtml(parsedData.city)}`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="background:${data.isUrgent ? '#ef4444' : '#0F2A47'};padding:20px;text-align:center;">
<div style="background:${parsedData.isUrgent ? '#ef4444' : '#0F2A47'};padding:20px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">
${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe ThermCool
${parsedData.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe ThermCool
</h1>
</div>
<div style="padding:24px;">
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
<p><strong>Imię:</strong> ${data.name}</p>
<p><strong>Telefon:</strong> <a href="tel:${data.phone}">${data.phone}</a></p>
<p><strong>Miasto:</strong> ${data.city}</p>
${data.preferredDate ? `<p><strong>Preferowany termin:</strong> ${data.preferredDate} ${data.preferredTime || ''}</p>` : ''}
<p><strong>Imię:</strong> ${escapeHtml(parsedData.name)}</p>
<p><strong>Telefon:</strong> <a href="tel:${escapeHtml(parsedData.phone)}">${escapeHtml(parsedData.phone)}</a></p>
<p><strong>Miasto:</strong> ${escapeHtml(parsedData.city)}</p>
${parsedData.preferredDate ? `<p><strong>Preferowany termin:</strong> ${escapeHtml(parsedData.preferredDate)} ${escapeHtml(parsedData.preferredTime || '')}</p>` : ''}
<h2 style="font-size:16px;">Urządzenie</h2>
<p><strong>Marka:</strong> ${data.brand}</p>
${data.model ? `<p><strong>Model:</strong> ${data.model}</p>` : ''}
${data.warranty ? `<p><strong>Gwarancja:</strong> ${data.warranty}</p>` : ''}
${data.lastService ? `<p><strong>Ostatni serwis:</strong> ${data.lastService}</p>` : ''}
<p><strong>Marka:</strong> ${escapeHtml(parsedData.brand)}</p>
${parsedData.model ? `<p><strong>Model:</strong> ${escapeHtml(parsedData.model)}</p>` : ''}
${parsedData.warranty ? `<p><strong>Gwarancja:</strong> ${escapeHtml(parsedData.warranty)}</p>` : ''}
${parsedData.lastService ? `<p><strong>Ostatni serwis:</strong> ${escapeHtml(parsedData.lastService)}</p>` : ''}
<h2 style="font-size:16px;">Problem</h2>
<p><strong>Typ problemu:</strong> ${data.problemType}</p>
${data.errorCode ? `<p><strong>Kod błędu:</strong> ${data.errorCode}</p>` : ''}
${data.description ? `<p><strong>Opis:</strong> ${data.description}</p>` : ''}
<p><strong>Typ problemu:</strong> ${escapeHtml(parsedData.problemType || '')}</p>
${parsedData.errorCode ? `<p><strong>Kod błędu:</strong> ${escapeHtml(parsedData.errorCode)}</p>` : ''}
${parsedData.description ? `<p><strong>Opis:</strong> ${escapeHtml(parsedData.description)}</p>` : ''}
</div>
</div>
`,
});
return { success: true };
} catch {
return { success: false };
return { success: false, message: 'Wystąpił błąd podczas wysyłania.' };
}
}