mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 06:31:13 -05:00
Fix validation and approval issues
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
useQueueQuery,
|
||||
} from "./index";
|
||||
import { useModerationQueue } from "@/hooks/useModerationQueue";
|
||||
import { useModerationActions } from "./useModerationActions";
|
||||
|
||||
import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation";
|
||||
|
||||
@@ -275,224 +276,36 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Perform moderation action (approve/reject)
|
||||
* Use validated action handler from useModerationActions
|
||||
*/
|
||||
const moderationActions = useModerationActions({
|
||||
user,
|
||||
onActionStart: setActionLoading,
|
||||
onActionComplete: () => {
|
||||
setActionLoading(null);
|
||||
refresh();
|
||||
queue.refreshStats();
|
||||
},
|
||||
currentLockSubmissionId: queue.currentLock?.submissionId,
|
||||
});
|
||||
|
||||
/**
|
||||
* Perform moderation action (approve/reject) - delegates to validated handler
|
||||
*/
|
||||
const performAction = useCallback(
|
||||
async (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => {
|
||||
if (actionLoading === item.id) return;
|
||||
|
||||
setActionLoading(item.id);
|
||||
|
||||
// Check MFA (AAL2) requirement before moderation action
|
||||
if (aal === null) {
|
||||
logger.log('⏳ [QUEUE MANAGER] AAL is null, waiting for authentication status...');
|
||||
toast({
|
||||
title: "Loading Authentication Status",
|
||||
description: "Please wait while we verify your authentication level...",
|
||||
});
|
||||
setActionLoading(null);
|
||||
|
||||
// Retry after 1 second
|
||||
setTimeout(() => {
|
||||
logger.log('🔄 [QUEUE MANAGER] Retrying action after AAL load');
|
||||
performAction(item, action, moderatorNotes);
|
||||
}, 1000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (aal !== 'aal2') {
|
||||
logger.warn('🚫 [QUEUE MANAGER] MFA check failed', {
|
||||
aal,
|
||||
expected: 'aal2',
|
||||
userId: user?.id
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "MFA Verification Required",
|
||||
description: "You must complete multi-factor authentication to perform moderation actions.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setActionLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('✅ [QUEUE MANAGER] MFA check passed', { aal });
|
||||
|
||||
// Calculate stat delta for optimistic update
|
||||
const statDelta: Partial<ModerationStats> = {};
|
||||
|
||||
if (action === 'approved' || action === 'rejected') {
|
||||
statDelta.pendingSubmissions = -1;
|
||||
}
|
||||
|
||||
// Optimistically update stats IMMEDIATELY
|
||||
if (optimisticallyUpdateStats) {
|
||||
optimisticallyUpdateStats(statDelta);
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
const shouldRemove =
|
||||
(filters.statusFilter === "pending" || filters.statusFilter === "flagged") &&
|
||||
(action === "approved" || action === "rejected");
|
||||
|
||||
if (shouldRemove) {
|
||||
setItems((prev) => prev.map((i) => (i.id === item.id ? { ...i, _removing: true } : i)));
|
||||
|
||||
setTimeout(() => {
|
||||
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
||||
recentlyRemovedRef.current.add(item.id);
|
||||
setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Release lock if claimed
|
||||
// Release lock if held
|
||||
if (queue.currentLock?.submissionId === item.id) {
|
||||
await queue.releaseLock(item.id, true); // Silent release
|
||||
await queue.releaseLock(item.id, true);
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle photo submissions
|
||||
if (action === "approved" && item.submission_type === "photo") {
|
||||
const { data: photoSubmission } = await supabase
|
||||
.from("photo_submissions")
|
||||
.select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`)
|
||||
.eq("submission_id", item.id)
|
||||
.single();
|
||||
|
||||
if (photoSubmission && photoSubmission.items) {
|
||||
const { data: existingPhotos } = await supabase.from("photos").select("id").eq("submission_id", item.id);
|
||||
|
||||
if (!existingPhotos || existingPhotos.length === 0) {
|
||||
const photoRecords = photoSubmission.items.map((photoItem: any) => ({
|
||||
entity_id: photoSubmission.entity_id,
|
||||
entity_type: photoSubmission.entity_type,
|
||||
cloudflare_image_id: photoItem.cloudflare_image_id,
|
||||
cloudflare_image_url: photoItem.cloudflare_image_url,
|
||||
title: photoItem.title || null,
|
||||
caption: photoItem.caption || null,
|
||||
date_taken: photoItem.date_taken || null,
|
||||
order_index: photoItem.order_index,
|
||||
submission_id: photoSubmission.submission_id,
|
||||
submitted_by: photoSubmission.submission?.user_id,
|
||||
approved_by: user?.id,
|
||||
approved_at: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await supabase.from("photos").insert(photoRecords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for submission items
|
||||
const { data: submissionItems } = await supabase
|
||||
.from("submission_items")
|
||||
.select("id, status")
|
||||
.eq("submission_id", item.id)
|
||||
.in("status", ["pending", "rejected"]);
|
||||
|
||||
if (submissionItems && submissionItems.length > 0) {
|
||||
if (action === "approved") {
|
||||
await supabase.functions.invoke("process-selective-approval", {
|
||||
body: {
|
||||
itemIds: submissionItems.map((i) => i.id),
|
||||
submissionId: item.id,
|
||||
},
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Submission Approved",
|
||||
description: `Successfully processed ${submissionItems.length} item(s)`,
|
||||
});
|
||||
return;
|
||||
} else if (action === "rejected") {
|
||||
await supabase
|
||||
.from("submission_items")
|
||||
.update({
|
||||
status: "rejected",
|
||||
rejection_reason: moderatorNotes || "Parent submission rejected",
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("submission_id", item.id)
|
||||
.eq("status", "pending");
|
||||
}
|
||||
}
|
||||
|
||||
// Standard update
|
||||
const table = item.type === "review" ? "reviews" : "content_submissions";
|
||||
const statusField = item.type === "review" ? "moderation_status" : "status";
|
||||
const timestampField = item.type === "review" ? "moderated_at" : "reviewed_at";
|
||||
const reviewerField = item.type === "review" ? "moderated_by" : "reviewer_id";
|
||||
|
||||
const updateData: any = {
|
||||
[statusField]: action,
|
||||
[timestampField]: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (user) {
|
||||
updateData[reviewerField] = user.id;
|
||||
}
|
||||
|
||||
if (moderatorNotes) {
|
||||
updateData.reviewer_notes = moderatorNotes;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from(table).update(updateData).eq("id", item.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: `Content ${action}`,
|
||||
description: `The ${item.type} has been ${action}. Version history updated.`,
|
||||
});
|
||||
|
||||
// Refresh stats to update counts
|
||||
queue.refreshStats();
|
||||
|
||||
// Force cache invalidation before refetching to ensure fresh data
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['moderation-queue'],
|
||||
exact: false // Invalidate all query variants
|
||||
});
|
||||
await queueQuery.refetch();
|
||||
} catch (error) {
|
||||
const errorMsg = getErrorMessage(error);
|
||||
console.error("Error moderating content:", errorMsg, error);
|
||||
|
||||
// Revert optimistic update
|
||||
setItems((prev) => {
|
||||
const exists = prev.find((i) => i.id === item.id);
|
||||
if (exists) {
|
||||
return prev.map((i) => (i.id === item.id ? item : i));
|
||||
} else {
|
||||
return [...prev, item];
|
||||
}
|
||||
});
|
||||
|
||||
// Check for RLS/permission errors
|
||||
if (errorMsg.includes('row-level security') ||
|
||||
errorMsg.includes('permission denied') ||
|
||||
errorMsg.includes('policy') ||
|
||||
errorMsg.includes('violates row-level security')) {
|
||||
toast({
|
||||
title: "Permission Denied",
|
||||
description: "You don't have permission to perform this action. MFA verification may be required.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: errorMsg || `Failed to ${action} content`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
// Use validated action handler
|
||||
await moderationActions.performAction(item, action, moderatorNotes);
|
||||
},
|
||||
[actionLoading, filters.statusFilter, queue, user, toast],
|
||||
[moderationActions, queue]
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Delete a submission permanently
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user