import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", }; const RATE_LIMIT_MS = 5_000; const IP_RATE_LIMIT_MS = 60_000; const IP_MAX_REQUESTS = 15; const rateLimitMap = new Map(); const ipRequestLog = new Map(); Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const body = await req.json(); const { search_term, selected_address_id, selected_address_formatted, fiber_available, max_speed, contact_email, contact_phone } = body; // Validate required field if (!search_term || typeof search_term !== "string") { return new Response( JSON.stringify({ error: "Missing or invalid search_term" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Length validation if (search_term.length > 500) { return new Response( JSON.stringify({ error: "search_term too long" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } if (selected_address_id && (typeof selected_address_id !== "string" || selected_address_id.length > 100)) { return new Response( JSON.stringify({ error: "Invalid selected_address_id" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } if (selected_address_formatted && (typeof selected_address_formatted !== "string" || selected_address_formatted.length > 500)) { return new Response( JSON.stringify({ error: "Invalid selected_address_formatted" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Validate contact fields if provided const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^\+?[1-9]\d{1,14}$/; if (contact_email) { if (typeof contact_email !== "string" || contact_email.length > 254 || !emailRegex.test(contact_email)) { return new Response( JSON.stringify({ error: "Invalid contact_email" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } } if (contact_phone) { if (typeof contact_phone !== "string" || contact_phone.length > 20 || !phoneRegex.test(contact_phone)) { return new Response( JSON.stringify({ error: "Invalid contact_phone" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } } // Rate limiting const clientIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; const now = Date.now(); // Global per-IP limit const ipLog = ipRequestLog.get(clientIp) || []; const recentIpRequests = ipLog.filter((ts) => now - ts < IP_RATE_LIMIT_MS); if (recentIpRequests.length >= IP_MAX_REQUESTS) { return new Response( JSON.stringify({ error: "Too many requests" }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Per-IP+term cooldown const rateLimitKey = `${clientIp}:${search_term.substring(0, 50)}`; const lastRequest = rateLimitMap.get(rateLimitKey); if (lastRequest && now - lastRequest < RATE_LIMIT_MS) { return new Response( JSON.stringify({ error: "Too many requests" }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } rateLimitMap.set(rateLimitKey, now); recentIpRequests.push(now); ipRequestLog.set(clientIp, recentIpRequests); // Cleanup if (rateLimitMap.size > 1000) { for (const [key, ts] of rateLimitMap) { if (now - ts > RATE_LIMIT_MS) rateLimitMap.delete(key); } } if (ipRequestLog.size > 500) { for (const [key, timestamps] of ipRequestLog) { const valid = timestamps.filter((ts) => now - ts < IP_RATE_LIMIT_MS); if (valid.length === 0) ipRequestLog.delete(key); else ipRequestLog.set(key, valid); } } // Insert using service role const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! ); const { error } = await supabase.from("search_logs").insert({ search_term, selected_address_id: selected_address_id || null, selected_address_formatted: selected_address_formatted || null, fiber_available: typeof fiber_available === "boolean" ? fiber_available : null, max_speed: typeof max_speed === "number" ? max_speed : null, contact_email: contact_email || null, contact_phone: contact_phone || null, }); if (error) { console.error("Insert error:", error.message); return new Response( JSON.stringify({ error: "Failed to log search" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } return new Response( JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (err) { console.error("log-search error:", err); return new Response( JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } });