From b8787ee6de507f3f2589df9c1bdc10b67726fe99 Mon Sep 17 00:00:00 2001 From: pac7 <47831526-pac7@users.noreply.replit.com> Date: Tue, 7 Oct 2025 20:12:39 +0000 Subject: [PATCH] Improve security by verifying user authentication and authorization Update the 'process-selective-approval' Supabase function to enforce authentication and authorization checks before processing requests. Also, modify the 'upload-image' function to prevent banned users from uploading images. Additionally, enable future React Router v7 features for enhanced navigation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 6d6e48da-5b1b-47f9-a65c-9fa4a352936a Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/6d6e48da-5b1b-47f9-a65c-9fa4a352936a/u05utRo --- .replit | 4 + src/App.tsx | 7 +- src/components/moderation/ModerationQueue.tsx | 2 - .../moderation/SubmissionReviewManager.tsx | 2 - .../process-selective-approval/index.ts | 73 ++++++++++++++----- supabase/functions/upload-image/index.ts | 56 ++++++++++++++ 6 files changed, 120 insertions(+), 24 deletions(-) diff --git a/.replit b/.replit index fc81a45d..4a9bbd83 100644 --- a/.replit +++ b/.replit @@ -33,3 +33,7 @@ outputType = "webview" [[ports]] localPort = 5000 externalPort = 80 + +[[ports]] +localPort = 42081 +externalPort = 3000 diff --git a/src/App.tsx b/src/App.tsx index 10568640..043419b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,12 @@ function AppContent() { return ( - +
diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index b277fb9f..cc1c753a 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -544,7 +544,6 @@ export const ModerationQueue = forwardRef((props, ref) => { { body: { itemIds: failedItems.map(i => i.id), - userId: user?.id, submissionId: item.id } } @@ -813,7 +812,6 @@ export const ModerationQueue = forwardRef((props, ref) => { { body: { itemIds: submissionItems.map(i => i.id), - userId: user?.id, submissionId: item.id } } diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index a53d8ab7..15fac0ca 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -156,7 +156,6 @@ export function SubmissionReviewManager({ const { data, error } = await supabase.functions.invoke('process-selective-approval', { body: { itemIds: Array.from(selectedItemIds), - userId: user.id, submissionId } }); @@ -330,7 +329,6 @@ export function SubmissionReviewManager({ const { data, error } = await supabase.functions.invoke('process-selective-approval', { body: { itemIds: [itemId], - userId: user.id, submissionId } }); diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index b24ce2e2..90bcc120 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -8,7 +8,6 @@ const corsHeaders = { interface ApprovalRequest { itemIds: string[]; - userId: string; submissionId: string; } @@ -49,12 +48,63 @@ serve(async (req) => { } try { + // Verify authentication first with a client that respects RLS + const authHeader = req.headers.get('Authorization'); + if (!authHeader) { + return new Response( + JSON.stringify({ error: 'Authentication required. Please log in.' }), + { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + // Create Supabase client with user's auth token to verify authentication + const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? ''; + const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? ''; + const supabaseAuth = createClient(supabaseUrl, supabaseAnonKey, { + global: { headers: { Authorization: authHeader } } + }); + + // Verify JWT and get authenticated user + const { data: { user }, error: authError } = await supabaseAuth.auth.getUser(); + if (authError || !user) { + console.error('Auth verification failed:', authError); + return new Response( + JSON.stringify({ error: 'Invalid authentication token.' }), + { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + const authenticatedUserId = user.id; + + // Create service role client for privileged operations (including role check) const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ); - const { itemIds, userId, submissionId }: ApprovalRequest = await req.json(); + // Check if user has moderator permissions using service role to bypass RLS + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('role') + .eq('user_id', authenticatedUserId) + .single(); + + if (profileError || !profile) { + console.error('Failed to fetch profile:', profileError); + return new Response( + JSON.stringify({ error: 'User profile not found.' }), + { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + if (profile.role !== 'moderator' && profile.role !== 'admin') { + return new Response( + JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }), + { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + const { itemIds, submissionId }: ApprovalRequest = await req.json(); // UUID validation regex const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -74,21 +124,6 @@ serve(async (req) => { ); } - // Validate userId - if (!userId || typeof userId !== 'string' || userId.trim() === '') { - return new Response( - JSON.stringify({ error: 'userId is required and must be a non-empty string' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ); - } - - if (!uuidRegex.test(userId)) { - return new Response( - JSON.stringify({ error: 'userId must be a valid UUID format' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ); - } - // Validate submissionId if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') { return new Response( @@ -104,7 +139,7 @@ serve(async (req) => { ); } - console.log('Processing selective approval:', { itemIds, userId, submissionId }); + console.log('Processing selective approval:', { itemIds, userId: authenticatedUserId, submissionId }); // Fetch all items for the submission const { data: items, error: fetchError } = await supabase @@ -241,7 +276,7 @@ serve(async (req) => { .from('content_submissions') .update({ status: allApproved ? 'approved' : 'partially_approved', - reviewer_id: userId, + reviewer_id: authenticatedUserId, reviewed_at: new Date().toISOString() }) .eq('id', submissionId); diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index 3c9ca169..12abc912 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -57,6 +57,34 @@ serve(async (req) => { ) } + // Check if user is banned + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('banned') + .eq('user_id', user.id) + .single() + + if (profileError || !profile) { + console.error('Failed to fetch user profile:', profileError) + return new Response( + JSON.stringify({ error: 'User profile not found' }), + { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + + if (profile.banned) { + return new Response( + JSON.stringify({ error: 'Account suspended. Contact support for assistance.' }), + { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + // Delete image from Cloudflare let requestBody; try { @@ -149,6 +177,34 @@ serve(async (req) => { ) } + // Check if user is banned + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('banned') + .eq('user_id', user.id) + .single() + + if (profileError || !profile) { + console.error('Failed to fetch user profile:', profileError) + return new Response( + JSON.stringify({ error: 'User profile not found' }), + { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + + if (profile.banned) { + return new Response( + JSON.stringify({ error: 'Account suspended. Contact support for assistance.' }), + { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + // Request a direct upload URL from Cloudflare let requestBody; try {