// Modération : Jules consulte public/data/pratiques-pending.json, // déplace les entrées validées dans public/data/pratiques-regeneratives.json, // supprime de pending. À automatiser en V2 (UI admin). import { z } from 'zod' import { checkRateLimitJson } from '~/server/utils/rateLimitJson' import { existsSync, readFileSync, writeFileSync } from 'fs' import { resolve } from 'path' // ── Schéma Zod (miroir du client) ───────────────────────────────────────────── export const PratiqueSubmitSchema = z.object({ nom: z.string().min(3, 'Minimum 3 caractères').max(150, 'Maximum 150 caractères').trim(), url: z .string() .url('URL invalide (commencer par https://)') .optional() .or(z.literal('')) .transform((v) => v || undefined), description_user: z .string() .min(50, 'Minimum 50 caractères') .max(500, 'Maximum 500 caractères') .trim(), criteres: z .array(z.number().int().min(1).max(8)) .min(3, 'Sélectionne au moins 3 critères') .max(8, 'Maximum 8 critères'), pays: z.string().length(2, 'Code pays invalide').or(z.literal('AUTRE')), pays_autre: z.string().max(50).optional(), ville: z.string().max(100).optional().transform((v) => v?.trim() || undefined), type: z.enum([ 'agence', 'cooperative', 'collectif', 'reseau', 'asso', 'recherche', 'mouvement', 'plateforme', 'inconnu', ], { errorMap: () => ({ message: 'Type d\'entité invalide' }) }), tags: z.array(z.string().max(30)).max(6).optional(), submitted_by_email: z .string() .email('Email invalide') .optional() .or(z.literal('')) .transform((v) => v || undefined), }) export type PratiqueSubmitInput = z.infer // ── Chemin du fichier pending ───────────────────────────────────────────────── function getPendingPath(): string { return resolve(process.cwd(), 'public/data/pratiques-pending.json') } function readPending(): PratiqueSubmitInput[] { const path = getPendingPath() try { if (!existsSync(path)) return [] return JSON.parse(readFileSync(path, 'utf-8')) } catch { return [] } } function writePending(entries: unknown[]) { writeFileSync(getPendingPath(), JSON.stringify(entries, null, 2), 'utf-8') } // ── Handler principal ───────────────────────────────────────────────────────── export default defineEventHandler(async (event) => { // 1. Récupérer l'IP (proxy-aware) const ip = getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() || event.node.req.socket?.remoteAddress || '0.0.0.0' // 2. Rate limit JSON : 3 soumissions / IP / jour const allowed = checkRateLimitJson(ip, 'submit-pratique', 3) if (!allowed) { throw createError({ statusCode: 429, statusMessage: 'Trop de soumissions. Réessaie demain.', }) } // 3. Lire et valider le body const body = await readBody(event) const parsed = PratiqueSubmitSchema.safeParse(body) if (!parsed.success) { throw createError({ statusCode: 422, statusMessage: 'Validation échouée', data: parsed.error.flatten().fieldErrors, }) } const data = parsed.data // 4. Construire l'entrée pending const timestamp = Date.now() const entry = { ...data, id: timestamp, submitted_at: new Date().toISOString(), moderation_status: 'pending' as const, } // 5. Append à pratiques-pending.json const pending = readPending() pending.push(entry) writePending(pending) return { ok: true, trackingId: timestamp, } })