import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger } from "../_shared/logger.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface ContactSubmission { name: string; email: string; subject: string; message: string; category: 'general' | 'moderation' | 'technical' | 'account' | 'partnership' | 'report' | 'other'; captchaToken?: string; } const handler = async (req: Request): Promise => { // Handle CORS preflight requests if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } const requestId = crypto.randomUUID(); const startTime = Date.now(); try { // Parse request body const body: ContactSubmission = await req.json(); const { name, email, subject, message, category, captchaToken } = body; edgeLogger.info('Contact form submission received', { requestId, email, category }); // Validate required fields if (!name || !email || !subject || !message || !category) { return createErrorResponse( { message: 'Missing required fields' }, 400, corsHeaders ); } // Validate field lengths if (name.length < 2 || name.length > 100) { return createErrorResponse( { message: 'Name must be between 2 and 100 characters' }, 400, corsHeaders ); } if (subject.length < 5 || subject.length > 200) { return createErrorResponse( { message: 'Subject must be between 5 and 200 characters' }, 400, corsHeaders ); } if (message.length < 20 || message.length > 2000) { return createErrorResponse( { message: 'Message must be between 20 and 2000 characters' }, 400, corsHeaders ); } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return createErrorResponse( { message: 'Invalid email address' }, 400, corsHeaders ); } // Validate category const validCategories = ['general', 'moderation', 'technical', 'account', 'partnership', 'report', 'other']; if (!validCategories.includes(category)) { return createErrorResponse( { message: 'Invalid category' }, 400, corsHeaders ); } // Get user agent and create IP hash const userAgent = req.headers.get('user-agent') || 'Unknown'; const clientIP = req.headers.get('x-forwarded-for') || 'Unknown'; const ipHash = clientIP !== 'Unknown' ? await crypto.subtle.digest('SHA-256', new TextEncoder().encode(clientIP + 'thrillwiki_salt')) .then(buf => Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')) : null; // Initialize Supabase client with service role for rate limiting and insertion const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); // Check rate limiting (max 3 submissions per email per hour) const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const { data: recentSubmissions, error: rateLimitError } = await supabase .from('contact_submissions') .select('id') .eq('email', email) .gte('created_at', oneHourAgo); if (rateLimitError) { edgeLogger.error('Rate limit check failed', { requestId, error: rateLimitError.message }); } else if (recentSubmissions && recentSubmissions.length >= 3) { edgeLogger.warn('Rate limit exceeded', { requestId, email }); return createErrorResponse( { message: 'Too many submissions. Please wait an hour before submitting again.' }, 429, corsHeaders ); } // Get user ID if authenticated const authHeader = req.headers.get('Authorization'); let userId: string | null = null; if (authHeader) { const supabaseClient = createClient( supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } } ); const { data: { user } } = await supabaseClient.auth.getUser(); userId = user?.id || null; } // Insert contact submission const { data: submission, error: insertError } = await supabase .from('contact_submissions') .insert({ user_id: userId, name: name.trim(), email: email.trim().toLowerCase(), subject: subject.trim(), message: message.trim(), category, user_agent: userAgent, ip_address_hash: ipHash, status: 'pending' }) .select() .single(); if (insertError) { edgeLogger.error('Failed to insert contact submission', { requestId, error: insertError.message }); return createErrorResponse(insertError, 500, corsHeaders); } edgeLogger.info('Contact submission created successfully', { requestId, submissionId: submission.id }); // Send notification email to admin (async, don't wait) const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS') || 'admin@thrillwiki.com'; const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com'; const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY'); if (forwardEmailKey) { // Send admin notification fetch('https://api.forwardemail.net/v1/emails', { method: 'POST', headers: { 'Authorization': `Basic ${btoa(forwardEmailKey + ':')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: fromEmail, to: adminEmail, subject: `New Contact Form Submission - ${category.charAt(0).toUpperCase() + category.slice(1)}`, text: `A new contact message has been received: From: ${name} (${email}) Category: ${category.charAt(0).toUpperCase() + category.slice(1)} Subject: ${subject} Message: ${message} Reference ID: ${submission.id} Submitted: ${new Date(submission.created_at).toLocaleString()} View in admin panel: https://thrillwiki.com/admin/contact`, }), }).catch(err => { edgeLogger.error('Failed to send admin notification', { requestId, error: err.message }); }); // Send user confirmation email fetch('https://api.forwardemail.net/v1/emails', { method: 'POST', headers: { 'Authorization': `Basic ${btoa(forwardEmailKey + ':')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: fromEmail, to: email, subject: "We've received your message - ThrillWiki Support", text: `Hi ${name}, Thank you for contacting ThrillWiki! We've received your message and will respond within 24-48 hours. Your Message Details: Category: ${category.charAt(0).toUpperCase() + category.slice(1)} Subject: ${subject} Reference ID: ${submission.id} Our support team will review your message and get back to you as soon as possible. Best regards, The ThrillWiki Team`, }), }).catch(err => { edgeLogger.error('Failed to send confirmation email', { requestId, error: err.message }); }); } const duration = Date.now() - startTime; edgeLogger.info('Contact submission processed successfully', { requestId, duration, submissionId: submission.id }); return new Response( JSON.stringify({ success: true, submissionId: submission.id, message: 'Your message has been received. We will respond within 24-48 hours.' }), { status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders }, } ); } catch (error) { const duration = Date.now() - startTime; edgeLogger.error('Contact submission failed', { requestId, duration, error: error instanceof Error ? error.message : String(error) }); return createErrorResponse(error, 500, corsHeaders); } }; serve(handler);