diff --git a/src/components/moderation/EscalationDialog.tsx b/src/components/moderation/EscalationDialog.tsx index a5d5ca88..b6d84135 100644 --- a/src/components/moderation/EscalationDialog.tsx +++ b/src/components/moderation/EscalationDialog.tsx @@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ interface EscalationDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onEscalate: (reason: string, additionalNotes?: string) => void; + onEscalate: (reason: string) => void; } const escalationReasons = [ @@ -31,9 +31,9 @@ export function EscalationDialog({ const handleEscalate = () => { const reason = selectedReason === 'Other' ? additionalNotes - : selectedReason; + : `${selectedReason}${additionalNotes ? ': ' + additionalNotes : ''}`; - onEscalate(reason, selectedReason !== 'Other' ? additionalNotes : undefined); + onEscalate(reason); onOpenChange(false); // Reset form diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index 7a3dfa6f..7893ac27 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -150,38 +150,13 @@ export function SubmissionReviewManager({ setLoading(true); try { - // Use edge function for complex approval processing - const { supabase } = await import('@/integrations/supabase/client'); + const selectedItems = items.filter(item => selectedItemIds.has(item.id)); + await approveSubmissionItems(selectedItems, user.id); - const { data, error } = await supabase.functions.invoke('process-selective-approval', { - body: { - submissionId, - selectedItemIds: Array.from(selectedItemIds), - moderatorId: user.id, - } + toast({ + title: 'Success', + description: `Approved ${selectedItems.length} item(s)`, }); - - if (error) throw error; - - const result = data as { - success: boolean; - processedItems: any[]; - successCount: number; - failureCount: number; - }; - - if (result.failureCount > 0) { - toast({ - title: 'Partial Success', - description: `Approved ${result.successCount} of ${result.processedItems.length} item(s). ${result.failureCount} failed.`, - variant: 'default', - }); - } else { - toast({ - title: 'Success', - description: `Approved ${result.successCount} item(s)`, - }); - } onComplete(); onOpenChange(false); @@ -251,7 +226,7 @@ export function SubmissionReviewManager({ } }; - const handleEscalate = async (reason: string, additionalNotes?: string) => { + const handleEscalate = async (reason: string) => { if (!user?.id) { toast({ title: 'Authentication Required', @@ -263,35 +238,12 @@ export function SubmissionReviewManager({ setLoading(true); try { - // Update submission escalation status await escalateSubmission(submissionId, reason, user.id); - // Send email notifications via edge function - const { supabase } = await import('@/integrations/supabase/client'); - - const { data, error: emailError } = await supabase.functions.invoke('send-escalation-notification', { - body: { - submissionId, - escalationReason: reason, - escalatedBy: user.id, - additionalNotes, - } + toast({ + title: 'Escalated', + description: 'Submission escalated to admin for review', }); - - if (emailError) { - console.error('Failed to send escalation emails:', emailError); - toast({ - title: 'Escalated', - description: 'Submission escalated but email notifications failed', - variant: 'default', - }); - } else { - const result = data as { emailsSent: number; totalRecipients: number }; - toast({ - title: 'Escalated', - description: `Submission escalated to admin. ${result.emailsSent} of ${result.totalRecipients} admin(s) notified.`, - }); - } onComplete(); onOpenChange(false); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ffb6c7ad..778bff31 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -130,8 +130,6 @@ export type Database = { approval_mode: string | null content: Json created_at: string - escalated: boolean | null - escalated_at: string | null escalated_by: string | null escalation_reason: string | null id: string @@ -148,8 +146,6 @@ export type Database = { approval_mode?: string | null content: Json created_at?: string - escalated?: boolean | null - escalated_at?: string | null escalated_by?: string | null escalation_reason?: string | null id?: string @@ -166,8 +162,6 @@ export type Database = { approval_mode?: string | null content?: Json created_at?: string - escalated?: boolean | null - escalated_at?: string | null escalated_by?: string | null escalation_reason?: string | null id?: string @@ -190,36 +184,6 @@ export type Database = { }, ] } - email_aliases: { - Row: { - created_at: string - description: string | null - email: string - id: string - key: string - owner_id: string | null - updated_at: string - } - Insert: { - created_at?: string - description?: string | null - email: string - id?: string - key: string - owner_id?: string | null - updated_at?: string - } - Update: { - created_at?: string - description?: string | null - email?: string - id?: string - key?: string - owner_id?: string | null - updated_at?: string - } - Relationships: [] - } locations: { Row: { city: string | null diff --git a/supabase/config.toml b/supabase/config.toml index 459675b5..05e015f8 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -3,8 +3,4 @@ project_id = "ydvtmnrszybqnbcqbdcy" [functions.detect-location] verify_jwt = false -[functions.upload-image] - -[functions.process-selective-approval] - -[functions.send-escalation-notification] \ No newline at end of file +[functions.upload-image] \ No newline at end of file diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts deleted file mode 100644 index 91771869..00000000 --- a/supabase/functions/process-selective-approval/index.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; -import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; - -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', -}; - -interface ProcessApprovalRequest { - submissionId: string; - selectedItemIds: string[]; - moderatorId: string; -} - -interface ProcessedItem { - id: string; - entityType: string; - entityId: string | null; - status: 'success' | 'failed'; - error?: string; -} - -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { - const supabaseClient = createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '', - { - auth: { - autoRefreshToken: false, - persistSession: false - } - } - ); - - const authHeader = req.headers.get('Authorization'); - if (!authHeader) { - throw new Error('No authorization header'); - } - - const token = authHeader.replace('Bearer ', ''); - const { data: { user }, error: authError } = await supabaseClient.auth.getUser(token); - - if (authError || !user) { - throw new Error('Unauthorized'); - } - - // Verify user is moderator or admin - const { data: userRoles } = await supabaseClient - .from('user_roles') - .select('role') - .eq('user_id', user.id); - - const isModerator = userRoles?.some(r => - ['moderator', 'admin', 'superuser'].includes(r.role) - ); - - if (!isModerator) { - throw new Error('Insufficient permissions'); - } - - const { submissionId, selectedItemIds, moderatorId }: ProcessApprovalRequest = await req.json(); - - console.log('Processing selective approval:', { submissionId, selectedItemIds, moderatorId }); - - // Fetch all selected items with their dependencies - const { data: items, error: itemsError } = await supabaseClient - .from('submission_items') - .select('*') - .in('id', selectedItemIds) - .eq('submission_id', submissionId); - - if (itemsError) { - throw new Error(`Failed to fetch items: ${itemsError.message}`); - } - - // Topological sort to process items in correct order - const sortedItems = topologicalSort(items || []); - const processedItems: ProcessedItem[] = []; - const entityMap = new Map(); // temp_id -> real_id mapping - - // Process each item in dependency order - for (const item of sortedItems) { - try { - let entityId: string | null = null; - - // Resolve parent dependency if exists - if (item.depends_on && entityMap.has(item.depends_on)) { - item.item_data.parent_id = entityMap.get(item.depends_on); - } - - // Create the entity based on type - switch (item.item_type) { - case 'manufacturer': - const { data: manufacturer, error: mfgError } = await supabaseClient - .from('companies') - .insert({ - name: item.item_data.name, - slug: item.item_data.slug, - company_type: 'MANUFACTURER', - description: item.item_data.description, - founded_year: item.item_data.founded_year, - headquarters_location: item.item_data.headquarters_location, - website_url: item.item_data.website_url, - }) - .select('id') - .single(); - - if (mfgError) throw mfgError; - entityId = manufacturer.id; - break; - - case 'designer': - const { data: designer, error: designerError } = await supabaseClient - .from('companies') - .insert({ - name: item.item_data.name, - slug: item.item_data.slug, - company_type: 'DESIGNER', - description: item.item_data.description, - founded_year: item.item_data.founded_year, - headquarters_location: item.item_data.headquarters_location, - website_url: item.item_data.website_url, - }) - .select('id') - .single(); - - if (designerError) throw designerError; - entityId = designer.id; - break; - - case 'ride_model': - const { data: model, error: modelError } = await supabaseClient - .from('ride_models') - .insert({ - name: item.item_data.name, - slug: item.item_data.slug, - manufacturer_id: item.item_data.manufacturer_id || item.item_data.parent_id, - description: item.item_data.description, - model_type: item.item_data.model_type, - }) - .select('id') - .single(); - - if (modelError) throw modelError; - entityId = model.id; - break; - - case 'ride': - const { data: ride, error: rideError } = await supabaseClient - .from('rides') - .insert({ - name: item.item_data.name, - slug: item.item_data.slug, - park_id: item.item_data.park_id, - manufacturer_id: item.item_data.manufacturer_id, - designer_id: item.item_data.designer_id, - model_id: item.item_data.model_id || item.item_data.parent_id, - ride_type: item.item_data.ride_type, - status: item.item_data.status, - opening_date: item.item_data.opening_date, - closing_date: item.item_data.closing_date, - description: item.item_data.description, - }) - .select('id') - .single(); - - if (rideError) throw rideError; - entityId = ride.id; - break; - - case 'park': - const { data: park, error: parkError } = await supabaseClient - .from('parks') - .insert({ - name: item.item_data.name, - slug: item.item_data.slug, - operator_id: item.item_data.operator_id, - property_owner_id: item.item_data.property_owner_id, - location_id: item.item_data.location_id, - status: item.item_data.status, - opening_date: item.item_data.opening_date, - closing_date: item.item_data.closing_date, - description: item.item_data.description, - website_url: item.item_data.website_url, - }) - .select('id') - .single(); - - if (parkError) throw parkError; - entityId = park.id; - break; - - default: - throw new Error(`Unknown item type: ${item.item_type}`); - } - - // Update submission item status - await supabaseClient - .from('submission_items') - .update({ - status: 'approved', - entity_id: entityId, - reviewed_by: moderatorId, - reviewed_at: new Date().toISOString(), - }) - .eq('id', item.id); - - // Store mapping for dependent items - entityMap.set(item.id, entityId!); - - processedItems.push({ - id: item.id, - entityType: item.item_type, - entityId, - status: 'success', - }); - - console.log(`Successfully processed ${item.item_type} ${item.id} -> ${entityId}`); - } catch (error: any) { - console.error(`Failed to process item ${item.id}:`, error); - - // Mark item as failed - await supabaseClient - .from('submission_items') - .update({ - status: 'rejected', - rejection_reason: error.message, - reviewed_by: moderatorId, - reviewed_at: new Date().toISOString(), - }) - .eq('id', item.id); - - processedItems.push({ - id: item.id, - entityType: item.item_type, - entityId: null, - status: 'failed', - error: error.message, - }); - } - } - - // Update submission status if all items are processed - const { data: remainingItems } = await supabaseClient - .from('submission_items') - .select('id') - .eq('submission_id', submissionId) - .eq('status', 'pending'); - - if (!remainingItems || remainingItems.length === 0) { - await supabaseClient - .from('content_submissions') - .update({ - status: 'approved', - reviewed_at: new Date().toISOString(), - }) - .eq('id', submissionId); - } - - return new Response( - JSON.stringify({ - success: true, - processedItems, - totalProcessed: processedItems.length, - successCount: processedItems.filter(p => p.status === 'success').length, - failureCount: processedItems.filter(p => p.status === 'failed').length, - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 200, - } - ); - } catch (error: any) { - console.error('Error in process-selective-approval:', error); - return new Response( - JSON.stringify({ error: error.message }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 500, - } - ); - } -}); - -// Topological sort helper -function topologicalSort(items: any[]): any[] { - const sorted: any[] = []; - const visited = new Set(); - const visiting = new Set(); - - function visit(item: any) { - if (visited.has(item.id)) return; - if (visiting.has(item.id)) { - throw new Error('Circular dependency detected'); - } - - visiting.add(item.id); - - // Visit dependencies first - if (item.depends_on) { - const parent = items.find(i => i.id === item.depends_on); - if (parent) { - visit(parent); - } - } - - visiting.delete(item.id); - visited.add(item.id); - sorted.push(item); - } - - items.forEach(item => visit(item)); - return sorted; -} diff --git a/supabase/functions/send-escalation-notification/index.ts b/supabase/functions/send-escalation-notification/index.ts deleted file mode 100644 index c3d0f830..00000000 --- a/supabase/functions/send-escalation-notification/index.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; -import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; - -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', -}; - -interface EscalationRequest { - submissionId: string; - escalationReason: string; - escalatedBy: string; - additionalNotes?: string; -} - -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { - const supabaseClient = createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '', - { - auth: { - autoRefreshToken: false, - persistSession: false - } - } - ); - - const { submissionId, escalationReason, escalatedBy, additionalNotes }: EscalationRequest = await req.json(); - - console.log('Processing escalation notification:', { submissionId, escalationReason, escalatedBy }); - - // Get submission details - const { data: submission, error: submissionError } = await supabaseClient - .from('content_submissions') - .select(` - *, - submitter:profiles!content_submissions_user_id_fkey(username, display_name) - `) - .eq('id', submissionId) - .single(); - - if (submissionError || !submission) { - throw new Error('Submission not found'); - } - - // Get escalated by user details - const { data: escalator, error: escalatorError } = await supabaseClient - .from('profiles') - .select('username, display_name') - .eq('user_id', escalatedBy) - .single(); - - // Get all admin/superuser emails - const { data: adminUsers, error: adminError } = await supabaseClient - .from('user_roles') - .select('user_id, profiles!inner(user_id, display_name, username)') - .in('role', ['admin', 'superuser']); - - if (adminError || !adminUsers || adminUsers.length === 0) { - throw new Error('No admin users found to notify'); - } - - // Get email addresses for admins - const adminUserIds = adminUsers.map(u => u.user_id); - const { data: authUsers, error: authError } = await supabaseClient.auth.admin.listUsers(); - - if (authError) { - throw new Error('Failed to fetch admin emails'); - } - - const adminEmails = authUsers.users - .filter(u => adminUserIds.includes(u.id)) - .map(u => u.email) - .filter(Boolean); - - if (adminEmails.length === 0) { - throw new Error('No admin emails found'); - } - - // Get submission items for context - const { data: items } = await supabaseClient - .from('submission_items') - .select('item_type, item_data') - .eq('submission_id', submissionId); - - const itemsSummary = items?.map(item => - `${item.item_type}: ${item.item_data.name || 'Unnamed'}` - ).join(', ') || 'No items'; - - // Prepare email content - const emailSubject = `🚨 Escalated Submission: ${submission.title || submissionId}`; - const emailHtml = ` - - - - - - -
-
-

⚠️ Submission Escalated

-

Immediate attention required

-
-
-
-
Submission Title
-
${submission.title || 'Untitled Submission'}
-
- -
-
Submitted By
-
${submission.submitter?.display_name || submission.submitter?.username || 'Unknown'}
-
- -
-
Items in Submission
-
${itemsSummary}
-
- -
-
Escalation Reason
-
${escalationReason}
- ${additionalNotes ? `
Additional Notes:
${additionalNotes}
` : ''} -
- -
-
Escalated By
-
${escalator?.display_name || escalator?.username || 'System'}
-
- -
-
Submission ID
-
${submissionId}
-
- - - Review Submission → - - - -
-
- - - `; - - const emailText = ` -ESCALATED SUBMISSION - IMMEDIATE ATTENTION REQUIRED - -Submission: ${submission.title || 'Untitled'} -Submitted By: ${submission.submitter?.display_name || submission.submitter?.username || 'Unknown'} - -Items: ${itemsSummary} - -ESCALATION REASON: -${escalationReason} - -${additionalNotes ? `Additional Notes:\n${additionalNotes}\n` : ''} - -Escalated By: ${escalator?.display_name || escalator?.username || 'System'} - -Submission ID: ${submissionId} - -Review at: ${Deno.env.get('SUPABASE_URL')?.replace('supabase.co', 'lovableproject.com')}/admin?tab=moderation&submission=${submissionId} - `.trim(); - - // Send emails to all admins using forwardemail_proxy - const emailResults = []; - - for (const adminEmail of adminEmails) { - try { - const { data: emailData, error: emailError } = await supabaseClient.functions.invoke('forwardemail_proxy', { - body: { - to: adminEmail, - subject: emailSubject, - html: emailHtml, - text: emailText, - } - }); - - if (emailError) { - console.error(`Failed to send email to ${adminEmail}:`, emailError); - emailResults.push({ email: adminEmail, status: 'failed', error: emailError.message }); - } else { - console.log(`Email sent successfully to ${adminEmail}`); - emailResults.push({ email: adminEmail, status: 'sent' }); - } - } catch (error: any) { - console.error(`Exception sending email to ${adminEmail}:`, error); - emailResults.push({ email: adminEmail, status: 'failed', error: error.message }); - } - } - - // Log escalation - await supabaseClient - .from('content_submissions') - .update({ - escalated: true, - escalation_reason: escalationReason, - escalated_at: new Date().toISOString(), - escalated_by: escalatedBy, - }) - .eq('id', submissionId); - - const successCount = emailResults.filter(r => r.status === 'sent').length; - const failureCount = emailResults.filter(r => r.status === 'failed').length; - - return new Response( - JSON.stringify({ - success: true, - emailsSent: successCount, - totalRecipients: adminEmails.length, - failures: failureCount, - results: emailResults, - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 200, - } - ); - } catch (error: any) { - console.error('Error in send-escalation-notification:', error); - return new Response( - JSON.stringify({ error: error.message }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 500, - } - ); - } -}); diff --git a/supabase/migrations/20250930183419_97666361-7c5e-4875-863a-424c6018ffbc.sql b/supabase/migrations/20250930183419_97666361-7c5e-4875-863a-424c6018ffbc.sql deleted file mode 100644 index d431ed30..00000000 --- a/supabase/migrations/20250930183419_97666361-7c5e-4875-863a-424c6018ffbc.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Add escalation tracking fields to content_submissions table -ALTER TABLE public.content_submissions -ADD COLUMN IF NOT EXISTS escalated BOOLEAN DEFAULT FALSE, -ADD COLUMN IF NOT EXISTS escalation_reason TEXT, -ADD COLUMN IF NOT EXISTS escalated_at TIMESTAMP WITH TIME ZONE, -ADD COLUMN IF NOT EXISTS escalated_by UUID REFERENCES auth.users(id); - --- Add index for escalated submissions -CREATE INDEX IF NOT EXISTS idx_content_submissions_escalated -ON public.content_submissions(escalated) -WHERE escalated = TRUE; - --- Add index for escalated_by -CREATE INDEX IF NOT EXISTS idx_content_submissions_escalated_by -ON public.content_submissions(escalated_by); - --- Add comments for documentation -COMMENT ON COLUMN public.content_submissions.escalated IS 'Whether this submission has been escalated to admin'; -COMMENT ON COLUMN public.content_submissions.escalation_reason IS 'Reason for escalation'; -COMMENT ON COLUMN public.content_submissions.escalated_at IS 'Timestamp when submission was escalated'; -COMMENT ON COLUMN public.content_submissions.escalated_by IS 'User who escalated the submission'; \ No newline at end of file diff --git a/supabase/migrations/20250930183452_6626976f-214b-4eee-a50c-f6c61be19e32.sql b/supabase/migrations/20250930183452_6626976f-214b-4eee-a50c-f6c61be19e32.sql deleted file mode 100644 index df4f9fd0..00000000 --- a/supabase/migrations/20250930183452_6626976f-214b-4eee-a50c-f6c61be19e32.sql +++ /dev/null @@ -1,15 +0,0 @@ --- The content_submissions table already has RLS policies defined --- We just need to ensure RLS is enabled (it should already be enabled from previous migrations) --- This migration verifies RLS is enabled - --- Verify RLS is enabled on content_submissions -ALTER TABLE public.content_submissions ENABLE ROW LEVEL SECURITY; - --- Verify all existing RLS policies are in place --- (They should already exist from previous migrations, but we document them here) - --- Users can create submissions (already exists) --- Users can view their own submissions (already exists) --- Moderators can view all content submissions (already exists) --- Moderators can update content submissions (already exists) --- Moderators can delete content submissions (already exists) \ No newline at end of file