feat: add marketing email functionality for beta users#2
Conversation
Add the ability to send marketing emails to beta users through Resend API: - New API endpoint /api/send-marketing-email for sending marketing emails - UI component in beta invites admin page with email form - Support for filtering recipients (all, approved, used, pending) - Email preview with recipient count - Confirmation dialog before sending - Success/error feedback to admin - Bilingual support (English and Swedish) Features: - Compose custom subject and HTML content - Select target audience with recipient filters - Real-time recipient count display - BCC recipients for privacy - Admin-only access with authentication checks
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Pull Request Review: Marketing Email FunctionalityThank you for this contribution! I've reviewed the code and have the following feedback organized by category: 🔴 Critical Security Issues1. Missing Authentication in API RouteLocation: The endpoint only validates the Issue: const { subject, html, recipientFilter, adminUserId } = await request.json();
// ... later
if (!adminUserId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}Recommended Fix: Follow the pattern in // Get and verify the authorization header
const authHeader = request.headers.get('authorization');
if (!authHeader) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const token = authHeader.replace('Bearer ', '');
const { data: { user }, error: userError } = await supabase.auth.getUser(token);
if (userError || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Then verify this authenticated user is an admin
const { data: adminData, error: adminError } = await supabase
.from('admin_users')
.select('user_id')
.eq('user_id', user.id) // Use the authenticated user.id, not from request body
.single();2. Email Injection / XSS RiskLocation: The Risks:
Recommended Fix: // Validate and sanitize subject (no newlines)
if (!subject || subject.includes('\n') || subject.includes('\r')) {
return NextResponse.json(
{ error: 'Invalid subject: newlines not allowed' },
{ status: 400 }
);
}
// Consider adding a character limit
if (subject.length > 200 || html.length > 50000) {
return NextResponse.json(
{ error: 'Subject or content too long' },
{ status: 400 }
);
}
// For HTML: Consider using a library like DOMPurify or sanitize-html
// to prevent malicious scripts, or at minimum warn/log for review3. Rate Limiting MissingLocation: No rate limiting exists on this endpoint. An attacker (or accidental double-click) could send multiple bulk emails. Recommended Fix: Add rate limiting middleware or check for recent sends: // Check for recent marketing emails sent
const { data: recentEmails } = await supabase
.from('marketing_email_log') // You'd need to create this table
.select('sent_at')
.eq('sent_by', user.id)
.gte('sent_at', new Date(Date.now() - 60000).toISOString()) // Last minute
.single();
if (recentEmails) {
return NextResponse.json(
{ error: 'Please wait before sending another email' },
{ status: 429 }
);
}🟡 Moderate Issues4. Missing CSRF ProtectionThe frontend doesn't send any CSRF tokens. While Next.js API routes are generally protected by same-origin policy, consider adding explicit CSRF protection for admin actions. 5. No Audit LoggingLocation: There's no database record of who sent what emails to whom. This is critical for compliance (GDPR, CAN-SPAM) and debugging. Recommended: Create a
6. BCC Limit RiskLocation: Resend (and most email providers) have limits on BCC recipients (often 50-100). The code doesn't check or batch recipients. Recommended Fix: const BATCH_SIZE = 50; // Check Resend's actual limit
if (recipients.length > BATCH_SIZE) {
// Batch the sending
for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
const batch = recipients.slice(i, i + BATCH_SIZE);
await resend.emails.send({
from: `${appName} <${fromEmail}>`,
to: fromEmail,
bcc: batch,
subject: subject,
html: html,
});
}
} else {
// Send single batch
}7. Error Handling Exposes Internal DetailsLocation: Error responses include Recommended: Only expose detailed errors in development: return NextResponse.json(
{
error: 'Failed to send email',
...(process.env.NODE_ENV === 'development' && { details: emailResult.error })
},
{ status: 500 }
);🟢 Code Quality & Best Practices8. Duplicate Locale LogicLocation: This logic is duplicated from 9. Inconsistent Variable NamingLocation: Some variables use Fix: Remove unused variable: const resend = new Resend(process.env.RESEND_API_KEY);
// Remove lines 48-56 as they're redundant - resend is already initialized10. Unused ImportLocation:
11. Client-Side Recipient Count CalculationLocation: The recipient count is calculated client-side, but the actual filtering happens server-side. These could get out of sync if Recommended: Add a preview/count endpoint or have the API return the count before confirmation. 12. Missing Loading State UILocation: While
|
Security fixes: - CRITICAL: Fix admin authorization bypass by verifying session server-side instead of trusting client-provided user ID - Add proper authentication using Supabase SSR auth cookies - Sanitize HTML content with DOMPurify to prevent XSS attacks - Remove detailed error messages that could aid attackers - Add audit logging of admin actions Changes: - Use @supabase/ssr createServerClient to verify user session from cookies - Extract user ID from verified session, not from client request - Sanitize email HTML with DOMPurify before sending - Remove sensitive error details from API responses - Log admin user ID when marketing emails are sent - Install required packages: @supabase/ssr, isomorphic-dompurify 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
PR Review: Marketing Email Functionality for Beta UsersSummaryThis PR adds a marketing email feature allowing admins to send emails to beta users through the Resend API. The implementation includes proper authentication, HTML sanitization, and a user-friendly admin interface. ✅ Strengths
🔍 Issues & RecommendationsCritical Issues1. Rate Limiting Missing 🔴
// Consider adding rate limiting middleware or check
const lastSentKey = `marketing_email_sent_${user.id}`;
// Use Redis or database to track last send time2. Missing Webhook Secret Variable 🔴
Performance Concerns3. Potential Email Sending Limits 🟡
4. Missing Error Recovery 🟡
Security Concerns5. DOMPurify Configuration Could Be Stricter 🟡
const sanitizedHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'div', 'span', 'table', 'tr', 'td', 'th', 'thead', 'tbody'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'target'],
ALLOWED_URI_REGEXP: /^https?:\/\//, // Only allow http/https links
});6. No Email Content Validation 🟡
if (subject.length > 200) {
return NextResponse.json({ error: 'Subject too long (max 200 chars)' }, { status: 400 });
}
if (html.length > 100000) {
return NextResponse.json({ error: 'Email content too long' }, { status: 400 });
}Code Quality7. Duplicate Recipient Count Logic 🟡
const getRecipientCount = (filter: string, invites: BetaInvite[]) => {
switch (filter) {
case 'all': return invites.length;
case 'approved': return invites.filter(i => i.approved).length;
// ... etc
}
};8. Magic Strings for Recipient Filters 🟡
export const RECIPIENT_FILTERS = {
ALL: 'all',
APPROVED: 'approved',
APPROVED_NOT_USED: 'approved_not_used',
USED: 'used',
PENDING: 'pending',
} as const;Missing Features9. No Audit Trail 🟡
10. No Email Preview 🟢
🧪 Testing ConcernsMissing Test Coverage 🔴
Recommendations: // Example test structure needed:
describe('POST /api/send-marketing-email', () => {
it('should reject unauthenticated requests', async () => { ... });
it('should reject non-admin users', async () => { ... });
it('should sanitize HTML content', async () => { ... });
it('should validate recipient filters', async () => { ... });
it('should send emails to correct recipients', async () => { ... });
});📋 Additional Recommendations
🎯 Action Items Before MergeMust Fix (Blocking):
Should Fix (Recommended):
Nice to Have (Future PRs):
📊 Overall AssessmentCode Quality: 7/10 This is a solid foundation for the marketing email feature with good security practices (admin auth, XSS prevention, BCC privacy). However, it needs rate limiting, better error handling, and test coverage before production deployment. The code follows existing patterns in the codebase well and integrates nicely with the bilingual support and admin UI. With the recommended improvements, this will be a robust feature. Great work on the security-first approach with DOMPurify and admin authentication! 🎉 |
The SSR cookie-based auth was failing in API routes with "Auth session missing" error. Switched to using Authorization header with Bearer token instead. Changes: - API route now expects Authorization header with Bearer token - Client-side sends session access_token in Authorization header - Removed @supabase/ssr dependency in favor of standard supabase-js client - Added session check on client before making API call 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
PR Review: Marketing Email Functionality for Beta UsersSummaryThis PR adds marketing email functionality for beta users through the Resend API. The implementation includes proper authentication, HTML sanitization, recipient filtering, and a clean UI. Overall, this is a solid implementation with good security practices. ✅ StrengthsSecurity
Code Quality
UX
|
Add the ability to send marketing emails to beta users through Resend API:
Features: