Improve security and error handling in backend functions

Update Supabase functions for cancel-email-change, detect-location, send-escalation-notification, and upload-image to enhance security and implement robust error handling.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: a46bc7a0-bbf8-43ab-97c0-a58c66c2e365
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-10-08 12:06:35 +00:00
parent ccea99fecd
commit 0b57cba16f
4 changed files with 132 additions and 44 deletions

View File

@@ -1,23 +1,27 @@
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; 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 = { const corsHeaders = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', '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 { function decodeJWT(token: string): { sub: string } | null {
try { try {
const parts = token.split('.'); const parts = token.split('.');
if (parts.length !== 3) return null; if (parts.length !== 3) return null;
// JWT uses base64url encoding, convert to standard base64 // JWT uses base64url encoding, convert to standard base64
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padding = '='.repeat((4 - base64.length % 4) % 4);
// Decode using Deno's standard library instead of browser-specific atob // Add padding if needed
const decoded = new TextDecoder().decode(base64Decode(base64 + padding)); while (base64.length % 4) {
base64 += '=';
}
// Decode and parse the payload
const decoded = atob(base64);
const payload = JSON.parse(decoded); const payload = JSON.parse(decoded);
return payload; return payload;
} catch (error) { } catch (error) {

View File

@@ -25,17 +25,29 @@ serve(async (req) => {
console.log('Detecting location for IP:', clientIP); console.log('Detecting location for IP:', clientIP);
// Use a free IP geolocation service // Use a free IP geolocation service with proper error handling
const geoResponse = await fetch(`http://ip-api.com/json/${clientIP}?fields=status,country,countryCode`); let geoResponse;
try {
if (!geoResponse.ok) { geoResponse = await fetch(`http://ip-api.com/json/${clientIP}?fields=status,country,countryCode`);
throw new Error('Failed to fetch location data'); } 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') { 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 // Countries that primarily use imperial system

View File

@@ -213,7 +213,7 @@ serve(async (req) => {
console.error('Error in send-escalation-notification:', error); console.error('Error in send-escalation-notification:', error);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: error.message, error: error instanceof Error ? error.message : 'Unknown error occurred',
details: 'Failed to send escalation notification' details: 'Failed to send escalation notification'
}), }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }

View File

@@ -111,7 +111,9 @@ serve(async (req) => {
) )
} }
const deleteResponse = await fetch( let deleteResponse;
try {
deleteResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`,
{ {
method: 'DELETE', method: 'DELETE',
@@ -120,8 +122,30 @@ serve(async (req) => {
}, },
} }
) )
} 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) { if (!deleteResponse.ok) {
console.error('Cloudflare delete error:', deleteResult) console.error('Cloudflare delete error:', deleteResult)
@@ -235,7 +259,9 @@ serve(async (req) => {
formData.append('metadata', JSON.stringify(metadata)) formData.append('metadata', JSON.stringify(metadata))
} }
const directUploadResponse = await fetch( let directUploadResponse;
try {
directUploadResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`, `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`,
{ {
method: 'POST', method: 'POST',
@@ -245,8 +271,30 @@ serve(async (req) => {
body: formData, 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) { if (!directUploadResponse.ok) {
console.error('Cloudflare direct upload error:', directUploadResult) console.error('Cloudflare direct upload error:', directUploadResult)
@@ -321,7 +369,9 @@ serve(async (req) => {
) )
} }
const imageResponse = await fetch( let imageResponse;
try {
imageResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`,
{ {
headers: { headers: {
@@ -329,8 +379,30 @@ serve(async (req) => {
}, },
} }
) )
} 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) { if (!imageResponse.ok) {
console.error('Cloudflare image status error:', imageResult) console.error('Cloudflare image status error:', imageResult)