feat: security audit, rate limiting, server-side pricing, html escaping and security headers
This commit is contained in:
@@ -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.' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user