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>
223 lines
6.7 KiB
TypeScript
223 lines
6.7 KiB
TypeScript
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<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);
|
||
};
|