Fix validation and approval issues

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 23:11:51 +00:00
parent c74a41c2e3
commit ba67d5414f
3 changed files with 78 additions and 209 deletions

View File

@@ -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
*/