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
-10
View File
@@ -1,10 +0,0 @@
import { products } from './content/products';
const values = new Set<number>();
products.filter(p => p.category === 'multisplit').forEach(p => {
if (p.specs && p.specs['Liczba jednostek wewn.']) {
values.add(parseFloat(p.specs['Liczba jednostek wewn.']));
}
});
const sorted = Array.from(values).sort((a, b) => a - b);
console.log('Unique unit counts:', sorted);
+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.' };
}
}
-110
View File
@@ -1,110 +0,0 @@
/* eslint-disable */
const fs = require('fs');
const path = require('path');
function walkDir(dir, callback) {
if (!fs.existsSync(dir)) return;
fs.readdirSync(dir).forEach(f => {
const dirPath = path.join(dir, f);
const isDirectory = fs.statSync(dirPath).isDirectory();
if (isDirectory) {
walkDir(dirPath, callback);
} else if (dirPath.endsWith('.tsx')) {
callback(dirPath);
}
});
}
const appFiles = [];
const componentsFiles = [];
walkDir('app', f => appFiles.push(f));
walkDir('components', f => componentsFiles.push(f));
const allFiles = [...appFiles, ...componentsFiles];
const results = {};
allFiles.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const headings = [];
lines.forEach((line, index) => {
const regex = /<h([1-6])[\s>]/g;
let match;
while ((match = regex.exec(line)) !== null) {
headings.push({ level: parseInt(match[1]), line: index + 1, content: line.trim() });
}
});
if (headings.length > 0 || file.endsWith('page.tsx')) {
results[file] = headings;
}
});
// Analysis
console.log("=== HEADINGS AUDIT ===");
// 1. Pages with >1 H1
console.log("\n1. Strony z więcej niż jednym <h1>:");
let foundMultipleH1 = false;
for (const [file, headings] of Object.entries(results)) {
if (file.includes('page.tsx')) {
const h1s = headings.filter(h => h.level === 1);
if (h1s.length > 1) {
console.log(`- ${file} (Liczba <h1>: ${h1s.length})`);
foundMultipleH1 = true;
}
}
}
if (!foundMultipleH1) console.log("Brak.");
// 2. Pages with 0 H1
console.log("\n2. Strony (page.tsx) bez <h1>:");
let foundZeroH1 = false;
for (const [file, headings] of Object.entries(results)) {
if (file.includes('page.tsx') && !file.includes('api/')) { // ignoring api if any
const h1s = headings.filter(h => h.level === 1);
if (h1s.length === 0) {
console.log(`- ${file}`);
foundZeroH1 = true;
}
}
}
if (!foundZeroH1) console.log("Brak.");
// 3. Components rendering H1
console.log("\n3. Komponenty współdzielone (w components/) z <h1>:");
let foundComponentH1 = false;
for (const [file, headings] of Object.entries(results)) {
if (file.startsWith('components/')) {
const h1s = headings.filter(h => h.level === 1);
if (h1s.length > 0) {
console.log(`- ${file} (${h1s.length} wystąpień <h1>)`);
foundComponentH1 = true;
}
}
}
if (!foundComponentH1) console.log("Brak.");
// 4. Heading Skips
console.log("\n4. Skoki poziomów nagłówków w plikach:");
let foundSkips = false;
for (const [file, headings] of Object.entries(results)) {
if (headings.length === 0) continue;
let currentLevel = null;
for (const h of headings) {
if (currentLevel !== null) {
// A skip happens if the new level is strictly greater than currentLevel + 1
if (h.level > currentLevel + 1) {
console.log(`- ${file}: Skok z <h${currentLevel}> na <h${h.level}> w linii ${h.line}`);
foundSkips = true;
}
}
currentLevel = h.level;
}
}
if (!foundSkips) console.log("Brak oczywistych skoków.");
-43
View File
@@ -1,43 +0,0 @@
import os
import re
from collections import Counter
dirs_to_search = ['app', 'components']
button_classes = []
class_regex = re.compile(r'className=["\']([^"\']+)["\']')
dynamic_class_regex = re.compile(r'className=\{`([^`]+)`\}')
for d in dirs_to_search:
for root, _, files in os.walk(d):
for file in files:
if file.endswith(('.tsx', '.jsx', '.ts', '.js')):
filepath = os.path.join(root, file)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Find all classNames
matches = class_regex.findall(content) + dynamic_class_regex.findall(content)
for m in matches:
# Check if it looks like a button
if 'btn' in m or ('px-' in m and 'py-' in m and 'rounded' in m):
# Normalize spacing
normalized = ' '.join(m.split())
button_classes.append((normalized, filepath))
# Count frequencies
counts = Counter([c[0] for c in button_classes])
most_common = counts.most_common(10)
print("Top variants:")
for variant, count in most_common:
print(f"\nVariant ({count} times): {variant}")
# Find up to 3 files where it appears
files_seen = set()
for c, fpath in button_classes:
if c == variant and fpath not in files_seen:
files_seen.add(fpath)
print(f" - {fpath}")
if len(files_seen) >= 3:
break
-9
View File
@@ -1,9 +0,0 @@
export async function verifyRecaptcha(token: string): Promise<{ success: boolean; score?: number }> {
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`,
});
const data = await res.json();
return { success: data.success && (data.score === undefined || data.score >= 0.5), score: data.score };
}
-6426
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -1,20 +0,0 @@
import json
transcript_path = '/Users/michalcukrowski/.gemini/antigravity-ide/brain/1d7cf0f7-087b-4c82-94a5-07c51084a559/.system_generated/logs/transcript_full.jsonl'
with open(transcript_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
if data.get('type') == 'USER_INPUT':
content = data.get('content', '')
if 'export interface Product {' in content and len(content) > 100000:
print(f"Found large user input of length {len(content)}")
# Extract the file content. It probably starts with "export interface Product {"
# or there is some text before it.
idx = content.find('export interface Product {')
if idx != -1:
file_content = content[idx:]
with open('content/products.ts', 'w', encoding='utf-8') as out:
out.write(file_content)
print("Wrote to content/products.ts successfully!")
break
-14
View File
@@ -1,14 +0,0 @@
import json
transcript_path = '/Users/michalcukrowski/.gemini/antigravity-ide/brain/1d7cf0f7-087b-4c82-94a5-07c51084a559/.system_generated/logs/transcript_full.jsonl'
with open(transcript_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
if data.get('type') == 'USER_INPUT':
content = data.get('content', '')
print(f"USER_INPUT len: {len(content)}")
if len(content) > 50000:
with open('large_input.txt', 'w', encoding='utf-8') as out:
out.write(content)
print("Saved large input to large_input.txt")