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