Files
NexStudio/src/pages/api/newsletter/subscribe.ts
Javaxman 770db3067d Mostra il link di disiscrizione dopo la conferma newsletter.
Passa il token unsub alla pagina di conferma e lo include anche nell'email di double opt-in.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:45:54 +02:00

223 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 lindirizzo email aprendo questo link:',
confirmUrl,
'',
'Se non hai richiesto liscrizione, 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<NewsletterSubscriber>();
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);
};