feat: Implement contact page and backend

This commit is contained in:
gpt-engineer-app[bot]
2025-10-28 17:01:57 +00:00
parent e2bd71cf24
commit e5f8ecefeb
11 changed files with 1510 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
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<Response> => {
// 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);