import { z } from "zod"; /** Supprime les caractères de contrôle et normalise les espaces. */ export function cleanText(value: string): string { return value .normalize("NFC") .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "") .replace(/[ \t]+/g, " ") .trim(); } /** Une seule ligne : empêche l'injection d'en-têtes dans les courriels. */ const singleLine = (max: number) => z .string() .transform(cleanText) .pipe( z .string() .min(1, "Ce champ est requis.") .max(max, `Maximum ${max} caractères.`) .refine((v) => !/[\r\n]/.test(v), "Les retours à la ligne ne sont pas permis."), ); export const contactSchema = z.object({ name: singleLine(120), email: z .string() .transform((v) => cleanText(v).toLowerCase()) .pipe(z.email("Adresse courriel invalide.").max(254)), organization: z .string() .optional() .default("") .transform(cleanText) .pipe(z.string().max(160, "Maximum 160 caractères.")), subject: singleLine(160), message: z .string() .transform((v) => v.normalize("NFC").replace(/\r\n?/g, "\n").trim()) .pipe( z .string() .min(20, "Décrivez votre demande en au moins 20 caractères.") .max(4000, "Maximum 4 000 caractères."), ), consent: z.literal(true, { error: "Votre consentement est requis pour traiter la demande.", }), /** Honeypot : doit rester vide (champ invisible pour les humains). */ website: z.string().max(0).optional().default(""), /** Horodatage du rendu du formulaire : rejette les soumissions instantanées. */ startedAt: z.coerce.number().int().nonnegative().optional(), }); export type ContactInput = z.input; export type ContactData = z.output; /** Schéma côté client (sans honeypot/horodatage, consentement booléen). */ export const contactFormSchema = contactSchema.omit({ website: true, startedAt: true, }); export type ContactFormValues = z.input;