import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { query, transaction } from '../../../lib/db' import nodemailer from 'nodemailer' // Enhanced validation schema for early access leads const EarlyAccessSchema = z.object({ firstName: z.string().min(1, 'First name is required').max(50), lastName: z.string().min(1, 'Last name is required').max(50), email: z.string().email('Invalid email address'), companyName: z.string().optional(), companyRole: z.string().optional(), interestLevel: z.enum(['technical_evaluation', 'strategic_demo', 'general_interest']), gdprConsent: z.boolean().refine(val => val === true, 'GDPR consent is required'), marketingConsent: z.boolean().default(false), leadSource: z.enum(['early_access_waitlist', 'request_early_access']), countryCode: z.string().default('AU'), customMessage: z.string().optional(), }) // Database-based rate limiting async function checkDatabaseRateLimit(clientIP: string, email: string): Promise { const now = new Date() const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) // Check IP-based rate limit (5 per hour) const ipAttempts = await query(` SELECT COUNT(*) as count FROM rate_limits WHERE identifier = $1 AND identifier_type = 'ip' AND window_start > $2 AND (blocked_until IS NULL OR blocked_until < NOW()) `, [clientIP, oneHourAgo]) if (parseInt(ipAttempts[0].count) >= 5) { throw new Error('Too many requests from this IP address. Please try again later.') } // Check email-based rate limit (3 per day) const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) const emailAttempts = await query(` SELECT COUNT(*) as count FROM rate_limits WHERE identifier = $1 AND identifier_type = 'email' AND window_start > $2 AND (blocked_until IS NULL OR blocked_until < NOW()) `, [email, oneDayAgo]) if (parseInt(emailAttempts[0].count) >= 3) { throw new Error('This email has been used too frequently. Please try again tomorrow.') } // Record this attempt (insert without conflict handling for now) try { await query(` INSERT INTO rate_limits (identifier, identifier_type, action_type, attempt_count) VALUES ($1, 'ip', 'form_submission', 1) `, [clientIP]) } catch (e) { // Ignore duplicate key errors for now } try { await query(` INSERT INTO rate_limits (identifier, identifier_type, action_type, attempt_count) VALUES ($1, 'email', 'form_submission', 1) `, [email]) } catch (e) { // Ignore duplicate key errors for now } } // Email configuration const createTransporter = () => { return nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), secure: process.env.SMTP_SECURE === 'true', auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }) } // Send notification email async function sendNotificationEmail(leadData: any, leadId: string) { if (!process.env.SMTP_USER || !process.env.NOTIFICATION_TO_EMAIL) { console.log('Email notifications not configured, skipping...') return } try { const transporter = createTransporter() const emailContent = ` New ${leadData.leadSource === 'request_early_access' ? 'Early Access Request' : 'Waitlist Signup'} - CHORUS Teaser Website Lead ID: ${leadId} Name: ${leadData.firstName} ${leadData.lastName} Email: ${leadData.email} Company: ${leadData.companyName || 'Not provided'} Role: ${leadData.companyRole || 'Not provided'} Interest Level: ${leadData.interestLevel} Lead Source: ${leadData.leadSource} GDPR Consent: ${leadData.gdprConsent ? 'Given' : 'Not given'} Marketing Consent: ${leadData.marketingConsent ? 'Given' : 'Not given'} Custom Message: ${leadData.customMessage || 'None'} Timestamp: ${new Date().toISOString()} IP Address: ${leadData.clientIP} User Agent: ${leadData.userAgent} ` await transporter.sendMail({ from: process.env.NOTIFICATION_FROM_EMAIL, to: process.env.NOTIFICATION_TO_EMAIL, subject: `New CHORUS ${leadData.leadSource === 'request_early_access' ? 'Early Access Request' : 'Waitlist Signup'}`, text: emailContent, }) console.log('Notification email sent successfully') } catch (error) { console.error('Failed to send notification email:', error) } } export async function POST(request: NextRequest) { try { // Parse request body const body = await request.json() // Validate input const validatedData = EarlyAccessSchema.parse(body) // Get client IP for rate limiting const clientIP = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || '127.0.0.1' const userAgent = request.headers.get('user-agent') || '' // Check rate limits using database await checkDatabaseRateLimit(clientIP, validatedData.email) // Insert lead into database using transaction const leadId = await transaction(async (client) => { // Insert the main lead record const leadResult = await client.query(` INSERT INTO leads ( first_name, last_name, email, company_name, company_role, lead_source, inquiry_details, custom_message, ip_address, user_agent, country_code, gdpr_consent_given, gdpr_consent_date, marketing_consent ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id `, [ validatedData.firstName, validatedData.lastName, validatedData.email, validatedData.companyName || null, validatedData.companyRole || null, validatedData.leadSource, validatedData.interestLevel, validatedData.customMessage || null, clientIP, userAgent, validatedData.countryCode, validatedData.gdprConsent, validatedData.gdprConsent ? new Date() : null, // gdpr_consent_date validatedData.marketingConsent ]) const leadId = leadResult.rows[0].id // Insert consent audit record await client.query(` INSERT INTO consent_audit ( lead_id, consent_type, consent_status, consent_method, ip_address, user_agent ) VALUES ($1, $2, $3, $4, $5, $6) `, [ leadId, 'gdpr_consent', 'given', 'web_form', clientIP, userAgent ]) // If marketing consent given, record that too if (validatedData.marketingConsent) { await client.query(` INSERT INTO consent_audit ( lead_id, consent_type, consent_status, consent_method, ip_address, user_agent ) VALUES ($1, $2, $3, $4, $5, $6) `, [ leadId, 'marketing_consent', 'given', 'web_form', clientIP, userAgent ]) } return leadId }) // Send notification email (async, don't block response) sendNotificationEmail({ ...validatedData, clientIP, userAgent }, leadId).catch(console.error) // Log successful capture console.log('Lead successfully captured:', { leadId, email: validatedData.email, leadSource: validatedData.leadSource, timestamp: new Date().toISOString() }) // Send success response const message = validatedData.leadSource === 'request_early_access' ? 'Early access request submitted successfully!' : 'Successfully joined the CHORUS waitlist!' return NextResponse.json({ success: true, message, leadId, }, { status: 201 }) } catch (error) { console.error('Early access signup error:', error) if (error instanceof z.ZodError) { return NextResponse.json( { error: 'Validation error', details: error.errors.map(err => ({ field: err.path.join('.'), message: err.message })) }, { status: 400 } ) } if (error instanceof Error) { // Check for specific database errors if (error.message.includes('duplicate key')) { return NextResponse.json( { error: 'This email has already been registered.' }, { status: 409 } ) } if (error.message.includes('rate limit')) { return NextResponse.json( { error: error.message }, { status: 429 } ) } } return NextResponse.json( { error: 'Internal server error. Please try again.' }, { status: 500 } ) } } // Handle OPTIONS for CORS export async function OPTIONS(request: NextRequest) { return new NextResponse(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, }) }