sojorn/website/newsletter-reference.ts

196 lines
6 KiB
TypeScript

import type { APIRoute } from 'astro';
const SENDPULSE_ID = process?.env?.SENDPULSE_ID || '';
const SENDPULSE_SECRET = process?.env?.SENDPULSE_SECRET || '';
const MPLS_ADDRESS_BOOK_ID = process?.env?.MPLS_ADDRESS_BOOK_ID || '1'; // Will be updated after creating the MPLS list
interface SendPulseTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
interface SendPulseAddressBook {
id: number;
name: string;
emails: number;
}
interface SendPulseSubscribeResponse {
result: boolean;
error?: string;
}
async function verifyAltchaToken(token: string): Promise<boolean> {
if (!token || token.length < 10) return false;
try {
const decoded = JSON.parse(atob(token));
return decoded.challenge && decoded.salt && decoded.signature && typeof decoded.number === 'number';
} catch {
return false;
}
}
async function getSendPulseToken(): Promise<string> {
const response = await fetch('https://api.sendpulse.com/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: SENDPULSE_ID,
client_secret: SENDPULSE_SECRET,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('SendPulse token error:', errorText);
throw new Error('Failed to get SendPulse access token');
}
const data: SendPulseTokenResponse = await response.json();
return data.access_token;
}
async function getAddressBooks(token: string): Promise<SendPulseAddressBook[]> {
const response = await fetch('https://api.sendpulse.com/addressbooks', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to get address books');
}
return response.json();
}
async function subscribeToSendPulse(email: string, token: string): Promise<void> {
// Try to use the MPLS address book, or get the first available one
let addressBookId = MPLS_ADDRESS_BOOK_ID;
try {
const addressBooks = await getAddressBooks(token);
// Look for MPLS-specific address book first
const mplsBook = addressBooks.find(book =>
book.name.toLowerCase().includes('mpls') ||
book.name.toLowerCase().includes('website')
);
if (mplsBook) {
addressBookId = mplsBook.id.toString();
console.log(`Using MPLS address book: ${mplsBook.name} (ID: ${addressBookId})`);
} else if (!addressBooks.find(book => book.id.toString() === addressBookId) && addressBooks.length > 0) {
// If the specified ID doesn't exist, use the first available address book
addressBookId = addressBooks[0].id.toString();
console.log(`Using default address book: ${addressBooks[0].name} (ID: ${addressBookId})`);
}
} catch (error) {
console.warn('Could not fetch address books, using default ID');
}
const response = await fetch(`https://api.sendpulse.com/addressbooks/${addressBookId}/emails`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
emails: [
{
email: email,
variables: {
source: 'mpls-website',
subscribed_date: new Date().toISOString()
}
}
]
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || errorData.message || 'Failed to subscribe to newsletter';
throw new Error(errorMessage);
}
const data: SendPulseSubscribeResponse = await response.json();
if (!data.result) {
throw new Error(data.error || 'Subscription failed');
}
}
export const POST: APIRoute = async ({ request }) => {
try {
if (!SENDPULSE_ID || !SENDPULSE_SECRET) {
return new Response(
JSON.stringify({ error: 'Server is not configured for newsletter signup' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
const { email, altchaToken } = await request.json();
// Validate email
if (!email || typeof email !== 'string' || !email.includes('@')) {
return new Response(
JSON.stringify({ error: 'Invalid email address' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Validate ALTCHA token
if (!altchaToken || typeof altchaToken !== 'string') {
return new Response(
JSON.stringify({ error: 'Security verification required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Verify ALTCHA token
const isValid = await verifyAltchaToken(altchaToken);
if (!isValid) {
return new Response(
JSON.stringify({ error: 'Security verification failed. Please try again.' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Get SendPulse token
const token = await getSendPulseToken();
// Subscribe to newsletter
await subscribeToSendPulse(email.toLowerCase().trim(), token);
return new Response(
JSON.stringify({ success: true, message: 'Successfully subscribed to newsletter' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Newsletter subscription error:', error);
// Don't expose detailed error messages to the client
const userMessage = error instanceof Error && error.message.includes('already exists')
? 'This email is already subscribed to our newsletter.'
: 'Failed to subscribe. Please try again later.';
return new Response(
JSON.stringify({ error: userMessage }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
export const GET: APIRoute = () => {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
};