import type { APIRoute } from 'astro'; import { absoluteUrl, createToken, getEmailBinding, getNewsletterDb, getRuntimeEnv, isValidEmail, normalizeEmail, readEnv, required, sanitizeRedirect, sendCloudflareEmail, siteOrigin, verifyTurnstile, type NewsletterSubscriber, } from '../../../lib/newsletter'; export const prerender = false; function confirmEmailCopy( locale: string, confirmUrl: string, unsubscribeUrl: string, ): { subject: string; text: string } { if (locale === 'en') { return { subject: 'Confirm your NexStudio newsletter subscription', text: [ 'Thanks for subscribing to the NexStudio newsletter.', '', 'Confirm your email by opening this link:', confirmUrl, '', 'If you did not request this, you can ignore this message.', '', 'Unsubscribe:', unsubscribeUrl, ].join('\n'), }; } if (locale === 'th') { return { subject: 'ยืนยันการสมัครรับจดหมายข่าว NexStudio', text: [ 'ขอบคุณที่สมัครรับจดหมายข่าว NexStudio', '', 'ยืนยันอีเมลของคุณโดยเปิดลิงก์นี้:', confirmUrl, '', 'หากคุณไม่ได้ขอสมัคร สามารถเพิกเฉยต่อข้อความนี้ได้', '', 'ยกเลิกการสมัคร:', unsubscribeUrl, ].join('\n'), }; } return { subject: 'Conferma iscrizione newsletter NexStudio', text: [ 'Grazie per esserti iscritto alla newsletter di NexStudio.', '', 'Conferma l’indirizzo email aprendo questo link:', confirmUrl, '', 'Se non hai richiesto l’iscrizione, puoi ignorare questo messaggio.', '', 'Per disiscriverti:', unsubscribeUrl, ].join('\n'), }; } export const POST: APIRoute = async ({ request, locals, url }) => { const runtimeEnv = getRuntimeEnv(locals); const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase(); const db = getNewsletterDb(runtimeEnv); const form = await request.formData(); const redirectOk = sanitizeRedirect(required(form.get('_next')), url) || '/newsletter/grazie'; const redirectErr = sanitizeRedirect(required(form.get('_error')), url) || redirectOk; // Honeypot if (required(form.get('_gotcha'))) { return Response.redirect(new URL(redirectOk, url).toString(), 303); } const emailRaw = required(form.get('email')); const locale = required(form.get('locale')) || 'it'; const source = required(form.get('source')) || 'footer'; if (!emailRaw || !isValidEmail(emailRaw)) { return new Response('Invalid email.', { status: 400 }); } const email = normalizeEmail(emailRaw); // Turnstile solo se configurato esplicitamente per la newsletter. // Non riusare CONTACT_TURNSTILE_SECRET_KEY: il form footer non ha il widget. const turnstileSecret = readEnv('NEWSLETTER_TURNSTILE_SECRET_KEY', runtimeEnv); if (turnstileSecret) { const turnstileToken = required(form.get('cf-turnstile-response')); const isHuman = await verifyTurnstile( turnstileToken, turnstileSecret, request.headers.get('CF-Connecting-IP'), ); if (!isHuman) { return new Response('Turnstile verification failed.', { status: 400 }); } } const confirmToken = createToken(); const unsubscribeToken = createToken(); const origin = siteOrigin(url, runtimeEnv); const confirmUrl = absoluteUrl(origin, `/api/newsletter/confirm?token=${confirmToken}`); const unsubscribeUrl = absoluteUrl( origin, `/api/newsletter/unsubscribe?token=${unsubscribeToken}`, ); if (mode !== 'live') { console.info('[newsletter:dev] Subscription request', { email, locale, source, confirmUrl, }); return Response.redirect(new URL(redirectOk, url).toString(), 303); } if (!db) { return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', { status: 500, }); } const emailBinding = getEmailBinding(runtimeEnv); const fromEmail = readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) || readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv); const notifyTo = readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) || readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv); if (!emailBinding || !fromEmail) { return new Response( 'Live mode requires EMAIL binding (Cloudflare Email Service) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).', { status: 500 }, ); } const existing = await db .prepare('SELECT * FROM newsletter_subscribers WHERE email = ? LIMIT 1') .bind(email) .first(); if (existing?.status === 'active') { // Already subscribed: redirect without revealing status too loudly. return Response.redirect(new URL(redirectOk, url).toString(), 303); } if (existing?.status === 'unsubscribed' || existing?.status === 'pending') { await db .prepare( `UPDATE newsletter_subscribers SET status = 'pending', locale = ?, source = ?, confirm_token = ?, unsubscribe_token = ?, confirmed_at = NULL, unsubscribed_at = NULL WHERE id = ?`, ) .bind(locale, source, confirmToken, unsubscribeToken, existing.id) .run(); } else { await db .prepare( `INSERT INTO newsletter_subscribers (email, status, locale, confirm_token, unsubscribe_token, source) VALUES (?, 'pending', ?, ?, ?, ?)`, ) .bind(email, locale, confirmToken, unsubscribeToken, source) .run(); } const copy = confirmEmailCopy(locale, confirmUrl, unsubscribeUrl); try { await sendCloudflareEmail(emailBinding, { fromEmail, toEmail: email, subject: copy.subject, text: copy.text, }); } catch (error) { console.error('[newsletter] confirm email failed', error); return Response.redirect(new URL(redirectErr, url).toString(), 303); } if (notifyTo) { try { await sendCloudflareEmail(emailBinding, { fromEmail, toEmail: notifyTo, subject: required(form.get('_subject')) || 'Nuova richiesta newsletter — NexStudio', text: [ `Nuova richiesta di iscrizione (pending conferma).`, `Email: ${email}`, `Locale: ${locale}`, `Source: ${source}`, ].join('\n'), }); } catch (error) { console.warn('[newsletter] admin notify failed', error); } } return Response.redirect(new URL(redirectOk, url).toString(), 303); };