doxa-maps-cdn/server/api/contact.post.ts

99 lines
3.4 KiB
TypeScript

// Port of marketing-theme/functions/contact-rest-api.php.
// Browser POSTs form data + Turnstile token here; we verify the token
// server-side and forward the sanitized payload to the campaigns-sever
// public endpoint at `{prayBaseUrl}/api/contact` with X-API-Key.
import { defineEventHandler, readBody, createError } from 'h3'
// Message categories the form offers. They map onto the campaigns-server
// feedback types, which give the inbox conversation a `[Type]` subject prefix
// and a filterable tag. The server re-validates, so this list only needs to
// stay in sync with the labels rendered by app/pages/contact-us.vue.
const MESSAGE_TYPES = ['compliment', 'suggestion', 'problem']
// Origin marker the campaigns-server records on the conversation, so messages
// from this site stay distinguishable from in-app feedback (which shares the
// same pipeline). Must be an allowlisted value on the receiving end.
const SOURCE = 'doxa_life'
interface ContactBody {
name?: string
email?: string
message?: string
message_type?: string
consent_doxa_general?: boolean
language?: string
cf_turnstile?: string
}
function sanitizeText(s: unknown): string {
if (typeof s !== 'string') return ''
return s.trim().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').slice(0, 5000)
}
function sanitizeEmail(s: unknown): string {
if (typeof s !== 'string') return ''
return s.trim().toLowerCase().slice(0, 320)
}
export default defineEventHandler(async (event) => {
const body = await readBody<ContactBody>(event)
const config = useRuntimeConfig()
// 1. Turnstile verification (server-side). `verifyTurnstileToken` is
// auto-imported by @nuxtjs/turnstile.
const token = body?.cf_turnstile
if (!token) {
throw createError({ statusCode: 400, statusMessage: 'Invalid token' })
}
const turnstileResult = await verifyTurnstileToken(token)
if (!turnstileResult?.success) {
throw createError({ statusCode: 400, statusMessage: 'Invalid token' })
}
// 2. Sanitize inputs (matches PHP sanitize_text_field / sanitize_email)
const name = sanitizeText(body.name)
const email = sanitizeEmail(body.email)
const message = sanitizeText(body.message)
const consent = Boolean(body.consent_doxa_general)
const language = sanitizeText(body.language) || 'en'
const messageType = MESSAGE_TYPES.includes(sanitizeText(body.message_type))
? sanitizeText(body.message_type)
: ''
if (!email || !message) {
throw createError({ statusCode: 400, statusMessage: 'Email and message are required' })
}
if (!messageType) {
throw createError({ statusCode: 400, statusMessage: 'Please choose what your message is about' })
}
// 3. Forward to campaigns-sever (mirrors contact-rest-api.php)
if (!config.prayBaseUrl || !config.formApiKey) {
throw createError({ statusCode: 500, statusMessage: 'Prayer Tools integration not configured' })
}
try {
await $fetch(`${String(config.prayBaseUrl).replace(/\/$/, '')}/api/contact`, {
method: 'POST',
headers: {
'X-API-Key': config.formApiKey as string,
'Content-Type': 'application/json'
},
body: {
name,
email,
message,
feedback_type: messageType,
source: SOURCE,
consent_doxa_general: consent,
language
},
timeout: 15_000
})
} catch {
throw createError({ statusCode: 500, statusMessage: 'Failed to send message. Please try again.' })
}
return { status: 'success' }
})