Files
avbeat-com/src/app/api/newsletter/subscribers/route.ts

188 lines
7.5 KiB
TypeScript

import { createClient } from '@/lib/supabase/server';
import { NextRequest, NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
// GET: list all subscribers
export async function GET(req: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { searchParams } = new URL(req.url);
const status = searchParams.get('status') || 'all';
const search = searchParams.get('search') || '';
let query = supabase
.from('newsletter_subscribers')
.select('*')
.order('subscribed_at', { ascending: false });
if (status !== 'all') query = query.eq('status', status);
if (search) query = query.ilike('email', `%${search}%`);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ subscribers: data || [] });
}
// DELETE: unsubscribe a subscriber
export async function DELETE(req: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await req.json();
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
const { error } = await supabase
.from('newsletter_subscribers')
.update({ status: 'unsubscribed', unsubscribed_at: new Date().toISOString() })
.eq('id', id);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ success: true });
}
// PATCH: bulk actions (bulk_unsubscribe, bulk_delete)
export async function PATCH(req: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { action, ids } = await req.json();
if (!action || !Array.isArray(ids) || ids.length === 0) {
return NextResponse.json({ error: 'action and ids[] required' }, { status: 400 });
}
if (action === 'bulk_unsubscribe') {
const { error } = await supabase
.from('newsletter_subscribers')
.update({ status: 'unsubscribed', unsubscribed_at: new Date().toISOString() })
.in('id', ids)
.eq('status', 'active');
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ success: true, count: ids.length });
}
if (action === 'bulk_delete') {
const { error } = await supabase
.from('newsletter_subscribers')
.delete()
.in('id', ids);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ success: true, count: ids.length });
}
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
}
// POST: send digest email to all active subscribers
export async function POST(req: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { subject, previewText, articles, customMessage } = await req.json();
if (!subject) return NextResponse.json({ error: 'Subject is required' }, { status: 400 });
if (!articles || articles.length === 0) {
return NextResponse.json({ error: 'At least one article is required' }, { status: 400 });
}
const smtpHost = process.env.SMTP_HOST;
const smtpPort = parseInt(process.env.SMTP_PORT || '587');
const smtpUser = process.env.SMTP_USER;
const smtpPass = process.env.SMTP_PASS;
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
const fromName = process.env.SMTP_FROM_NAME || 'BroadcastBeat';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
if (!smtpHost || !smtpUser || !smtpPass) {
return NextResponse.json({ error: 'SMTP credentials not configured' }, { status: 500 });
}
// Fetch active subscribers
const { data: subscribers, error: subError } = await supabase
.from('newsletter_subscribers')
.select('email')
.eq('status', 'active');
if (subError) return NextResponse.json({ error: subError.message }, { status: 500 });
if (!subscribers || subscribers.length === 0) {
return NextResponse.json({ error: 'No active subscribers found' }, { status: 400 });
}
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465,
auth: { user: smtpUser, pass: smtpPass },
});
// Build article HTML blocks
const articlesHtml = articles
.map(
(a: { title: string; excerpt?: string; url?: string; image?: string; category?: string }) => `
<div style="border-bottom:1px solid #1e3a5f;padding:20px 0;">
${a.category ? `<span style="color:#3b82f6;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''}
<h2 style="color:#e0e0e0;font-size:18px;margin:8px 0 6px;">
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#e0e0e0;text-decoration:none;">${a.title}</a>` : a.title}
</h2>
${a.excerpt ? `<p style="color:#888;font-size:14px;margin:0 0 10px;">${a.excerpt}</p>` : ''}
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#3b82f6;font-size:13px;text-decoration:none;">Read more →</a>` : ''}
</div>
`
)
.join('');
const emailHtml = `
<div style="background:#0d1520;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;">
<!-- Header -->
<div style="background:#111;border-bottom:2px solid #3b82f6;padding:24px 32px;">
<h1 style="color:#3b82f6;font-size:22px;margin:0;font-weight:bold;">BroadcastBeat</h1>
<p style="color:#555;font-size:12px;margin:4px 0 0;">News & Intelligence for Broadcast, Post-Production & Streaming Technology</p>
</div>
<!-- Body -->
<div style="padding:32px;">
${previewText ? `<p style="color:#aaa;font-size:15px;margin:0 0 24px;">${previewText}</p>` : ''}
${customMessage ? `<div style="background:#1a2535;border-left:3px solid #3b82f6;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''}
${articlesHtml}
</div>
<!-- Footer -->
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;">
<p style="color:#555;font-size:12px;margin:0;">You're receiving this because you subscribed at broadcastbeat.com.</p>
<p style="color:#555;font-size:12px;margin:8px 0 0;">
<a href="${siteUrl}" style="color:#3b82f6;">Visit BroadcastBeat</a> ·
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a>
</p>
</div>
</div>
`;
// Send to all subscribers (batch with BCC or individual)
const recipientEmails = subscribers.map((s) => s.email);
let sent = 0;
let failed = 0;
// Send in batches of 50 to avoid rate limits
const batchSize = 50;
for (let i = 0; i < recipientEmails.length; i += batchSize) {
const batch = recipientEmails.slice(i, i + batchSize);
try {
await transporter.sendMail({
from: `"${fromName}" <${fromEmail}>`,
bcc: batch,
subject,
html: emailHtml,
});
sent += batch.length;
} catch (err) {
console.error('Batch send error:', err);
failed += batch.length;
}
}
return NextResponse.json({ success: true, sent, failed, total: recipientEmails.length });
}