mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 08:31:13 -05:00
Implement the destructive migration plan to remove the old approval flow entirely. This includes deleting the legacy edge function, removing the toggle component, simplifying frontend code, and updating documentation.
189 lines
6.1 KiB
TypeScript
189 lines
6.1 KiB
TypeScript
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
|
|
|
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || 'https://api.thrillwiki.com';
|
|
const SUPABASE_ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')!;
|
|
|
|
interface ApprovalRequest {
|
|
submissionId: string;
|
|
itemIds: string[];
|
|
idempotencyKey: string;
|
|
}
|
|
|
|
serve(async (req) => {
|
|
// Generate request ID for tracking
|
|
const requestId = crypto.randomUUID();
|
|
|
|
try {
|
|
// STEP 1: Authentication
|
|
const authHeader = req.headers.get('Authorization');
|
|
if (!authHeader) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing Authorization header' }),
|
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
global: { headers: { Authorization: authHeader } }
|
|
});
|
|
|
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
if (authError || !user) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Unauthorized' }),
|
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
console.log(`[${requestId}] Approval request from moderator ${user.id}`);
|
|
|
|
// STEP 2: Parse request
|
|
const body: ApprovalRequest = await req.json();
|
|
const { submissionId, itemIds, idempotencyKey } = body;
|
|
|
|
if (!submissionId || !itemIds || itemIds.length === 0) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing required fields: submissionId, itemIds' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// STEP 3: Idempotency check
|
|
const { data: existingKey } = await supabase
|
|
.from('submission_idempotency_keys')
|
|
.select('*')
|
|
.eq('idempotency_key', idempotencyKey)
|
|
.single();
|
|
|
|
if (existingKey?.status === 'completed') {
|
|
console.log(`[${requestId}] Idempotency key already processed, returning cached result`);
|
|
return new Response(
|
|
JSON.stringify(existingKey.result_data),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Cache-Status': 'HIT'
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// STEP 4: Fetch submission to get submitter_id
|
|
const { data: submission, error: submissionError } = await supabase
|
|
.from('content_submissions')
|
|
.select('user_id, status, assigned_to')
|
|
.eq('id', submissionId)
|
|
.single();
|
|
|
|
if (submissionError || !submission) {
|
|
console.error(`[${requestId}] Submission not found:`, submissionError);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Submission not found' }),
|
|
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// STEP 5: Verify moderator can approve this submission
|
|
if (submission.assigned_to && submission.assigned_to !== user.id) {
|
|
console.error(`[${requestId}] Submission locked by another moderator`);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Submission is locked by another moderator' }),
|
|
{ status: 409, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
if (!['pending', 'partially_approved'].includes(submission.status)) {
|
|
console.error(`[${requestId}] Invalid submission status: ${submission.status}`);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Submission already processed' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// STEP 6: Register idempotency key as processing
|
|
if (!existingKey) {
|
|
await supabase.from('submission_idempotency_keys').insert({
|
|
idempotency_key: idempotencyKey,
|
|
submission_id: submissionId,
|
|
moderator_id: user.id,
|
|
status: 'processing'
|
|
});
|
|
}
|
|
|
|
console.log(`[${requestId}] Calling process_approval_transaction RPC`);
|
|
|
|
// ============================================================================
|
|
// STEP 7: Call RPC function - entire approval in single atomic transaction
|
|
// ============================================================================
|
|
const { data: result, error: rpcError } = await supabase.rpc(
|
|
'process_approval_transaction',
|
|
{
|
|
p_submission_id: submissionId,
|
|
p_item_ids: itemIds,
|
|
p_moderator_id: user.id,
|
|
p_submitter_id: submission.user_id,
|
|
p_request_id: requestId
|
|
}
|
|
);
|
|
|
|
if (rpcError) {
|
|
// Transaction failed - EVERYTHING rolled back automatically by PostgreSQL
|
|
console.error(`[${requestId}] Approval transaction failed:`, rpcError);
|
|
|
|
// Update idempotency key to failed
|
|
await supabase
|
|
.from('submission_idempotency_keys')
|
|
.update({
|
|
status: 'failed',
|
|
error_message: rpcError.message,
|
|
completed_at: new Date().toISOString()
|
|
})
|
|
.eq('idempotency_key', idempotencyKey);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Approval transaction failed',
|
|
message: rpcError.message,
|
|
details: rpcError.details
|
|
}),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
console.log(`[${requestId}] Transaction completed successfully:`, result);
|
|
|
|
// STEP 8: Success - update idempotency key
|
|
await supabase
|
|
.from('submission_idempotency_keys')
|
|
.update({
|
|
status: 'completed',
|
|
result_data: result,
|
|
completed_at: new Date().toISOString()
|
|
})
|
|
.eq('idempotency_key', idempotencyKey);
|
|
|
|
return new Response(
|
|
JSON.stringify(result),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Request-Id': requestId
|
|
}
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error(`[${requestId}] Unexpected error:`, error);
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Internal server error',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
});
|