diff --git a/src/hooks/useModerationQueue.ts b/src/hooks/useModerationQueue.ts index 8f428485..3ece6821 100644 --- a/src/hooks/useModerationQueue.ts +++ b/src/hooks/useModerationQueue.ts @@ -324,20 +324,19 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => { .single(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); - const now = new Date().toISOString(); - const { error } = await supabase - .from('content_submissions') - .update({ - assigned_to: user.id, - assigned_at: new Date().toISOString(), - locked_until: expiresAt.toISOString(), - }) - .eq('id', submissionId) - .or(`assigned_to.is.null,locked_until.lt."${now}"`); // Only if unclaimed or lock expired + const { data, error } = await supabase.rpc('claim_specific_submission', { + p_submission_id: submissionId, + p_moderator_id: user.id, + p_lock_duration: '15 minutes', + }); if (error) throw error; + if (!data) { + throw new Error('Submission is already claimed or no longer available'); + } + setCurrentLock({ submissionId, expiresAt, diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index dedb8a94..d8079158 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -5403,6 +5403,14 @@ export type Database = { waiting_time: unknown }[] } + claim_specific_submission: { + Args: { + p_lock_duration?: unknown + p_moderator_id: string + p_submission_id: string + } + Returns: boolean + } cleanup_expired_sessions: { Args: never; Returns: undefined } cleanup_old_page_views: { Args: never; Returns: undefined } cleanup_old_request_metadata: { Args: never; Returns: undefined } diff --git a/supabase/migrations/20251104003206_81d138c5-f4f4-4e97-a8b8-4876d91e0ce1.sql b/supabase/migrations/20251104003206_81d138c5-f4f4-4e97-a8b8-4876d91e0ce1.sql new file mode 100644 index 00000000..310c2af9 --- /dev/null +++ b/supabase/migrations/20251104003206_81d138c5-f4f4-4e97-a8b8-4876d91e0ce1.sql @@ -0,0 +1,51 @@ +-- Create function to atomically claim a specific submission +CREATE OR REPLACE FUNCTION public.claim_specific_submission( + p_submission_id UUID, + p_moderator_id UUID, + p_lock_duration INTERVAL DEFAULT '15 minutes' +) RETURNS BOOLEAN +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + rows_updated INTEGER; +BEGIN + -- Atomically update the submission if it's unclaimed or lock expired + UPDATE content_submissions + SET + assigned_to = p_moderator_id, + assigned_at = NOW(), + locked_until = NOW() + p_lock_duration, + first_reviewed_at = COALESCE(first_reviewed_at, NOW()) + WHERE id = p_submission_id + AND ( + assigned_to IS NULL + OR locked_until < NOW() + ) + AND status = 'pending'; + + GET DIAGNOSTICS rows_updated = ROW_COUNT; + + -- Log the action if successful + IF rows_updated > 0 THEN + BEGIN + PERFORM log_admin_action( + p_moderator_id, + (SELECT user_id FROM content_submissions WHERE id = p_submission_id), + 'submission_claimed', + jsonb_build_object( + 'submission_id', p_submission_id, + 'claim_type', 'specific' + ) + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to log submission claim audit: %', SQLERRM; + END; + + RETURN TRUE; + END IF; + + RETURN FALSE; +END; +$$; \ No newline at end of file