61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
export function escapeHtml(unsafe: string): string {
|
|
if (typeof unsafe !== 'string') return '';
|
|
return unsafe
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
const rateLimitMap = new Map<string, { count: number; expiresAt: number }>();
|
|
const MAX_REQUESTS = 5;
|
|
const WINDOW_MS = 10 * 60 * 1000; // 10 minutes
|
|
|
|
export function rateLimit(ip: string | undefined | null): boolean {
|
|
if (!ip) return true; // fallback if IP can't be resolved
|
|
|
|
const now = Date.now();
|
|
const entry = rateLimitMap.get(ip);
|
|
|
|
if (!entry || entry.expiresAt < now) {
|
|
rateLimitMap.set(ip, { count: 1, expiresAt: now + WINDOW_MS });
|
|
return true;
|
|
}
|
|
|
|
if (entry.count >= MAX_REQUESTS) {
|
|
return false;
|
|
}
|
|
|
|
entry.count++;
|
|
return true;
|
|
}
|
|
|
|
export async function verifyRecaptcha(token: string): Promise<boolean> {
|
|
const secretKey = process.env.RECAPTCHA_SECRET_KEY;
|
|
if (!secretKey) return false;
|
|
|
|
try {
|
|
const res = await fetch(`https://www.google.com/recaptcha/api/siteverify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `secret=${secretKey}&response=${token}`,
|
|
});
|
|
const data = await res.json();
|
|
return data.success;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const baseContactSchema = z.object({
|
|
name: z.string().min(2, "Imię musi mieć co najmniej 2 znaki").max(100, "Imię jest zbyt długie"),
|
|
phone: z.string().regex(/^[+\d\s-]{9,20}$/, "Niepoprawny format telefonu"),
|
|
email: z.string().email("Niepoprawny adres e-mail").max(150, "E-mail jest zbyt długi"),
|
|
consent: z.string().refine(val => val === "on" || val === "true", "Wymagana zgoda"),
|
|
});
|