diff --git a/supabase/functions/cancel-email-change/index.ts b/supabase/functions/cancel-email-change/index.ts index 7aa4f125..3276f49d 100644 --- a/supabase/functions/cancel-email-change/index.ts +++ b/supabase/functions/cancel-email-change/index.ts @@ -1,23 +1,27 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; -import { decode as base64Decode } from "https://deno.land/std@0.190.0/encoding/base64.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; -// Helper function to decode JWT and extract user ID using secure base64 decoding +// Helper function to decode JWT and extract user ID +// Properly handles base64url encoding used by JWTs function decodeJWT(token: string): { sub: string } | null { try { const parts = token.split('.'); if (parts.length !== 3) return null; // JWT uses base64url encoding, convert to standard base64 - const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); - const padding = '='.repeat((4 - base64.length % 4) % 4); + let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); - // Decode using Deno's standard library instead of browser-specific atob - const decoded = new TextDecoder().decode(base64Decode(base64 + padding)); + // Add padding if needed + while (base64.length % 4) { + base64 += '='; + } + + // Decode and parse the payload + const decoded = atob(base64); const payload = JSON.parse(decoded); return payload; } catch (error) { diff --git a/supabase/functions/detect-location/index.ts b/supabase/functions/detect-location/index.ts index f123d1dd..66811457 100644 --- a/supabase/functions/detect-location/index.ts +++ b/supabase/functions/detect-location/index.ts @@ -25,17 +25,29 @@ serve(async (req) => { console.log('Detecting location for IP:', clientIP); - // Use a free IP geolocation service - const geoResponse = await fetch(`http://ip-api.com/json/${clientIP}?fields=status,country,countryCode`); - - if (!geoResponse.ok) { - throw new Error('Failed to fetch location data'); + // Use a free IP geolocation service with proper error handling + let geoResponse; + try { + geoResponse = await fetch(`http://ip-api.com/json/${clientIP}?fields=status,country,countryCode`); + } catch (fetchError) { + console.error('Network error fetching location data:', fetchError); + throw new Error('Network error: Unable to reach geolocation service'); } - const geoData = await geoResponse.json(); + if (!geoResponse.ok) { + throw new Error(`Geolocation service returned ${geoResponse.status}: ${geoResponse.statusText}`); + } + + let geoData; + try { + geoData = await geoResponse.json(); + } catch (parseError) { + console.error('Failed to parse geolocation response:', parseError); + throw new Error('Invalid response format from geolocation service'); + } if (geoData.status !== 'success') { - throw new Error('Invalid location data received'); + throw new Error(`Geolocation failed: ${geoData.message || 'Invalid location data'}`); } // Countries that primarily use imperial system diff --git a/supabase/functions/send-escalation-notification/index.ts b/supabase/functions/send-escalation-notification/index.ts index 1f3742cc..33bebbb3 100644 --- a/supabase/functions/send-escalation-notification/index.ts +++ b/supabase/functions/send-escalation-notification/index.ts @@ -213,7 +213,7 @@ serve(async (req) => { console.error('Error in send-escalation-notification:', error); return new Response( JSON.stringify({ - error: error.message, + error: error instanceof Error ? error.message : 'Unknown error occurred', details: 'Failed to send escalation notification' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index 12abc912..011dfdbb 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -111,17 +111,41 @@ serve(async (req) => { ) } - const deleteResponse = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, - { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, - }, - } - ) + let deleteResponse; + try { + deleteResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, + { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, + }, + } + ) + } catch (fetchError) { + console.error('Network error deleting image:', fetchError) + return new Response( + JSON.stringify({ error: 'Network error: Unable to reach Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } - const deleteResult = await deleteResponse.json() + let deleteResult; + try { + deleteResult = await deleteResponse.json() + } catch (parseError) { + console.error('Failed to parse Cloudflare delete response:', parseError) + return new Response( + JSON.stringify({ error: 'Invalid response from Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } if (!deleteResponse.ok) { console.error('Cloudflare delete error:', deleteResult) @@ -235,18 +259,42 @@ serve(async (req) => { formData.append('metadata', JSON.stringify(metadata)) } - const directUploadResponse = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, - }, - body: formData, - } - ) + let directUploadResponse; + try { + directUploadResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, + }, + body: formData, + } + ) + } catch (fetchError) { + console.error('Network error getting upload URL:', fetchError) + return new Response( + JSON.stringify({ error: 'Network error: Unable to reach Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } - const directUploadResult = await directUploadResponse.json() + let directUploadResult; + try { + directUploadResult = await directUploadResponse.json() + } catch (parseError) { + console.error('Failed to parse Cloudflare upload response:', parseError) + return new Response( + JSON.stringify({ error: 'Invalid response from Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } if (!directUploadResponse.ok) { console.error('Cloudflare direct upload error:', directUploadResult) @@ -321,16 +369,40 @@ serve(async (req) => { ) } - const imageResponse = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, - { - headers: { - 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, - }, - } - ) + let imageResponse; + try { + imageResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, + { + headers: { + 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, + }, + } + ) + } catch (fetchError) { + console.error('Network error fetching image status:', fetchError) + return new Response( + JSON.stringify({ error: 'Network error: Unable to reach Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } - const imageResult = await imageResponse.json() + let imageResult; + try { + imageResult = await imageResponse.json() + } catch (parseError) { + console.error('Failed to parse Cloudflare image status response:', parseError) + return new Response( + JSON.stringify({ error: 'Invalid response from Cloudflare Images API' }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } if (!imageResponse.ok) { console.error('Cloudflare image status error:', imageResult)