feat: Implement comprehensive request tracking and state management

This commit is contained in:
gpt-engineer-app[bot]
2025-10-21 12:51:44 +00:00
parent 12433e49e3
commit 74860c6774
8 changed files with 400 additions and 77 deletions

View File

@@ -0,0 +1,114 @@
/**
* Edge Function Request Tracking Wrapper
*
* Wraps Supabase function invocations with request tracking for debugging and monitoring.
* Provides correlation IDs for tracing requests across the system.
*/
import { supabase } from '@/integrations/supabase/client';
import { trackRequest } from './requestTracking';
import { getErrorMessage } from './errorHandler';
/**
* Invoke a Supabase edge function with request tracking
*
* @param functionName - Name of the edge function to invoke
* @param payload - Request payload
* @param userId - User ID for tracking (optional)
* @param parentRequestId - Parent request ID for chaining (optional)
* @param traceId - Trace ID for distributed tracing (optional)
* @returns Response data with requestId
*/
export async function invokeWithTracking<T = any>(
functionName: string,
payload: Record<string, unknown> = {},
userId?: string,
parentRequestId?: string,
traceId?: string
): Promise<{ data: T | null; error: any; requestId: string; duration: number }> {
try {
const { result, requestId, duration } = await trackRequest(
{
endpoint: `/functions/${functionName}`,
method: 'POST',
userId,
parentRequestId,
traceId,
},
async (context) => {
// Include client request ID in payload for correlation
const { data, error } = await supabase.functions.invoke<T>(functionName, {
body: { ...payload, clientRequestId: context.requestId },
});
if (error) throw error;
return data;
}
);
return { data: result, error: null, requestId, duration };
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
// On error, we don't have tracking info, so create basic response
return {
data: null,
error: { message: errorMessage },
requestId: 'unknown',
duration: 0,
};
}
}
/**
* Invoke multiple edge functions in parallel with batch tracking
*
* Uses a shared trace ID to correlate all operations.
*
* @param operations - Array of function invocation configurations
* @param userId - User ID for tracking
* @returns Array of results with their request IDs
*/
export async function invokeBatchWithTracking<T = any>(
operations: Array<{
functionName: string;
payload: Record<string, unknown>;
}>,
userId?: string
): Promise<
Array<{
functionName: string;
data: T | null;
error: any;
requestId: string;
duration: number;
}>
> {
const traceId = crypto.randomUUID();
const results = await Promise.allSettled(
operations.map(async (op) => {
const result = await invokeWithTracking<T>(
op.functionName,
op.payload,
userId,
undefined,
traceId
);
return { functionName: op.functionName, ...result };
})
);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
functionName: operations[index].functionName,
data: null,
error: { message: result.reason?.message || 'Unknown error' },
requestId: 'unknown',
duration: 0,
};
}
});
}

View File

@@ -1,5 +1,7 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { invokeWithTracking } from './edgeFunctionTracking';
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader'; import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
import { logger } from './logger';
export interface CloudflareUploadResponse { export interface CloudflareUploadResponse {
result: { result: {
@@ -25,16 +27,28 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (image.isLocal && image.file) { if (image.isLocal && image.file) {
const fileName = image.file.name; const fileName = image.file.name;
// Step 1: Get upload URL from our Supabase Edge Function // Step 1: Get upload URL from our Supabase Edge Function (with tracking)
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', { const { data: uploadUrlData, error: urlError, requestId } = await invokeWithTracking(
body: { action: 'get-upload-url' } 'upload-image',
}); { action: 'get-upload-url' }
);
if (urlError || !uploadUrlData?.uploadURL) { if (urlError || !uploadUrlData?.uploadURL) {
console.error(`imageUploadHelper.uploadPendingImages: Failed to get upload URL for "${fileName}":`, urlError); logger.error('Failed to get upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
error: urlError?.message || 'Unknown error',
});
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`); throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
} }
logger.info('Got upload URL', {
action: 'upload_pending_images',
fileName,
requestId,
});
// Step 2: Upload file directly to Cloudflare // Step 2: Upload file directly to Cloudflare
const formData = new FormData(); const formData = new FormData();
formData.append('file', image.file); formData.append('file', image.file);
@@ -46,17 +60,31 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
const errorText = await uploadResponse.text(); const errorText = await uploadResponse.text();
console.error(`imageUploadHelper.uploadPendingImages: Upload failed for "${fileName}" (status ${uploadResponse.status}):`, errorText); logger.error('Cloudflare upload failed', {
action: 'upload_pending_images',
fileName,
status: uploadResponse.status,
error: errorText,
});
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`); throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
} }
const result: CloudflareUploadResponse = await uploadResponse.json(); const result: CloudflareUploadResponse = await uploadResponse.json();
if (!result.success || !result.result) { if (!result.success || !result.result) {
console.error(`imageUploadHelper.uploadPendingImages: Cloudflare upload unsuccessful for "${fileName}"`); logger.error('Cloudflare upload unsuccessful', {
action: 'upload_pending_images',
fileName,
});
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`); throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
} }
logger.info('Image uploaded successfully', {
action: 'upload_pending_images',
fileName,
imageId: result.result.id,
});
// Clean up object URL // Clean up object URL
URL.revokeObjectURL(image.url); URL.revokeObjectURL(image.url);
@@ -106,13 +134,18 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// If any uploads failed, clean up ONLY newly uploaded images and throw error // If any uploads failed, clean up ONLY newly uploaded images and throw error
if (errors.length > 0) { if (errors.length > 0) {
if (newlyUploadedImageIds.length > 0) { if (newlyUploadedImageIds.length > 0) {
console.error(`imageUploadHelper.uploadPendingImages: Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`); logger.error('Some uploads failed, cleaning up', {
action: 'upload_pending_images',
newlyUploadedCount: newlyUploadedImageIds.length,
failureCount: errors.length,
});
// Attempt cleanup in parallel with detailed error tracking // Attempt cleanup in parallel with detailed error tracking
const cleanupResults = await Promise.allSettled( const cleanupResults = await Promise.allSettled(
newlyUploadedImageIds.map(imageId => newlyUploadedImageIds.map(imageId =>
supabase.functions.invoke('upload-image', { invokeWithTracking('upload-image', {
body: { action: 'delete', imageId } action: 'delete',
imageId,
}) })
) )
); );
@@ -120,13 +153,17 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// Track cleanup failures for better debugging // Track cleanup failures for better debugging
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected'); const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
if (cleanupFailures.length > 0) { if (cleanupFailures.length > 0) {
console.error( logger.error('Failed to cleanup images', {
`imageUploadHelper.uploadPendingImages: Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images.`, action: 'upload_pending_images_cleanup',
'These images may remain orphaned in Cloudflare:', cleanupFailures: cleanupFailures.length,
newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected') totalCleanup: newlyUploadedImageIds.length,
); orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected'),
});
} else { } else {
console.log(`imageUploadHelper.uploadPendingImages: Successfully cleaned up ${newlyUploadedImageIds.length} images.`); logger.info('Successfully cleaned up images', {
action: 'upload_pending_images_cleanup',
cleanedCount: newlyUploadedImageIds.length,
});
} }
} }

View File

@@ -11,6 +11,7 @@ import { createTableQuery } from '@/lib/supabaseHelpers';
import type { ModerationItem } from '@/types/moderation'; import type { ModerationItem } from '@/types/moderation';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage } from '@/lib/errorHandler';
import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking';
/** /**
* Type-safe update data for review moderation * Type-safe update data for review moderation
@@ -184,20 +185,32 @@ export async function approveSubmissionItems(
itemIds: string[] itemIds: string[]
): Promise<ModerationActionResult> { ): Promise<ModerationActionResult> {
try { try {
const { error: approvalError } = await supabase.functions.invoke( const { data: approvalData, error: approvalError, requestId } = await invokeWithTracking(
'process-selective-approval', 'process-selective-approval',
{ {
body: { itemIds,
itemIds, submissionId,
submissionId,
},
} }
); );
if (approvalError) { if (approvalError) {
logger.error('Submission items approval failed via edge function', {
action: 'approve_submission_items',
submissionId,
itemCount: itemIds.length,
requestId,
error: approvalError.message,
});
throw new Error(`Failed to process submission items: ${approvalError.message}`); throw new Error(`Failed to process submission items: ${approvalError.message}`);
} }
logger.info('Submission items approved successfully', {
action: 'approve_submission_items',
submissionId,
itemCount: itemIds.length,
requestId,
});
return { return {
success: true, success: true,
message: `Successfully processed ${itemIds.length} item(s)`, message: `Successfully processed ${itemIds.length} item(s)`,
@@ -478,24 +491,28 @@ export async function deleteSubmission(
// Delete photos from Cloudflare // Delete photos from Cloudflare
if (validImageIds.length > 0) { if (validImageIds.length > 0) {
const deletePromises = validImageIds.map(async imageId => { const deleteResults = await invokeBatchWithTracking(
try { validImageIds.map(imageId => ({
await supabase.functions.invoke('upload-image', { functionName: 'upload-image',
method: 'DELETE', payload: { action: 'delete', imageId },
body: { imageId }, })),
}); undefined
} catch (photoDeleteError: unknown) { );
const errorMessage = getErrorMessage(photoDeleteError);
logger.error('Photo deletion failed', {
action: 'delete_submission_photo',
imageId,
error: errorMessage
});
}
});
await Promise.allSettled(deletePromises); // Count successful deletions
deletedPhotoCount = validImageIds.length; const successfulDeletions = deleteResults.filter(r => !r.error);
deletedPhotoCount = successfulDeletions.length;
// Log any failures
const failedDeletions = deleteResults.filter(r => r.error);
if (failedDeletions.length > 0) {
logger.error('Some photo deletions failed', {
action: 'delete_submission_photos',
failureCount: failedDeletions.length,
totalAttempted: validImageIds.length,
failedRequestIds: failedDeletions.map(r => r.requestId),
});
}
} }
} }
} }

View File

@@ -0,0 +1,108 @@
/**
* Moderation Lock Monitor
*
* Monitors lock expiry and provides automatic renewal prompts for moderators.
* Prevents loss of work due to expired locks.
*/
import { useEffect } from 'react';
import type { ModerationState } from '../moderationStateMachine';
import type { ModerationAction } from '../moderationStateMachine';
import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine';
import { toast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client';
import { logger } from '../logger';
/**
* Hook to monitor lock status and warn about expiry
*
* @param state - Current moderation state
* @param dispatch - State machine dispatch function
* @param itemId - ID of the locked item (optional, for manual extension)
*/
export function useLockMonitor(
state: ModerationState,
dispatch: React.Dispatch<ModerationAction>,
itemId?: string
) {
useEffect(() => {
if (!hasActiveLock(state)) {
return;
}
const checkInterval = setInterval(() => {
if (needsLockRenewal(state)) {
logger.warn('Lock expiring soon', {
action: 'lock_expiry_warning',
itemId,
lockExpires: state.status === 'locked' || state.status === 'reviewing'
? state.lockExpires
: undefined,
});
// Dispatch lock expiry warning
dispatch({ type: 'LOCK_EXPIRED' });
// Show toast with extension option
toast({
title: 'Lock Expiring Soon',
description: 'Your lock on this submission will expire in less than 2 minutes. Click to extend.',
variant: 'default',
});
}
}, 30000); // Check every 30 seconds
return () => clearInterval(checkInterval);
}, [state, dispatch, itemId]);
}
/**
* Extend the lock on a submission
*
* @param submissionId - Submission ID
* @param dispatch - State machine dispatch function
*/
async function handleExtendLock(
submissionId: string,
dispatch: React.Dispatch<ModerationAction>
) {
try {
// Call Supabase to extend lock (assumes 15 minute extension)
const { error } = await supabase
.from('content_submissions')
.update({
locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
})
.eq('id', submissionId);
if (error) throw error;
// Update state machine with new lock time
dispatch({
type: 'LOCK_ACQUIRED',
payload: { lockExpires: new Date(Date.now() + 15 * 60 * 1000).toISOString() },
});
toast({
title: 'Lock Extended',
description: 'You have 15 more minutes to complete your review.',
});
logger.info('Lock extended successfully', {
action: 'lock_extended',
submissionId,
});
} catch (error) {
logger.error('Failed to extend lock', {
action: 'extend_lock_error',
submissionId,
error: error instanceof Error ? error.message : String(error),
});
toast({
title: 'Extension Failed',
description: 'Could not extend lock. Please save your work and re-claim the item.',
variant: 'destructive',
});
}
}

View File

@@ -1,6 +1,6 @@
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';
import { edgeLogger } from '../_shared/logger.ts'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = { const corsHeaders = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
@@ -8,6 +8,8 @@ const corsHeaders = {
}; };
serve(async (req) => { serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders }); return new Response(null, { headers: corsHeaders });
} }
@@ -35,7 +37,7 @@ serve(async (req) => {
throw new Error('Unauthorized'); throw new Error('Unauthorized');
} }
edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id }); edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id, requestId: tracking.requestId });
// Find pending deletion request // Find pending deletion request
const { data: deletionRequest, error: requestError } = await supabaseClient const { data: deletionRequest, error: requestError } = await supabaseClient
@@ -101,29 +103,34 @@ serve(async (req) => {
`, `,
}), }),
}); });
edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id }); edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId });
} catch (emailError) { } catch (emailError) {
edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id }); edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId });
} }
} }
const duration = endRequest(tracking);
edgeLogger.info('Deletion cancelled successfully', { action: 'cancel_deletion_success', userId: user.id, requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: true, success: true,
message: 'Account deletion cancelled successfully', message: 'Account deletion cancelled successfully',
requestId: tracking.requestId
}), }),
{ {
status: 200, status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
} }
); );
} catch (error) { } catch (error) {
edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error) }); const duration = endRequest(tracking);
edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error), requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ error: error.message }), JSON.stringify({ error: error.message, requestId: tracking.requestId }),
{ {
status: 400, status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
} }
); );
} }

View File

@@ -1,15 +1,18 @@
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/api@1.6.0"; import { Novu } from "npm:@novu/api@1.6.0";
// 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',
}; };
// Simple request tracking
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
serve(async (req) => { serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders }); return new Response(null, { headers: corsHeaders });
} }
@@ -197,28 +200,32 @@ serve(async (req) => {
data, data,
}); });
console.log('Subscriber created successfully:', subscriber.data); const duration = endRequest(tracking);
console.log('Subscriber created successfully:', subscriber.data, { requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: true, success: true,
subscriberId: subscriber.data._id, subscriberId: subscriber.data._id,
requestId: tracking.requestId
}), }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 200, status: 200,
} }
); );
} catch (error: any) { } catch (error: any) {
console.error('Error creating Novu subscriber:', error); const duration = endRequest(tracking);
console.error('Error creating Novu subscriber:', error, { requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: false, success: false,
error: error.message, error: error.message,
requestId: tracking.requestId
}), }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 500, status: 500,
} }
); );

View File

@@ -1,5 +1,6 @@
import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import "jsr:@supabase/functions-js/edge-runtime.d.ts";
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 { startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = { const corsHeaders = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
@@ -73,6 +74,8 @@ async function ensureUniqueUsername(
} }
Deno.serve(async (req) => { Deno.serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders }); return new Response(null, { headers: corsHeaders });
} }
@@ -188,9 +191,10 @@ Deno.serve(async (req) => {
.single(); .single();
if (profile?.avatar_image_id) { if (profile?.avatar_image_id) {
console.log('[OAuth Profile] Avatar already exists, skipping'); const duration = endRequest(tracking);
return new Response(JSON.stringify({ success: true, message: 'Avatar already exists' }), { console.log('[OAuth Profile] Avatar already exists, skipping', { requestId: tracking.requestId, duration });
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, return new Response(JSON.stringify({ success: true, message: 'Avatar already exists', requestId: tracking.requestId }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
}); });
} }
@@ -326,22 +330,27 @@ Deno.serve(async (req) => {
}); });
} }
console.log('[OAuth Profile] Profile updated successfully'); console.log('[OAuth Profile] Profile updated successfully', { requestId: tracking.requestId });
} }
const duration = endRequest(tracking);
console.log('[OAuth Profile] Processing complete', { requestId: tracking.requestId, duration });
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: true, success: true,
avatar_uploaded: !!cloudflareImageId, avatar_uploaded: !!cloudflareImageId,
profile_updated: Object.keys(updateData).length > 0, profile_updated: Object.keys(updateData).length > 0,
requestId: tracking.requestId
}), { }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
}); });
} catch (error) { } catch (error) {
console.error('[OAuth Profile] Error:', error); const duration = endRequest(tracking);
return new Response(JSON.stringify({ error: error.message }), { console.error('[OAuth Profile] Error:', error, { requestId: tracking.requestId, duration });
return new Response(JSON.stringify({ error: error.message, requestId: tracking.requestId }), {
status: 500, status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
}); });
} }
}); });

View File

@@ -1,6 +1,6 @@
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'
import { edgeLogger } from '../_shared/logger.ts' import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'
// Environment-aware CORS configuration // Environment-aware CORS configuration
const getAllowedOrigin = (requestOrigin: string | null): string | null => { const getAllowedOrigin = (requestOrigin: string | null): string | null => {
@@ -70,12 +70,13 @@ const createAuthenticatedSupabaseClient = (authHeader: string) => {
} }
serve(async (req) => { serve(async (req) => {
const tracking = startRequest();
const requestOrigin = req.headers.get('origin'); const requestOrigin = req.headers.get('origin');
const allowedOrigin = getAllowedOrigin(requestOrigin); const allowedOrigin = getAllowedOrigin(requestOrigin);
// Check if this is a CORS request with a disallowed origin // Check if this is a CORS request with a disallowed origin
if (requestOrigin && !allowedOrigin) { if (requestOrigin && !allowedOrigin) {
edgeLogger.warn('CORS request rejected', { action: 'cors_validation', origin: requestOrigin }); edgeLogger.warn('CORS request rejected', { action: 'cors_validation', origin: requestOrigin, requestId: tracking.requestId });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Origin not allowed', error: 'Origin not allowed',
@@ -160,19 +161,26 @@ serve(async (req) => {
} }
if (profile.banned) { if (profile.banned) {
const duration = endRequest(tracking);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Account suspended', error: 'Account suspended',
message: 'Account suspended. Contact support for assistance.' message: 'Account suspended. Contact support for assistance.',
requestId: tracking.requestId
}), }),
{ {
status: 403, status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
} }
) )
} }
// Delete image from Cloudflare // Delete image from Cloudflare
edgeLogger.info('Deleting image', { action: 'delete_image', requestId: tracking.requestId });
let requestBody; let requestBody;
try { try {
requestBody = await req.json(); requestBody = await req.json();
@@ -280,10 +288,12 @@ serve(async (req) => {
) )
} }
const duration = endRequest(tracking);
edgeLogger.info('Image deleted successfully', { action: 'delete_image', requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ success: true, deleted: true }), JSON.stringify({ success: true, deleted: true, requestId: tracking.requestId }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} }
@@ -344,19 +354,22 @@ serve(async (req) => {
} }
if (profile.banned) { if (profile.banned) {
const duration = endRequest(tracking);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Account suspended', error: 'Account suspended',
message: 'Account suspended. Contact support for assistance.' message: 'Account suspended. Contact support for assistance.',
requestId: tracking.requestId
}), }),
{ {
status: 403, status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} }
// Request a direct upload URL from Cloudflare // Request a direct upload URL from Cloudflare
edgeLogger.info('Requesting upload URL', { action: 'request_upload_url', requestId: tracking.requestId });
let requestBody; let requestBody;
try { try {
requestBody = await req.json(); requestBody = await req.json();
@@ -448,14 +461,17 @@ serve(async (req) => {
} }
// Return the upload URL and image ID to the client // Return the upload URL and image ID to the client
const duration = endRequest(tracking);
edgeLogger.info('Upload URL created', { action: 'upload_url_success', requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: true, success: true,
uploadURL: directUploadResult.result.uploadURL, uploadURL: directUploadResult.result.uploadURL,
id: directUploadResult.result.id, id: directUploadResult.result.id,
requestId: tracking.requestId
}), }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} }
@@ -569,10 +585,13 @@ serve(async (req) => {
// Return the image details with convenient URLs // Return the image details with convenient URLs
const result = imageResult.result const result = imageResult.result
const duration = endRequest(tracking);
// Construct proper imagedelivery.net URLs using account hash and image ID // Construct proper imagedelivery.net URLs using account hash and image ID
const baseUrl = `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${result.id}` const baseUrl = `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${result.id}`
edgeLogger.info('Image status retrieved', { action: 'get_image_status', requestId: tracking.requestId, duration });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: true, success: true,
@@ -587,36 +606,41 @@ serve(async (req) => {
medium: `${baseUrl}/medium`, medium: `${baseUrl}/medium`,
large: `${baseUrl}/large`, large: `${baseUrl}/large`,
avatar: `${baseUrl}/avatar`, avatar: `${baseUrl}/avatar`,
} : null } : null,
requestId: tracking.requestId
}), }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} }
const duration = endRequest(tracking);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Method not allowed', error: 'Method not allowed',
message: 'HTTP method not supported for this endpoint' message: 'HTTP method not supported for this endpoint',
requestId: tracking.requestId
}), }),
{ {
status: 405, status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} catch (error: unknown) { } catch (error: unknown) {
const duration = endRequest(tracking);
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
console.error('[Upload] Error:', { error: errorMessage }); edgeLogger.error('Upload function error', { action: 'upload_error', requestId: tracking.requestId, duration, error: errorMessage });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Internal server error', error: 'Internal server error',
message: errorMessage message: errorMessage,
requestId: tracking.requestId
}), }),
{ {
status: 500, status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }
} }
) )
} }