mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
Implement superuser lock management
This commit is contained in:
@@ -8,6 +8,7 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { PhotoModal } from './PhotoModal';
|
import { PhotoModal } from './PhotoModal';
|
||||||
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
||||||
import { ItemEditDialog } from './ItemEditDialog';
|
import { ItemEditDialog } from './ItemEditDialog';
|
||||||
@@ -29,6 +30,7 @@ import { EnhancedEmptyState } from './EnhancedEmptyState';
|
|||||||
import { QueuePagination } from './QueuePagination';
|
import { QueuePagination } from './QueuePagination';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
|
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
|
||||||
|
import { SuperuserQueueControls } from './SuperuserQueueControls';
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||||
import type { ModerationQueueRef, ModerationItem } from '@/types/moderation';
|
import type { ModerationQueueRef, ModerationItem } from '@/types/moderation';
|
||||||
@@ -85,6 +87,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
const [availableItems, setAvailableItems] = useState<SubmissionItemWithDeps[]>([]);
|
const [availableItems, setAvailableItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||||
const [bulkEditMode, setBulkEditMode] = useState(false);
|
const [bulkEditMode, setBulkEditMode] = useState(false);
|
||||||
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||||
|
const [activeLocksCount, setActiveLocksCount] = useState(0);
|
||||||
|
|
||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
@@ -129,6 +132,27 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
};
|
};
|
||||||
}, [queueManager, toast]);
|
}, [queueManager, toast]);
|
||||||
|
|
||||||
|
// Fetch active locks count for superusers
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSuperuser()) return;
|
||||||
|
|
||||||
|
const fetchActiveLocksCount = async () => {
|
||||||
|
const { count } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('id', { count: 'exact', head: true })
|
||||||
|
.not('assigned_to', 'is', null)
|
||||||
|
.gt('locked_until', new Date().toISOString());
|
||||||
|
|
||||||
|
setActiveLocksCount(count || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchActiveLocksCount();
|
||||||
|
|
||||||
|
// Refresh count periodically
|
||||||
|
const interval = setInterval(fetchActiveLocksCount, 30000); // Every 30s
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isSuperuser, queueManager.queue.queueStats]);
|
||||||
|
|
||||||
// Virtual scrolling setup
|
// Virtual scrolling setup
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@@ -154,6 +178,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
});
|
});
|
||||||
}, [queueManager]);
|
}, [queueManager]);
|
||||||
|
|
||||||
|
// Superuser force release lock
|
||||||
|
const handleSuperuserReleaseLock = useCallback(async (submissionId: string) => {
|
||||||
|
await queueManager.queue.superuserReleaseLock(submissionId);
|
||||||
|
// Refresh locks count and queue
|
||||||
|
setActiveLocksCount(prev => Math.max(0, prev - 1));
|
||||||
|
queueManager.refresh();
|
||||||
|
}, [queueManager]);
|
||||||
|
|
||||||
|
// Superuser clear all locks
|
||||||
|
const handleClearAllLocks = useCallback(async () => {
|
||||||
|
const count = await queueManager.queue.superuserReleaseAllLocks();
|
||||||
|
setActiveLocksCount(0);
|
||||||
|
// Force queue refresh
|
||||||
|
queueManager.refresh();
|
||||||
|
}, [queueManager]);
|
||||||
|
|
||||||
// Clear filters handler
|
// Clear filters handler
|
||||||
const handleClearFilters = useCallback(() => {
|
const handleClearFilters = useCallback(() => {
|
||||||
queueManager.filters.clearFilters();
|
queueManager.filters.clearFilters();
|
||||||
@@ -310,6 +350,15 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Superuser Queue Controls */}
|
||||||
|
{isSuperuser() && (
|
||||||
|
<SuperuserQueueControls
|
||||||
|
activeLocksCount={activeLocksCount}
|
||||||
|
onClearAllLocks={handleClearAllLocks}
|
||||||
|
isLoading={queueManager.queue.isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filter Bar */}
|
{/* Filter Bar */}
|
||||||
<QueueFilters
|
<QueueFilters
|
||||||
activeEntityFilter={queueManager.filters.entityFilter}
|
activeEntityFilter={queueManager.filters.entityFilter}
|
||||||
@@ -390,6 +439,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
onDeleteSubmission={handleDeleteSubmission}
|
onDeleteSubmission={handleDeleteSubmission}
|
||||||
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
||||||
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
||||||
|
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
|
||||||
/>
|
/>
|
||||||
</ModerationErrorBoundary>
|
</ModerationErrorBoundary>
|
||||||
))}
|
))}
|
||||||
@@ -451,6 +501,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
onDeleteSubmission={handleDeleteSubmission}
|
onDeleteSubmission={handleDeleteSubmission}
|
||||||
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
||||||
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
||||||
|
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
|
||||||
/>
|
/>
|
||||||
</ModerationErrorBoundary>
|
</ModerationErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface QueueItemProps {
|
|||||||
onDeleteSubmission: (item: ModerationItem) => void;
|
onDeleteSubmission: (item: ModerationItem) => void;
|
||||||
onInteractionFocus: (id: string) => void;
|
onInteractionFocus: (id: string) => void;
|
||||||
onInteractionBlur: (id: string) => void;
|
onInteractionBlur: (id: string) => void;
|
||||||
|
onSuperuserReleaseLock?: (submissionId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -74,7 +75,8 @@ export const QueueItem = memo(({
|
|||||||
onClaimSubmission,
|
onClaimSubmission,
|
||||||
onDeleteSubmission,
|
onDeleteSubmission,
|
||||||
onInteractionFocus,
|
onInteractionFocus,
|
||||||
onInteractionBlur
|
onInteractionBlur,
|
||||||
|
onSuperuserReleaseLock,
|
||||||
}: QueueItemProps) => {
|
}: QueueItemProps) => {
|
||||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
|
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
|
||||||
const [isClaiming, setIsClaiming] = useState(false);
|
const [isClaiming, setIsClaiming] = useState(false);
|
||||||
@@ -345,6 +347,7 @@ export const QueueItem = memo(({
|
|||||||
onInteractionFocus={onInteractionFocus}
|
onInteractionFocus={onInteractionFocus}
|
||||||
onInteractionBlur={onInteractionBlur}
|
onInteractionBlur={onInteractionBlur}
|
||||||
onClaim={handleClaim}
|
onClaim={handleClaim}
|
||||||
|
onSuperuserReleaseLock={onSuperuserReleaseLock}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|||||||
84
src/components/moderation/SuperuserQueueControls.tsx
Normal file
84
src/components/moderation/SuperuserQueueControls.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Shield, Unlock } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
interface SuperuserQueueControlsProps {
|
||||||
|
activeLocksCount: number;
|
||||||
|
onClearAllLocks: () => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuperuserQueueControls = ({
|
||||||
|
activeLocksCount,
|
||||||
|
onClearAllLocks,
|
||||||
|
isLoading
|
||||||
|
}: SuperuserQueueControlsProps) => {
|
||||||
|
if (activeLocksCount === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert className="border-purple-500/50 bg-purple-500/5">
|
||||||
|
<Shield className="h-4 w-4 text-purple-600" />
|
||||||
|
<AlertTitle className="text-purple-900 dark:text-purple-100">
|
||||||
|
Superuser Queue Management
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription className="text-purple-800 dark:text-purple-200">
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span className="text-sm">
|
||||||
|
{activeLocksCount} active lock{activeLocksCount !== 1 ? 's' : ''} in queue
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-purple-500 text-purple-700 hover:bg-purple-50 dark:hover:bg-purple-950"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Unlock className="w-4 h-4 mr-2" />
|
||||||
|
Clear All Locks
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Clear All Active Locks?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will release {activeLocksCount} active lock{activeLocksCount !== 1 ? 's' : ''},
|
||||||
|
making all submissions available for claiming again.
|
||||||
|
This action will be logged in the audit trail.
|
||||||
|
<br /><br />
|
||||||
|
<strong>Use this for:</strong>
|
||||||
|
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||||
|
<li>Clearing stale locks after system issues</li>
|
||||||
|
<li>Resetting queue after team changes</li>
|
||||||
|
<li>Emergency queue management</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={onClearAllLocks}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
Clear All Locks
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import {
|
import {
|
||||||
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar
|
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar, Crown, Unlock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
@@ -37,6 +37,7 @@ interface QueueItemActionsProps {
|
|||||||
onInteractionFocus: (id: string) => void;
|
onInteractionFocus: (id: string) => void;
|
||||||
onInteractionBlur: (id: string) => void;
|
onInteractionBlur: (id: string) => void;
|
||||||
onClaim: () => void;
|
onClaim: () => void;
|
||||||
|
onSuperuserReleaseLock?: (submissionId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueueItemActions = memo(({
|
export const QueueItemActions = memo(({
|
||||||
@@ -60,7 +61,8 @@ export const QueueItemActions = memo(({
|
|||||||
onDeleteSubmission,
|
onDeleteSubmission,
|
||||||
onInteractionFocus,
|
onInteractionFocus,
|
||||||
onInteractionBlur,
|
onInteractionBlur,
|
||||||
onClaim
|
onClaim,
|
||||||
|
onSuperuserReleaseLock
|
||||||
}: QueueItemActionsProps) => {
|
}: QueueItemActionsProps) => {
|
||||||
// Memoize all handlers to prevent re-renders
|
// Memoize all handlers to prevent re-renders
|
||||||
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@@ -173,6 +175,34 @@ export const QueueItemActions = memo(({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Superuser Lock Override - Show for locked items */}
|
||||||
|
{isSuperuser && isLockedByOther && onSuperuserReleaseLock && (
|
||||||
|
<Alert className="border-purple-500/50 bg-purple-500/10">
|
||||||
|
<Crown className="h-4 w-4 text-purple-600" />
|
||||||
|
<AlertTitle className="text-purple-900 dark:text-purple-100">
|
||||||
|
Superuser Override
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription className="text-purple-800 dark:text-purple-200">
|
||||||
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
|
<p className="text-sm">
|
||||||
|
This submission is locked by another moderator.
|
||||||
|
You can force-release this lock.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-purple-500 text-purple-700 hover:bg-purple-50 dark:hover:bg-purple-950"
|
||||||
|
onClick={() => onSuperuserReleaseLock(item.id)}
|
||||||
|
disabled={actionLoading === item.id}
|
||||||
|
>
|
||||||
|
<Unlock className="w-4 h-4 mr-2" />
|
||||||
|
Force Release Lock
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={isMobile ? 'space-y-4 mt-4' : 'grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-6 items-start mt-4'}>
|
<div className={isMobile ? 'space-y-4 mt-4' : 'grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-6 items-start mt-4'}>
|
||||||
{/* Submitter Context - shown before moderator can add their notes */}
|
{/* Submitter Context - shown before moderator can add their notes */}
|
||||||
{(item.submission_items?.[0]?.item_data?.source_url || item.submission_items?.[0]?.item_data?.submission_notes) && (
|
{(item.submission_items?.[0]?.item_data?.source_url || item.submission_items?.[0]?.item_data?.submission_notes) && (
|
||||||
|
|||||||
@@ -494,6 +494,85 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
}
|
}
|
||||||
}, [currentLock, releaseLock]);
|
}, [currentLock, releaseLock]);
|
||||||
|
|
||||||
|
// Superuser: Force release a specific lock
|
||||||
|
const superuserReleaseLock = useCallback(async (
|
||||||
|
submissionId: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!user?.id) return false;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.rpc('superuser_release_lock', {
|
||||||
|
p_submission_id: submissionId,
|
||||||
|
p_superuser_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Lock Forcibly Released',
|
||||||
|
description: 'The submission has been unlocked and is now available',
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
|
|
||||||
|
if (onLockStateChange) {
|
||||||
|
onLockStateChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
toast({
|
||||||
|
title: 'Failed to Release Lock',
|
||||||
|
description: getErrorMessage(error),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [user, fetchStats, toast, onLockStateChange]);
|
||||||
|
|
||||||
|
// Superuser: Clear all locks
|
||||||
|
const superuserReleaseAllLocks = useCallback(async (): Promise<number> => {
|
||||||
|
if (!user?.id) return 0;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.rpc('superuser_release_all_locks', {
|
||||||
|
p_superuser_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const count = data || 0;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'All Locks Cleared',
|
||||||
|
description: `${count} submission${count !== 1 ? 's' : ''} unlocked`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
|
|
||||||
|
if (onLockStateChange) {
|
||||||
|
onLockStateChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
toast({
|
||||||
|
title: 'Failed to Clear Locks',
|
||||||
|
description: getErrorMessage(error),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [user, fetchStats, toast, onLockStateChange]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentLock, // Exposed for reactive UI updates
|
currentLock, // Exposed for reactive UI updates
|
||||||
queueStats,
|
queueStats,
|
||||||
@@ -510,6 +589,9 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
isLockedByOther,
|
isLockedByOther,
|
||||||
getLockProgress,
|
getLockProgress,
|
||||||
releaseAfterAction,
|
releaseAfterAction,
|
||||||
|
// Superuser lock management
|
||||||
|
superuserReleaseLock,
|
||||||
|
superuserReleaseAllLocks,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5760,6 +5760,14 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
}
|
}
|
||||||
|
superuser_release_all_locks: {
|
||||||
|
Args: { p_superuser_id: string }
|
||||||
|
Returns: number
|
||||||
|
}
|
||||||
|
superuser_release_lock: {
|
||||||
|
Args: { p_submission_id: string; p_superuser_id: string }
|
||||||
|
Returns: boolean
|
||||||
|
}
|
||||||
update_company_ratings: {
|
update_company_ratings: {
|
||||||
Args: { target_company_id: string }
|
Args: { target_company_id: string }
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
-- Create superuser lock management functions
|
||||||
|
|
||||||
|
-- Function to allow superusers to force-release any lock
|
||||||
|
CREATE OR REPLACE FUNCTION public.superuser_release_lock(
|
||||||
|
p_submission_id UUID,
|
||||||
|
p_superuser_id UUID
|
||||||
|
) RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_is_superuser BOOLEAN;
|
||||||
|
v_original_moderator UUID;
|
||||||
|
v_submission_type TEXT;
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
-- Verify caller is actually a superuser
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM user_roles
|
||||||
|
WHERE user_id = p_superuser_id
|
||||||
|
AND role = 'superuser'
|
||||||
|
) INTO v_is_superuser;
|
||||||
|
|
||||||
|
IF NOT v_is_superuser THEN
|
||||||
|
RAISE EXCEPTION 'Unauthorized: Only superusers can force-release locks';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Capture original moderator and submission details for audit logging
|
||||||
|
SELECT assigned_to, submission_type, user_id
|
||||||
|
INTO v_original_moderator, v_submission_type, v_user_id
|
||||||
|
FROM content_submissions
|
||||||
|
WHERE id = p_submission_id;
|
||||||
|
|
||||||
|
-- Release the lock
|
||||||
|
UPDATE content_submissions
|
||||||
|
SET
|
||||||
|
assigned_to = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
locked_until = NULL
|
||||||
|
WHERE id = p_submission_id
|
||||||
|
AND assigned_to IS NOT NULL;
|
||||||
|
|
||||||
|
-- Log the forced release if a lock was actually released
|
||||||
|
IF FOUND THEN
|
||||||
|
PERFORM log_admin_action(
|
||||||
|
p_superuser_id,
|
||||||
|
v_user_id,
|
||||||
|
'submission_lock_force_released',
|
||||||
|
jsonb_build_object(
|
||||||
|
'submission_id', p_submission_id,
|
||||||
|
'submission_type', v_submission_type,
|
||||||
|
'original_moderator', v_original_moderator,
|
||||||
|
'forced_release', true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN FOUND;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Function to allow superusers to clear all active locks
|
||||||
|
CREATE OR REPLACE FUNCTION public.superuser_release_all_locks(
|
||||||
|
p_superuser_id UUID
|
||||||
|
) RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_is_superuser BOOLEAN;
|
||||||
|
v_released_count INTEGER;
|
||||||
|
v_released_locks JSONB;
|
||||||
|
BEGIN
|
||||||
|
-- Verify caller is actually a superuser
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM user_roles
|
||||||
|
WHERE user_id = p_superuser_id
|
||||||
|
AND role = 'superuser'
|
||||||
|
) INTO v_is_superuser;
|
||||||
|
|
||||||
|
IF NOT v_is_superuser THEN
|
||||||
|
RAISE EXCEPTION 'Unauthorized: Only superusers can release all locks';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Capture all locked submissions for audit
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'submission_id', id,
|
||||||
|
'assigned_to', assigned_to,
|
||||||
|
'locked_until', locked_until,
|
||||||
|
'submission_type', submission_type
|
||||||
|
)
|
||||||
|
) INTO v_released_locks
|
||||||
|
FROM content_submissions
|
||||||
|
WHERE assigned_to IS NOT NULL
|
||||||
|
AND locked_until > NOW();
|
||||||
|
|
||||||
|
-- Release all active locks
|
||||||
|
UPDATE content_submissions
|
||||||
|
SET
|
||||||
|
assigned_to = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
locked_until = NULL
|
||||||
|
WHERE assigned_to IS NOT NULL
|
||||||
|
AND locked_until > NOW()
|
||||||
|
AND status IN ('pending', 'partially_approved');
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_released_count = ROW_COUNT;
|
||||||
|
|
||||||
|
-- Log the bulk release
|
||||||
|
IF v_released_count > 0 THEN
|
||||||
|
PERFORM log_admin_action(
|
||||||
|
p_superuser_id,
|
||||||
|
NULL, -- No specific target user
|
||||||
|
'submission_locks_bulk_released',
|
||||||
|
jsonb_build_object(
|
||||||
|
'released_count', v_released_count,
|
||||||
|
'released_locks', v_released_locks,
|
||||||
|
'bulk_operation', true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN v_released_count;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user