import 'server-only'; import { verifyTurnstile } from '../turnstile/index.js'; import { checkRateLimit } from './rateLimit.js'; import { validateSubmission } from './validateSubmission.js'; /** * Handles a form submission end to end: rate limit, verify Turnstile, validate * against the form's own schema, then store. * * The order is cost-ascending on purpose — the cheapest checks reject first, so * a flood never reaches Turnstile's network call or the database. * * Emails aren't sent here. The form-builder sends whatever an editor configured * under the form's "Emails" tab (form-submissions hook → payload.sendEmail), * which goes out over panelSmtpAdapter. Storing the submission is enough. * * server-only: touches the Turnstile secret. */ export async function submitForm({ data, formId, ip, maxPerMinute = 5, payload, turnstileToken }) { // 1. Rate limit — cheapest gate, drops a flood before any real work. if (maxPerMinute > 0 && ip) { if (!checkRateLimit({ key: ip, max: maxPerMinute })) { return { reason: 'rate_limited', success: false }; } } // 2. Turnstile — verify when a token is supplied; reject on failure. if (turnstileToken !== undefined) { const ok = await verifyTurnstile({ ip, payload, token: turnstileToken }); if (!ok) { return { reason: 'turnstile', success: false }; } } // 3. Validate against the form's schema. A public endpoint can't trust the // shape of `data` — drop unknown keys, enforce required, cap length. const validation = await validateSubmission(payload, formId, data); if (!validation.ok) { if (validation.reason === 'not_found') { return { reason: 'not_found', success: false }; } return { reason: 'validation', success: false, ...validation.field ? { field: validation.field } : {}, ...validation.kind ? { kind: validation.kind } : {} }; } // 4. Persist (form-builder shape: submissionData array). Triggers the email // hook. Only validated, known fields are stored. try { const submission = await payload.create({ collection: 'form-submissions', data: { form: formId, submissionData: Object.entries(validation.cleaned).map(([field, value])=>({ field, value: value == null ? '' : String(value) })) } }); return { submissionId: submission.id, success: true }; } catch (err) { payload.logger.error(`[ipal] Form submission failed: ${err.message}`); return { reason: 'error', success: false }; } } //# sourceMappingURL=submitForm.js.map