Compare commits
6 Commits
feat/cloud
...
fix/newsle
| Author | SHA1 | Date | |
|---|---|---|---|
| 770db3067d | |||
| 6ba58c3a86 | |||
| 8a99290209 | |||
| c04d43d9d7 | |||
| c4adc116d6 | |||
| 44007f99e1 |
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Outbound email via Cloudflare Email Service (`send_email` binding).
|
* Outbound email via Cloudflare Email Service (`send_email` binding).
|
||||||
*/
|
*/
|
||||||
|
import { env as workerEnv } from 'cloudflare:workers';
|
||||||
|
|
||||||
export type RuntimeEnv = Record<string, unknown>;
|
export type RuntimeEnv = Record<string, unknown>;
|
||||||
|
|
||||||
@ -15,8 +16,9 @@ type EmailBinding = {
|
|||||||
}) => Promise<{ messageId: string }>;
|
}) => Promise<{ messageId: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getEmailBinding(runtimeEnv: RuntimeEnv): EmailBinding | null {
|
export function getEmailBinding(runtimeEnv?: RuntimeEnv): EmailBinding | null {
|
||||||
const binding = runtimeEnv.EMAIL;
|
const env = (runtimeEnv ?? (workerEnv as unknown as RuntimeEnv)) as RuntimeEnv;
|
||||||
|
const binding = env.EMAIL;
|
||||||
if (binding && typeof binding === 'object' && 'send' in binding) {
|
if (binding && typeof binding === 'object' && 'send' in binding) {
|
||||||
return binding as EmailBinding;
|
return binding as EmailBinding;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Shared helpers for newsletter API routes (Cloudflare Worker / Astro).
|
* Shared helpers for newsletter API routes (Cloudflare Worker / Astro).
|
||||||
*/
|
*/
|
||||||
|
import { env as workerEnv } from 'cloudflare:workers';
|
||||||
|
|
||||||
export type RuntimeEnv = Record<string, unknown>;
|
export type RuntimeEnv = Record<string, unknown>;
|
||||||
|
|
||||||
@ -19,20 +20,9 @@ export type NewsletterSubscriber = {
|
|||||||
unsubscribed_at: string | null;
|
unsubscribed_at: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getRuntimeEnv(locals: unknown): RuntimeEnv {
|
/** Astro v6+: use `cloudflare:workers` env (locals.runtime.env was removed). */
|
||||||
if (
|
export function getRuntimeEnv(_locals?: unknown): RuntimeEnv {
|
||||||
locals &&
|
return workerEnv as unknown as RuntimeEnv;
|
||||||
typeof locals === 'object' &&
|
|
||||||
'runtime' in locals &&
|
|
||||||
locals.runtime &&
|
|
||||||
typeof locals.runtime === 'object' &&
|
|
||||||
'env' in locals.runtime &&
|
|
||||||
locals.runtime.env &&
|
|
||||||
typeof locals.runtime.env === 'object'
|
|
||||||
) {
|
|
||||||
return locals.runtime.env as RuntimeEnv;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
|
export function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
|
||||||
|
|||||||
@ -1,24 +1,13 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
|
import { env as workerEnv } from 'cloudflare:workers';
|
||||||
import { getEmailBinding, sendCloudflareEmail } from '../../lib/email';
|
import { getEmailBinding, sendCloudflareEmail } from '../../lib/email';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
type RuntimeEnv = Record<string, unknown>;
|
type RuntimeEnv = Record<string, unknown>;
|
||||||
|
|
||||||
function getRuntimeEnv(locals: unknown): RuntimeEnv {
|
function getRuntimeEnv(): RuntimeEnv {
|
||||||
if (
|
return workerEnv as unknown as RuntimeEnv;
|
||||||
locals &&
|
|
||||||
typeof locals === 'object' &&
|
|
||||||
'runtime' in locals &&
|
|
||||||
locals.runtime &&
|
|
||||||
typeof locals.runtime === 'object' &&
|
|
||||||
'env' in locals.runtime &&
|
|
||||||
locals.runtime.env &&
|
|
||||||
typeof locals.runtime.env === 'object'
|
|
||||||
) {
|
|
||||||
return locals.runtime.env as RuntimeEnv;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
|
function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
|
||||||
@ -69,8 +58,8 @@ function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: st
|
|||||||
return map[normalized] || defaultRecipient;
|
return map[normalized] || defaultRecipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, locals, url }) => {
|
export const POST: APIRoute = async ({ request, url }) => {
|
||||||
const runtimeEnv = getRuntimeEnv(locals);
|
const runtimeEnv = getRuntimeEnv();
|
||||||
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
|
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
|
||||||
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
||||||
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
||||||
|
|||||||
@ -79,5 +79,8 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.redirect(new URL(okPath, request.url).toString(), 303);
|
return Response.redirect(
|
||||||
|
new URL(`${okPath}?unsub=${encodeURIComponent(row.unsubscribe_token)}`, request.url).toString(),
|
||||||
|
303,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,7 +18,11 @@ import {
|
|||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
function confirmEmailCopy(locale: string, confirmUrl: string): { subject: string; text: string } {
|
function confirmEmailCopy(
|
||||||
|
locale: string,
|
||||||
|
confirmUrl: string,
|
||||||
|
unsubscribeUrl: string,
|
||||||
|
): { subject: string; text: string } {
|
||||||
if (locale === 'en') {
|
if (locale === 'en') {
|
||||||
return {
|
return {
|
||||||
subject: 'Confirm your NexStudio newsletter subscription',
|
subject: 'Confirm your NexStudio newsletter subscription',
|
||||||
@ -29,6 +33,9 @@ function confirmEmailCopy(locale: string, confirmUrl: string): { subject: string
|
|||||||
confirmUrl,
|
confirmUrl,
|
||||||
'',
|
'',
|
||||||
'If you did not request this, you can ignore this message.',
|
'If you did not request this, you can ignore this message.',
|
||||||
|
'',
|
||||||
|
'Unsubscribe:',
|
||||||
|
unsubscribeUrl,
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -42,6 +49,9 @@ function confirmEmailCopy(locale: string, confirmUrl: string): { subject: string
|
|||||||
confirmUrl,
|
confirmUrl,
|
||||||
'',
|
'',
|
||||||
'หากคุณไม่ได้ขอสมัคร สามารถเพิกเฉยต่อข้อความนี้ได้',
|
'หากคุณไม่ได้ขอสมัคร สามารถเพิกเฉยต่อข้อความนี้ได้',
|
||||||
|
'',
|
||||||
|
'ยกเลิกการสมัคร:',
|
||||||
|
unsubscribeUrl,
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -54,6 +64,9 @@ function confirmEmailCopy(locale: string, confirmUrl: string): { subject: string
|
|||||||
confirmUrl,
|
confirmUrl,
|
||||||
'',
|
'',
|
||||||
'Se non hai richiesto l’iscrizione, puoi ignorare questo messaggio.',
|
'Se non hai richiesto l’iscrizione, puoi ignorare questo messaggio.',
|
||||||
|
'',
|
||||||
|
'Per disiscriverti:',
|
||||||
|
unsubscribeUrl,
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -83,9 +96,9 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
}
|
}
|
||||||
const email = normalizeEmail(emailRaw);
|
const email = normalizeEmail(emailRaw);
|
||||||
|
|
||||||
const turnstileSecret =
|
// Turnstile solo se configurato esplicitamente per la newsletter.
|
||||||
readEnv('NEWSLETTER_TURNSTILE_SECRET_KEY', runtimeEnv) ||
|
// Non riusare CONTACT_TURNSTILE_SECRET_KEY: il form footer non ha il widget.
|
||||||
readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv);
|
const turnstileSecret = readEnv('NEWSLETTER_TURNSTILE_SECRET_KEY', runtimeEnv);
|
||||||
if (turnstileSecret) {
|
if (turnstileSecret) {
|
||||||
const turnstileToken = required(form.get('cf-turnstile-response'));
|
const turnstileToken = required(form.get('cf-turnstile-response'));
|
||||||
const isHuman = await verifyTurnstile(
|
const isHuman = await verifyTurnstile(
|
||||||
@ -102,6 +115,10 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
const unsubscribeToken = createToken();
|
const unsubscribeToken = createToken();
|
||||||
const origin = siteOrigin(url, runtimeEnv);
|
const origin = siteOrigin(url, runtimeEnv);
|
||||||
const confirmUrl = absoluteUrl(origin, `/api/newsletter/confirm?token=${confirmToken}`);
|
const confirmUrl = absoluteUrl(origin, `/api/newsletter/confirm?token=${confirmToken}`);
|
||||||
|
const unsubscribeUrl = absoluteUrl(
|
||||||
|
origin,
|
||||||
|
`/api/newsletter/unsubscribe?token=${unsubscribeToken}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (mode !== 'live') {
|
if (mode !== 'live') {
|
||||||
console.info('[newsletter:dev] Subscription request', {
|
console.info('[newsletter:dev] Subscription request', {
|
||||||
@ -170,7 +187,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const copy = confirmEmailCopy(locale, confirmUrl);
|
const copy = confirmEmailCopy(locale, confirmUrl, unsubscribeUrl);
|
||||||
try {
|
try {
|
||||||
await sendCloudflareEmail(emailBinding, {
|
await sendCloudflareEmail(emailBinding, {
|
||||||
fromEmail,
|
fromEmail,
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
---
|
---
|
||||||
import SubpageLayout from '../../layouts/SubpageLayout.astro';
|
import SubpageLayout from '../../layouts/SubpageLayout.astro';
|
||||||
|
|
||||||
|
const unsubToken = Astro.url.searchParams.get('unsub')?.trim() ?? '';
|
||||||
|
const unsubHref = unsubToken
|
||||||
|
? `/api/newsletter/unsubscribe?token=${encodeURIComponent(unsubToken)}`
|
||||||
|
: '';
|
||||||
---
|
---
|
||||||
|
|
||||||
<SubpageLayout
|
<SubpageLayout
|
||||||
@ -10,8 +15,19 @@ import SubpageLayout from '../../layouts/SubpageLayout.astro';
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Puoi disiscriverti in qualsiasi momento dal link presente in fondo a ogni
|
Puoi disiscriverti in qualsiasi momento dal link presente in fondo a ogni
|
||||||
messaggio.
|
messaggio della newsletter.
|
||||||
</p>
|
</p>
|
||||||
|
{
|
||||||
|
unsubHref ? (
|
||||||
|
<p>
|
||||||
|
Oppure disiscriviti subito:{' '}
|
||||||
|
<a class="text-sky-400/90 hover:text-sky-300" href={unsubHref}>
|
||||||
|
Annulla iscrizione
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
<p>
|
<p>
|
||||||
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
|
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user