mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:51:13 -05:00
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' } }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user