mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 16:11:13 -05:00
Consolidate CORS handling by introducing a shared supabase/functions/_shared/cors.ts and migrate edge functions to import from it. Remove inline cors.ts usage across functions, standardize headers (including traceparent and x-request-id), and prepare for environment-aware origins.
180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
/**
|
|
* Check Transaction Status Edge Function
|
|
*
|
|
* Allows clients to poll the status of a moderation transaction
|
|
* using its idempotency key.
|
|
*
|
|
* Part of Sacred Pipeline Phase 3: Enhanced Error Handling
|
|
*/
|
|
|
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
|
import { corsHeaders } from '../_shared/cors.ts';
|
|
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
|
|
|
interface StatusRequest {
|
|
idempotencyKey: string;
|
|
}
|
|
|
|
interface StatusResponse {
|
|
status: 'pending' | 'processing' | 'completed' | 'failed' | 'expired' | 'not_found';
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
expiresAt?: string;
|
|
attempts?: number;
|
|
lastError?: string;
|
|
completedAt?: string;
|
|
action?: string;
|
|
submissionId?: string;
|
|
}
|
|
|
|
const handler = async (req: Request): Promise<Response> => {
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
const tracking = startRequest();
|
|
|
|
try {
|
|
// Verify authentication
|
|
const authHeader = req.headers.get('Authorization');
|
|
if (!authHeader) {
|
|
edgeLogger.warn('Missing authorization header', { requestId: tracking.requestId });
|
|
return new Response(
|
|
JSON.stringify({ error: 'Unauthorized', status: 'not_found' }),
|
|
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
const supabase = createClient(
|
|
Deno.env.get('SUPABASE_URL')!,
|
|
Deno.env.get('SUPABASE_ANON_KEY')!,
|
|
{ global: { headers: { Authorization: authHeader } } }
|
|
);
|
|
|
|
// Verify user
|
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
if (authError || !user) {
|
|
edgeLogger.warn('Invalid auth token', { requestId: tracking.requestId, error: authError });
|
|
return new Response(
|
|
JSON.stringify({ error: 'Unauthorized', status: 'not_found' }),
|
|
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Parse request
|
|
const { idempotencyKey }: StatusRequest = await req.json();
|
|
|
|
if (!idempotencyKey) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Missing idempotencyKey', status: 'not_found' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
edgeLogger.info('Checking transaction status', {
|
|
requestId: tracking.requestId,
|
|
userId: user.id,
|
|
idempotencyKey,
|
|
});
|
|
|
|
// Query idempotency_keys table
|
|
const { data: keyRecord, error: queryError } = await supabase
|
|
.from('idempotency_keys')
|
|
.select('*')
|
|
.eq('key', idempotencyKey)
|
|
.single();
|
|
|
|
if (queryError || !keyRecord) {
|
|
edgeLogger.info('Idempotency key not found', {
|
|
requestId: tracking.requestId,
|
|
idempotencyKey,
|
|
error: queryError,
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
status: 'not_found',
|
|
error: 'Transaction not found. It may have expired or never existed.'
|
|
} as StatusResponse),
|
|
{
|
|
status: 404,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
|
}
|
|
);
|
|
}
|
|
|
|
// Verify user owns this key
|
|
if (keyRecord.user_id !== user.id) {
|
|
edgeLogger.warn('User does not own idempotency key', {
|
|
requestId: tracking.requestId,
|
|
userId: user.id,
|
|
keyUserId: keyRecord.user_id,
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({ error: 'Unauthorized', status: 'not_found' }),
|
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Build response
|
|
const response: StatusResponse = {
|
|
status: keyRecord.status,
|
|
createdAt: keyRecord.created_at,
|
|
updatedAt: keyRecord.updated_at,
|
|
expiresAt: keyRecord.expires_at,
|
|
attempts: keyRecord.attempts,
|
|
action: keyRecord.action,
|
|
submissionId: keyRecord.submission_id,
|
|
};
|
|
|
|
// Include error if failed
|
|
if (keyRecord.status === 'failed' && keyRecord.last_error) {
|
|
response.lastError = keyRecord.last_error;
|
|
}
|
|
|
|
// Include completed timestamp if completed
|
|
if (keyRecord.status === 'completed' && keyRecord.completed_at) {
|
|
response.completedAt = keyRecord.completed_at;
|
|
}
|
|
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.info('Transaction status retrieved', {
|
|
requestId: tracking.requestId,
|
|
duration,
|
|
status: response.status,
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify(response),
|
|
{
|
|
status: 200,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
const duration = endRequest(tracking);
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
|
|
edgeLogger.error('Error checking transaction status', {
|
|
requestId: tracking.requestId,
|
|
duration,
|
|
error: errorMessage,
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Internal server error',
|
|
status: 'not_found'
|
|
}),
|
|
{
|
|
status: 500,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
Deno.serve(handler);
|