Sostituisce locals.runtime.env con import da cloudflare:workers, ripristinando contatti e newsletter. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
/**
|
|
* Shared helpers for newsletter API routes (Cloudflare Worker / Astro).
|
|
*/
|
|
import { env as workerEnv } from 'cloudflare:workers';
|
|
|
|
export type RuntimeEnv = Record<string, unknown>;
|
|
|
|
export type SubscriberStatus = 'pending' | 'active' | 'unsubscribed';
|
|
|
|
export type NewsletterSubscriber = {
|
|
id: number;
|
|
email: string;
|
|
status: SubscriberStatus;
|
|
locale: string;
|
|
confirm_token: string | null;
|
|
unsubscribe_token: string;
|
|
source: string;
|
|
created_at: string;
|
|
confirmed_at: string | null;
|
|
unsubscribed_at: string | null;
|
|
};
|
|
|
|
/** Astro v6+: use `cloudflare:workers` env (locals.runtime.env was removed). */
|
|
export function getRuntimeEnv(_locals?: unknown): RuntimeEnv {
|
|
return workerEnv as unknown as RuntimeEnv;
|
|
}
|
|
|
|
export function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
|
|
const runtimeValue = runtimeEnv[name];
|
|
if (typeof runtimeValue === 'string' && runtimeValue.trim().length > 0) {
|
|
return runtimeValue.trim();
|
|
}
|
|
const staticValue = import.meta.env[name];
|
|
return typeof staticValue === 'string' ? staticValue.trim() : '';
|
|
}
|
|
|
|
export function getNewsletterDb(runtimeEnv: RuntimeEnv): D1Database | null {
|
|
const db = runtimeEnv.NEWSLETTER_DB;
|
|
if (db && typeof db === 'object' && 'prepare' in db) {
|
|
return db as D1Database;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function sanitizeRedirect(candidate: string, requestUrl: URL): string {
|
|
if (!candidate) return '';
|
|
try {
|
|
const u = new URL(candidate, requestUrl);
|
|
if (u.origin !== requestUrl.origin) return '';
|
|
return u.pathname + u.search + u.hash;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function required(value: FormDataEntryValue | null): string {
|
|
return typeof value === 'string' ? value.trim() : '';
|
|
}
|
|
|
|
export function isValidEmail(email: string): boolean {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254;
|
|
}
|
|
|
|
export function normalizeEmail(email: string): string {
|
|
return email.trim().toLowerCase();
|
|
}
|
|
|
|
export function createToken(): string {
|
|
const bytes = new Uint8Array(32);
|
|
crypto.getRandomValues(bytes);
|
|
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
export async function verifyTurnstile(
|
|
token: string,
|
|
secret: string,
|
|
ip?: string | null,
|
|
): Promise<boolean> {
|
|
const body = new URLSearchParams();
|
|
body.set('secret', secret);
|
|
body.set('response', token);
|
|
if (ip) body.set('remoteip', ip);
|
|
|
|
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
if (!response.ok) return false;
|
|
const payload = (await response.json()) as { success?: boolean };
|
|
return payload.success === true;
|
|
}
|
|
|
|
export { getEmailBinding, sendCloudflareEmail } from './email';
|
|
|
|
export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string {
|
|
const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv);
|
|
if (configured) {
|
|
try {
|
|
return new URL(configured).origin;
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
}
|
|
return requestUrl.origin;
|
|
}
|
|
|
|
export function absoluteUrl(origin: string, path: string): string {
|
|
return new URL(path, origin).toString();
|
|
}
|