mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:11:12 -05:00
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' } }
|
|
);
|
|
}
|
|
});
|