From 88ed3207c4dfa9222d2fe99bd8a000c946fd160f Mon Sep 17 00:00:00 2001 From: pac7 <47831526-pac7@users.noreply.replit.com> Date: Tue, 7 Oct 2025 14:55:35 +0000 Subject: [PATCH] Improve error handling and authentication for uploads and notifications Refactor PhotoUpload component to fetch session token before polling, enhance error handling in NotificationService and versioningHelpers with `instanceof Error` checks, and add comprehensive validation for request body fields in the create-novu-subscriber Supabase function. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 8d708ff6-09f1-4b67-8edc-de3fcb2349b3 Replit-Commit-Checkpoint-Type: intermediate_checkpoint --- src/components/upload/PhotoUpload.tsx | 13 +- src/lib/notificationService.ts | 4 +- src/lib/versioningHelpers.ts | 2 +- .../functions/create-novu-subscriber/index.ts | 122 +++++++++++++++++- supabase/functions/upload-image/index.ts | 3 + 5 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/components/upload/PhotoUpload.tsx b/src/components/upload/PhotoUpload.tsx index bee0f36c..c0dc44a3 100644 --- a/src/components/upload/PhotoUpload.tsx +++ b/src/components/upload/PhotoUpload.tsx @@ -148,16 +148,25 @@ export function PhotoUpload({ throw new Error('Direct upload to Cloudflare failed'); } + // Fetch session token once before polling + const sessionData = await supabase.auth.getSession(); + const accessToken = sessionData.data.session?.access_token; + + if (!accessToken) { + revokeObjectUrl(previewUrl); + throw new Error('Authentication required for upload'); + } + const maxAttempts = 60; let attempts = 0; + const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://ydvtmnrszybqnbcqbdcy.supabase.co'; while (attempts < maxAttempts) { try { - const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://ydvtmnrszybqnbcqbdcy.supabase.co'; const response = await fetch(`${supabaseUrl}/functions/v1/upload-image?id=${encodeURIComponent(id)}`, { method: 'GET', headers: { - 'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`, + 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); diff --git a/src/lib/notificationService.ts b/src/lib/notificationService.ts index 6cdbeebc..620991f4 100644 --- a/src/lib/notificationService.ts +++ b/src/lib/notificationService.ts @@ -67,7 +67,7 @@ class NotificationService { return { success: true }; } catch (error: any) { console.error('Error creating Novu subscriber:', error); - return { success: false, error: error.message }; + return { success: false, error: error instanceof Error ? error.message : 'An unexpected error occurred' }; } } @@ -91,7 +91,7 @@ class NotificationService { return { success: true }; } catch (error: any) { console.error('Error updating Novu subscriber:', error); - return { success: false, error: error.message }; + return { success: false, error: error instanceof Error ? error.message : 'An unexpected error occurred' }; } } diff --git a/src/lib/versioningHelpers.ts b/src/lib/versioningHelpers.ts index a01a47cc..102a1678 100644 --- a/src/lib/versioningHelpers.ts +++ b/src/lib/versioningHelpers.ts @@ -77,7 +77,7 @@ export async function createEntityVersion(params: { console.error('Error creating entity version:', error); toast({ title: 'Version Creation Failed', - description: error.message, + description: error instanceof Error ? error.message : 'An unexpected error occurred', variant: 'destructive', }); return null; diff --git a/supabase/functions/create-novu-subscriber/index.ts b/supabase/functions/create-novu-subscriber/index.ts index aaf9822d..ec054248 100644 --- a/supabase/functions/create-novu-subscriber/index.ts +++ b/supabase/functions/create-novu-subscriber/index.ts @@ -1,6 +1,9 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/node@2.0.2"; +// TODO: In production, restrict CORS to specific domains +// For now, allowing all origins for development flexibility +// Example production config: 'Access-Control-Allow-Origin': 'https://yourdomain.com' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', @@ -22,7 +25,24 @@ serve(async (req) => { backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co', }); - const { subscriberId, email, firstName, lastName, phone, avatar, data } = await req.json(); + // Parse and validate request body + let requestBody; + try { + requestBody = await req.json(); + } catch (parseError) { + return new Response( + JSON.stringify({ + success: false, + error: 'Invalid JSON in request body', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + const { subscriberId, email, firstName, lastName, phone, avatar, data } = requestBody; // Validate required fields if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') { @@ -66,6 +86,106 @@ serve(async (req) => { ); } + // Validate optional fields if provided + if (firstName !== undefined && firstName !== null && (typeof firstName !== 'string' || firstName.length > 100)) { + return new Response( + JSON.stringify({ + success: false, + error: 'firstName must be a string with maximum 100 characters', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + if (lastName !== undefined && lastName !== null && (typeof lastName !== 'string' || lastName.length > 100)) { + return new Response( + JSON.stringify({ + success: false, + error: 'lastName must be a string with maximum 100 characters', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + if (phone !== undefined && phone !== null) { + if (typeof phone !== 'string') { + return new Response( + JSON.stringify({ + success: false, + error: 'phone must be a string', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + // Validate phone format (basic validation for international numbers) + const phoneRegex = /^\+?[1-9]\d{1,14}$/; + if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) { + return new Response( + JSON.stringify({ + success: false, + error: 'Invalid phone format. Please provide a valid international phone number', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + } + + if (avatar !== undefined && avatar !== null && (typeof avatar !== 'string' || !avatar.startsWith('http'))) { + return new Response( + JSON.stringify({ + success: false, + error: 'avatar must be a valid URL', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + // Validate data field if provided + if (data !== undefined && data !== null) { + if (typeof data !== 'object' || Array.isArray(data)) { + return new Response( + JSON.stringify({ + success: false, + error: 'data must be a valid object', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + // Check data size (limit to 10KB serialized) + const dataSize = JSON.stringify(data).length; + if (dataSize > 10240) { + return new Response( + JSON.stringify({ + success: false, + error: 'data field is too large (maximum 10KB)', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + } + console.log('Creating Novu subscriber:', { subscriberId, email, firstName }); const subscriber = await novu.subscribers.identify(subscriberId, { diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index cd3f176e..3c9ca169 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -1,6 +1,9 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +// TODO: In production, restrict CORS to specific domains +// For now, allowing all origins for development flexibility +// Example production config: 'Access-Control-Allow-Origin': 'https://yourdomain.com' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',