src/app/api/submit-form/route.tsimport { studioUrl } from '@/utils/studio-helpers'import { NextResponse } from 'next/server'export const runtime = 'nodejs'function encodeFormData(data: Record<string, any>) {const fd = new FormData()for (const [k, v] of Object.entries(data)) {if (Array.isArray(v)) for (const item of v) fd.append(k, item as any)else if (v !== undefined && v !== null) fd.append(k, v as any)}return fd}function escapeHtml(s: string) {return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')}function renderValueCell(v: unknown): string {if (v == null) return ''if (typeof v === 'string') {const trimmed = v.trim()const esc = escapeHtml(trimmed).replace(/\r?\n/g, '<br>')return esc}if (Array.isArray(v)) {return v.map((x) =>typeof x === 'string' ? escapeHtml(x) : escapeHtml(JSON.stringify(x))).join(', ')}return escapeHtml(JSON.stringify(v))}function renderEmailHtml(payload: Record<string, any>) {const baseUrl = process.env.NEXT_PUBLIC_PRODUCTION_URL || ''// studioUrl returns an absolute CDN URL for processed images and a// root-relative path otherwise; only prepend the origin in the latter case.const logoPath = studioUrl('/logo.png')const logoSrc = /^https?:\/\//.test(logoPath)? logoPath: `${baseUrl}${logoPath}`const rows: string[] = []for (const [key, val] of Object.entries(payload)) {if (key === 'emailSubject') continue// filesif (key === 'files') {if (Array.isArray(val) && val.length) {const fileNames = val.map((f) => escapeHtml(f?.name || 'attachment')).join(', ')const label = escapeHtml(val[0]?.label ?? key)rows.push(`<p class="row"><strong class="key">${label}:</strong><span class="val">${fileNames}</span></p>`)} else {rows.push(`<p class="row"><strong class="key">${escapeHtml(key)}:</strong><span class="val"></span></p>`)}continue}const label =typeof val === 'object' && val? escapeHtml((val as any).label ?? key): escapeHtml(key)const value =typeof val === 'object' && val && (val as any).value !== undefined? renderValueCell((val as any).value): renderValueCell(val)rows.push(`<p class="row"><strong class="key">${label}:</strong><span class="val">${value}</span></p>`)}return `<!doctype html><html><head><meta charset="utf-8" /><style>body{background:#f7f7f7;font-family:Arial, sans-serif;margin:0;padding:20px}.container{max-width:100%;margin:auto;background:#f7f7f7;padding:75px 15px;}.content{width: 550px;margin:0 auto;}.logo{display:block;margin:0 auto 20px;max-width:200px; padding-bottom: 20px;}.row{margin:0 0 20px 0;line-height:1.45;font-size: 1.2rem;}.key{font-weight:700;margin-right:.35rem;display:inline-block}.val{display:inline}.multiline{white-space:pre-wrap;background:#f7f7f7;border-radius:6px;padding:8px;display:inline-block}</style></head><body><div class="container"><div class="content"><img class="logo" src="${logoSrc}" alt="Logo" />${rows.join('')}</div></div></body></html>`}export async function POST(req: Request) {const payload = (await req.json().catch(() => ({}))) as Record<string, any>// Honeypot checkif (payload.website?.value && String(payload.website.value).trim() !== '') {return NextResponse.json({ message: 'Message did not send.' },{ status: 400 })}delete payload.website// Time-based bot checkconst submissionTime = payload._submissionTimedelete payload._submissionTimeif (typeof submissionTime === 'number' && submissionTime < 3000) {return NextResponse.json({ message: 'Message did not send.' },{ status: 400 })}// Turnstile verificationconst turnstileToken = payload._turnstileTokendelete payload._turnstileTokenif (turnstileToken) {const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify',{method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({secret: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY,response: turnstileToken,}),})const turnstileData = await turnstileRes.json()if (!turnstileData.success) {return NextResponse.json({ message: 'Verification failed.' },{ status: 400 })}}const emailSubjectValue = payload.emailSubject?.value? String(payload.emailSubject.value): 'Form Submission'const displayPayload = { ...payload }delete (displayPayload as any).emailSubjectconst html = renderEmailHtml(displayPayload)const isDevelopment = process.env.NODE_ENV === 'development'const productionUrlClean = process.env.NEXT_PUBLIC_PRODUCTION_URL?.replace(/^https?:\/\//,'')const first =payload.firstName?.value ||payload.firstname?.value ||payload.name?.value ||''const last = payload.lastName?.value || payload.lastname?.value || ''const email = payload.email?.value || payload.contactEmail?.value || undefinedconst displayName = [first, last].filter(Boolean).join(' ') || first || emailconst subject = displayName? `${displayName}: ${emailSubjectValue} ${productionUrlClean}`: `${emailSubjectValue} ${productionUrlClean}`const recipients = (process.env.MAILGUN_TO_RECIPIENTS || '').split(',').map((r) => r.trim()).filter(Boolean)const productionTo = recipients.length? recipients: [`${process.env.MAILGUN_FROM_NAME} <${process.env.MAILGUN_SMTP_MAIL}>`]const body: Record<string, any> = {from: `${process.env.MAILGUN_FROM_NAME} <contact@${process.env.MAILGUN_DOMAIN_NAME}>`,to: isDevelopment? `${process.env.MAILGUN_DEV_NAME} <${process.env.MAILGUN_DEV_MAIL}>`: productionTo,...(!isDevelopment && {bcc: [`${process.env.MAILGUN_DEV_NAME} <${process.env.MAILGUN_DEV_MAIL}>`,...(email ? [`${displayName} <${email}>`] : []),],}),...(email ? { 'h:Reply-To': `${displayName} <${email}>` } : {}),subject,html,}// Attach files from base64 (client already encodes)const files = Array.isArray(payload.files) ? payload.files : []if (files.length) {body.attachment = files.map((file: any) => {const buffer = Buffer.from(file.base64.split(',')[1], 'base64')return new File([buffer], file.name, { type: file.type })})}const resp = await fetch(`https://api.mailgun.net/v3/${process.env.MAILGUN_DOMAIN_NAME}/messages`,{method: 'POST',headers: {Authorization: `Basic ${Buffer.from(`api:${process.env.MAILGUN_API_KEY}`).toString('base64')}`,},body: encodeFormData(body),})const text = await resp.text()let ok = falsetry {ok = text !== 'Forbidden' && Boolean(JSON.parse(text).id)} catch {ok = false}return NextResponse.json({ message: ok ? 'Message sent. Thank you.' : 'An error occurred.' },{ status: ok ? 200 : 500 })}