mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:31:13 -05:00
Fix superuser release locks RPC
This commit is contained in:
@@ -12,11 +12,15 @@ interface EditHistoryRecord {
|
|||||||
id: string;
|
id: string;
|
||||||
item_id: string;
|
item_id: string;
|
||||||
edited_at: string;
|
edited_at: string;
|
||||||
previous_data: Record<string, unknown>;
|
|
||||||
new_data: Record<string, unknown>;
|
|
||||||
edit_reason: string | null;
|
edit_reason: string | null;
|
||||||
changed_fields: string[];
|
changed_fields: string[];
|
||||||
profiles?: {
|
field_changes?: Array<{
|
||||||
|
id: string;
|
||||||
|
field_name: string;
|
||||||
|
old_value: string | null;
|
||||||
|
new_value: string | null;
|
||||||
|
}>;
|
||||||
|
editor?: {
|
||||||
username: string;
|
username: string;
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
@@ -44,11 +48,15 @@ export function EditHistoryAccordion({ submissionId }: EditHistoryAccordionProps
|
|||||||
id,
|
id,
|
||||||
item_id,
|
item_id,
|
||||||
edited_at,
|
edited_at,
|
||||||
previous_data,
|
|
||||||
new_data,
|
|
||||||
edit_reason,
|
edit_reason,
|
||||||
changed_fields,
|
changed_fields,
|
||||||
profiles:edited_by (
|
field_changes:item_field_changes(
|
||||||
|
id,
|
||||||
|
field_name,
|
||||||
|
old_value,
|
||||||
|
new_value
|
||||||
|
),
|
||||||
|
editor:profiles!item_edit_history_edited_by_fkey(
|
||||||
username,
|
username,
|
||||||
avatar_url
|
avatar_url
|
||||||
)
|
)
|
||||||
@@ -111,19 +119,30 @@ export function EditHistoryAccordion({ submissionId }: EditHistoryAccordionProps
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<ScrollArea className="h-[400px] pr-4">
|
<ScrollArea className="h-[400px] pr-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{editHistory.map((entry: EditHistoryRecord) => (
|
{editHistory.map((entry: EditHistoryRecord) => {
|
||||||
<EditHistoryEntry
|
// Transform relational field_changes into beforeData/afterData objects
|
||||||
key={entry.id}
|
const beforeData: Record<string, unknown> = {};
|
||||||
editId={entry.id}
|
const afterData: Record<string, unknown> = {};
|
||||||
editorName={entry.profiles?.username || 'Unknown User'}
|
|
||||||
editorAvatar={entry.profiles?.avatar_url || undefined}
|
entry.field_changes?.forEach(change => {
|
||||||
timestamp={entry.edited_at}
|
beforeData[change.field_name] = change.old_value;
|
||||||
changedFields={entry.changed_fields || []}
|
afterData[change.field_name] = change.new_value;
|
||||||
editReason={entry.edit_reason || undefined}
|
});
|
||||||
beforeData={entry.previous_data}
|
|
||||||
afterData={entry.new_data}
|
return (
|
||||||
/>
|
<EditHistoryEntry
|
||||||
))}
|
key={entry.id}
|
||||||
|
editId={entry.id}
|
||||||
|
editorName={entry.editor?.username || 'Unknown User'}
|
||||||
|
editorAvatar={entry.editor?.avatar_url || undefined}
|
||||||
|
timestamp={entry.edited_at}
|
||||||
|
changedFields={entry.changed_fields || []}
|
||||||
|
editReason={entry.edit_reason || undefined}
|
||||||
|
beforeData={beforeData}
|
||||||
|
afterData={afterData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -1290,13 +1290,37 @@ export async function editSubmissionItem(
|
|||||||
if (updateError) throw updateError;
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
// Phase 4: Record edit history
|
// Phase 4: Record edit history
|
||||||
const { error: historyError } = await supabase
|
const { data: historyData, error: historyError } = await supabase
|
||||||
.from('item_edit_history')
|
.from('item_edit_history')
|
||||||
.insert({
|
.insert({
|
||||||
item_id: itemId,
|
item_id: itemId,
|
||||||
editor_id: userId,
|
edited_by: userId,
|
||||||
changes: changes,
|
changed_fields: Object.keys(changes),
|
||||||
});
|
edit_reason: 'Direct edit by moderator',
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Insert field changes relationally (NO JSON!)
|
||||||
|
if (!historyError && historyData) {
|
||||||
|
const fieldChanges = Object.entries(changes).map(([fieldName, change]: [string, any]) => ({
|
||||||
|
edit_history_id: historyData.id,
|
||||||
|
field_name: fieldName,
|
||||||
|
old_value: String(change.old ?? ''),
|
||||||
|
new_value: String(change.new ?? ''),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { error: fieldChangesError } = await supabase
|
||||||
|
.from('item_field_changes')
|
||||||
|
.insert(fieldChanges);
|
||||||
|
|
||||||
|
if (fieldChangesError) {
|
||||||
|
handleNonCriticalError(fieldChangesError, {
|
||||||
|
action: 'Record Field Changes',
|
||||||
|
metadata: { editHistoryId: historyData.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (historyError) {
|
if (historyError) {
|
||||||
handleNonCriticalError(historyError, {
|
handleNonCriticalError(historyError, {
|
||||||
@@ -1435,9 +1459,17 @@ export async function fetchEditHistory(itemId: string) {
|
|||||||
.from('item_edit_history')
|
.from('item_edit_history')
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
changes,
|
item_id,
|
||||||
edited_at,
|
edited_at,
|
||||||
editor:profiles!item_edit_history_editor_id_fkey (
|
edit_reason,
|
||||||
|
changed_fields,
|
||||||
|
field_changes:item_field_changes(
|
||||||
|
id,
|
||||||
|
field_name,
|
||||||
|
old_value,
|
||||||
|
new_value
|
||||||
|
),
|
||||||
|
editor:profiles!item_edit_history_edited_by_fkey(
|
||||||
user_id,
|
user_id,
|
||||||
username,
|
username,
|
||||||
display_name,
|
display_name,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- Fix search_path security issue in superuser_release_all_locks function
|
||||||
|
CREATE OR REPLACE FUNCTION public.superuser_release_all_locks(p_superuser_id uuid)
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public', 'auth' -- Set immutable search path for security
|
||||||
|
AS $function$
|
||||||
|
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 public.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 public.content_submissions
|
||||||
|
WHERE assigned_to IS NOT NULL
|
||||||
|
AND locked_until > NOW();
|
||||||
|
|
||||||
|
-- Release all active locks
|
||||||
|
UPDATE public.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 public.log_admin_action(
|
||||||
|
p_superuser_id,
|
||||||
|
NULL,
|
||||||
|
'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;
|
||||||
|
$function$;
|
||||||
Reference in New Issue
Block a user