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
This commit is contained in:
pac7
2025-10-07 14:55:35 +00:00
parent f4020969d8
commit 88ed3207c4
5 changed files with 138 additions and 6 deletions

View File

@@ -148,16 +148,25 @@ export function PhotoUpload({
throw new Error('Direct upload to Cloudflare failed'); 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; const maxAttempts = 60;
let attempts = 0; let attempts = 0;
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://ydvtmnrszybqnbcqbdcy.supabase.co';
while (attempts < maxAttempts) { while (attempts < maxAttempts) {
try { 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)}`, { const response = await fetch(`${supabaseUrl}/functions/v1/upload-image?id=${encodeURIComponent(id)}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`, 'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });

View File

@@ -67,7 +67,7 @@ class NotificationService {
return { success: true }; return { success: true };
} catch (error: any) { } catch (error: any) {
console.error('Error creating Novu subscriber:', error); 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 }; return { success: true };
} catch (error: any) { } catch (error: any) {
console.error('Error updating Novu subscriber:', error); 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' };
} }
} }

View File

@@ -77,7 +77,7 @@ export async function createEntityVersion(params: {
console.error('Error creating entity version:', error); console.error('Error creating entity version:', error);
toast({ toast({
title: 'Version Creation Failed', title: 'Version Creation Failed',
description: error.message, description: error instanceof Error ? error.message : 'An unexpected error occurred',
variant: 'destructive', variant: 'destructive',
}); });
return null; return null;

View File

@@ -1,6 +1,9 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { Novu } from "npm:@novu/node@2.0.2"; 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 = { 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',
@@ -22,7 +25,24 @@ serve(async (req) => {
backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co', 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 // Validate required fields
if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') { 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 }); console.log('Creating Novu subscriber:', { subscriberId, email, firstName });
const subscriber = await novu.subscribers.identify(subscriberId, { const subscriber = await novu.subscribers.identify(subscriberId, {

View File

@@ -1,6 +1,9 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' 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 = { 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',