Files
thrilltrack-explorer/supabase/functions/check-transaction-status/index.ts
gpt-engineer-app[bot] 91a5b0e7dd Implement Phase 3: Enhanced Error Handling
This commit implements Phase 3 of the Sacred Pipeline, focusing on enhanced error handling. It includes:

- **Transaction Status Polling Endpoint**: A new edge function `check-transaction-status` allows clients to poll the status of moderation transactions using idempotency keys.
- **Expanded Error Sanitizer Patterns**: The `src/lib/errorSanitizer.ts` file has been updated with more comprehensive patterns to remove sensitive information from error messages, making them safer for display and logging. User-friendly replacements for common errors are also included.
- **Rate Limiting for Submission Creation**: Client-side rate limiting has been implemented in `src/lib/submissionRateLimiter.ts` and applied to key submission functions within `src/lib/entitySubmissionHelpers.ts` (e.g., `submitParkCreation`, `submitRideCreation`, `submitParkUpdate`, `submitRideUpdate`) to prevent abuse and accidental duplicate submissions.
2025-11-07 18:22:27 +00:00

184 lines
5.2 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 { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
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);