Files
fad-trade-next/actions/newsletter.ts
砂糖 17a30157db feat: 添加福安德外贸相关内容并重构产品页面
重构产品中心页面布局,添加产品展示网格和面包屑导航
更新关于页面支持多部分内容展示,包括公司简介、发展历史等
添加新的i18n翻译内容,更新品牌名称为福安德外贸
移除主题切换功能,更新欢迎邮件模板
添加多个关于页面的MDX内容,包括公司简介、发展历史等
2025-11-21 16:18:05 +08:00

125 lines
3.7 KiB
TypeScript

import { normalizeEmail, validateEmail } from '@/lib/email';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { headers } from 'next/headers';
import { Resend } from 'resend';
// initialize resend
const resend = new Resend(process.env.RESEND_API_KEY);
// Resend Audience ID
const AUDIENCE_ID = process.env.RESEND_AUDIENCE_ID!;
// initialize Redis
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const REDIS_RATE_LIMIT_KEY = process.env.UPSTASH_REDIS_NEWSLETTER_RATE_LIMIT_KEY!;
const DAY_MAX_SUBMISSIONS = parseInt(process.env.DAY_MAX_SUBMISSIONS || '10');
// create rate limiter
const limiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(DAY_MAX_SUBMISSIONS, '1d'),
prefix: REDIS_RATE_LIMIT_KEY,
});
// Shared rate limit check
async function checkRateLimit() {
const headersList = await headers();
const ip = headersList.get('x-real-ip') ||
headersList.get('x-forwarded-for') ||
'unknown';
const { success } = await limiter.limit(ip);
if (!success) {
throw new Error('Too many submissions, please try again later');
}
}
export async function subscribeToNewsletter(email: string) {
try {
await checkRateLimit();
const normalizedEmail = normalizeEmail(email);
const { isValid, error } = validateEmail(normalizedEmail);
if (!isValid) {
throw new Error(error || 'Invalid email address');
}
// Check if already subscribed
// const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
// const user = list.data?.data.find((item) => item.email === normalizedEmail);
// if (user) {
// return { success: true, alreadySubscribed: true };
// }
// Add to audience
await resend.contacts.create({
audienceId: AUDIENCE_ID,
email: normalizedEmail,
});
// Send welcome email
const unsubscribeToken = Buffer.from(normalizedEmail).toString('base64');
const unsubscribeLink = `${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?token=${unsubscribeToken}`;
await resend.emails.send({
from: 'NextForge <' + process.env.ADMIN_EMAIL! + '>',
to: normalizedEmail,
subject: 'Welcome to 福安德外贸',
html: `
<h2>Welcome to 福安德外贸</h2>
<p>Thank you for subscribing to the newsletter. You will receive the latest updates and news.</p>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
If you wish to unsubscribe, please <a href="${unsubscribeLink}">click here</a>
</p>
`,
headers: {
"List-Unsubscribe": `<${unsubscribeLink}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}
});
return { success: true };
} catch (error) {
console.error('Newsletter subscription failed:', error);
throw error;
}
}
export async function unsubscribeFromNewsletter(token: string) {
try {
await checkRateLimit();
const email = Buffer.from(token, 'base64').toString();
const normalizedEmail = normalizeEmail(email);
const { isValid, error } = validateEmail(normalizedEmail);
if (!isValid) {
throw new Error(error || 'Invalid email address');
}
// Check if subscribed
const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
const user = list.data?.data.find((item) => item.email === normalizedEmail);
if (!user) {
throw new Error('This email is not subscribed to our notifications');
}
// Remove from audience
await resend.contacts.remove({
audienceId: AUDIENCE_ID,
email: normalizedEmail,
});
return { success: true, email: normalizedEmail };
} catch (error) {
console.error('Newsletter unsubscribe failed:', error);
throw error;
}
}