Email marketing
This commit is contained in:
38
src/app/(main)/api/marketing/newsletter/images/route.ts
Normal file
38
src/app/(main)/api/marketing/newsletter/images/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { uploadFileToR2 } from '@/lib/r2';
|
||||
|
||||
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (cookies().get('newsletter-admin')?.value !== 'authenticated') {
|
||||
return NextResponse.json({ error: 'Unauthorized.' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('image');
|
||||
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: 'Choose an image to upload.' }, { status: 400 });
|
||||
}
|
||||
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
|
||||
return NextResponse.json({ error: 'Use a JPG, PNG, or WebP image.' }, { status: 400 });
|
||||
}
|
||||
if (!file.size || file.size > MAX_IMAGE_BYTES) {
|
||||
return NextResponse.json({ error: 'Images must be smaller than 5 MB.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const url = await uploadFileToR2(
|
||||
Buffer.from(await file.arrayBuffer()),
|
||||
`newsletter-${file.name}`,
|
||||
file.type
|
||||
);
|
||||
|
||||
return NextResponse.json({ url, filename: file.name });
|
||||
} catch (error) {
|
||||
console.error('Newsletter image upload error:', error);
|
||||
return NextResponse.json({ error: 'Unable to upload image.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
126
src/app/(main)/api/marketing/newsletter/route.ts
Normal file
126
src/app/(main)/api/marketing/newsletter/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { sendNewsletterEmail } from '@/lib/email';
|
||||
import { createMarketingUnsubscribeUrl } from '@/lib/marketingEmail';
|
||||
|
||||
export const maxDuration = 300;
|
||||
|
||||
type Audience = 'all_users' | 'newsletter_subscribers';
|
||||
type Format = 'text' | 'html';
|
||||
|
||||
function isAdmin() {
|
||||
return cookies().get('newsletter-admin')?.value === 'authenticated';
|
||||
}
|
||||
|
||||
function isEmail(value: unknown): value is string {
|
||||
return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||
}
|
||||
|
||||
async function getRecipients(audience: Audience) {
|
||||
if (audience === 'newsletter_subscribers') {
|
||||
const subscriptions = await db.newsletterSubscription.findMany({
|
||||
where: { status: 'subscribed' },
|
||||
select: { email: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
return subscriptions.map((subscription) => subscription.email);
|
||||
}
|
||||
|
||||
const suppressions = await db.newsletterSubscription.findMany({
|
||||
where: { status: 'unsubscribed' },
|
||||
select: { email: true },
|
||||
});
|
||||
const suppressed = new Set(suppressions.map((entry) => entry.email.toLowerCase()));
|
||||
const users = await db.user.findMany({
|
||||
select: { email: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return users.map((user) => user.email).filter((email) => !suppressed.has(email.toLowerCase()));
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
if (!isAdmin()) return NextResponse.json({ error: 'Unauthorized.' }, { status: 401 });
|
||||
|
||||
try {
|
||||
const [allUsers, newsletterSubscribers] = await Promise.all([
|
||||
getRecipients('all_users'),
|
||||
getRecipients('newsletter_subscribers'),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
audiences: {
|
||||
all_users: allUsers.length,
|
||||
newsletter_subscribers: newsletterSubscribers.length,
|
||||
},
|
||||
defaultTestEmail: process.env.NEWSLETTER_TEST_EMAIL || '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter composer audience error:', error);
|
||||
return NextResponse.json({ error: 'Unable to load eligible recipients.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!isAdmin()) return NextResponse.json({ error: 'Unauthorized.' }, { status: 401 });
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const subject = typeof body.subject === 'string' ? body.subject.trim() : '';
|
||||
const content = typeof body.content === 'string' ? body.content : '';
|
||||
const format: Format = body.format === 'html' ? 'html' : 'text';
|
||||
const audience: Audience = body.audience === 'newsletter_subscribers'
|
||||
? 'newsletter_subscribers'
|
||||
: 'all_users';
|
||||
|
||||
if (!subject || subject.length > 150 || !content.trim()) {
|
||||
return NextResponse.json({ error: 'Add a subject and email content before sending.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.action === 'test') {
|
||||
if (!isEmail(body.testEmail)) {
|
||||
return NextResponse.json({ error: 'Enter a valid test email address.' }, { status: 400 });
|
||||
}
|
||||
|
||||
await sendNewsletterEmail({
|
||||
email: body.testEmail,
|
||||
subject: `[Test] ${subject}`,
|
||||
content,
|
||||
format,
|
||||
unsubscribeUrl: createMarketingUnsubscribeUrl(body.testEmail),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, sent: 1 });
|
||||
}
|
||||
|
||||
if (body.action !== 'send' || body.confirm !== true) {
|
||||
return NextResponse.json({ error: 'Confirm the live send before continuing.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const recipients = await getRecipients(audience);
|
||||
let sent = 0;
|
||||
const failed: string[] = [];
|
||||
|
||||
for (const email of recipients) {
|
||||
try {
|
||||
await sendNewsletterEmail({
|
||||
email,
|
||||
subject,
|
||||
content,
|
||||
format,
|
||||
unsubscribeUrl: createMarketingUnsubscribeUrl(email),
|
||||
});
|
||||
sent++;
|
||||
} catch (error) {
|
||||
failed.push(email);
|
||||
console.error('Newsletter send failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, sent, failed: failed.length, total: recipients.length });
|
||||
} catch (error) {
|
||||
console.error('Newsletter composer send error:', error);
|
||||
return NextResponse.json({ error: 'Unable to send newsletter.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { db } from '@/lib/db';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/admin-login
|
||||
@@ -21,19 +19,22 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY: Only allow support@qrmaster.net to access newsletter admin
|
||||
const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net';
|
||||
const ALLOWED_ADMIN_PASSWORD = 'Timo.16092005';
|
||||
|
||||
if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) {
|
||||
const allowedAdminEmail = process.env.NEWSLETTER_ADMIN_EMAIL?.trim().toLowerCase();
|
||||
const allowedAdminPassword = process.env.NEWSLETTER_ADMIN_PASSWORD;
|
||||
|
||||
if (!allowedAdminEmail || !allowedAdminPassword) {
|
||||
console.error('Newsletter admin credentials are not configured.');
|
||||
return NextResponse.json({ error: 'Newsletter admin is not configured.' }, { status: 503 });
|
||||
}
|
||||
|
||||
if (email.toLowerCase() !== allowedAdminEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Access denied. Only authorized accounts can access this area.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password with hardcoded value
|
||||
if (password !== ALLOWED_ADMIN_PASSWORD) {
|
||||
if (password !== allowedAdminPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
|
||||
Reference in New Issue
Block a user