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:
@@ -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
@@ -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;">
|
||||
|
||||
@@ -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,
|
||||
@@ -121,8 +121,15 @@ export async function submitOrderForm(prevState: unknown, formData: FormData) {
|
||||
`,
|
||||
});
|
||||
|
||||
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.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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." };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user