Implement 5-day plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-21 12:37:28 +00:00
parent 638d49c8d9
commit 12433e49e3
8 changed files with 490 additions and 73 deletions

View File

@@ -1,7 +1,7 @@
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';
import { sanitizeError } from '../_shared/errorSanitizer.ts';
import { edgeLogger } from '../_shared/logger.ts';
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
@@ -17,9 +17,16 @@ interface ExportOptions {
}
serve(async (req) => {
const tracking = startRequest();
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
return new Response(null, {
headers: {
...corsHeaders,
'X-Request-ID': tracking.requestId
}
});
}
try {
@@ -40,14 +47,34 @@ serve(async (req) => {
} = await supabaseClient.auth.getUser();
if (authError || !user) {
edgeLogger.error('Authentication failed', { action: 'export_auth' });
const duration = endRequest(tracking);
edgeLogger.error('Authentication failed', {
action: 'export_auth',
requestId: tracking.requestId,
duration
});
return new Response(
JSON.stringify({ error: 'Unauthorized', success: false }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
JSON.stringify({
error: 'Unauthorized',
success: false,
requestId: tracking.requestId
}),
{
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
edgeLogger.info('Processing export request', { action: 'export_start', userId: user.id });
edgeLogger.info('Processing export request', {
action: 'export_start',
requestId: tracking.requestId,
userId: user.id
});
// Check rate limiting - max 1 export per hour
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
@@ -64,16 +91,31 @@ serve(async (req) => {
}
if (recentExports && recentExports.length > 0) {
const duration = endRequest(tracking);
const nextAvailableAt = new Date(new Date(recentExports[0].created_at).getTime() + 60 * 60 * 1000).toISOString();
console.log(`[Export] Rate limited for user ${user.id}, next available: ${nextAvailableAt}`);
edgeLogger.warn('Rate limit exceeded for export', {
action: 'export_rate_limit',
requestId: tracking.requestId,
userId: user.id,
duration,
nextAvailableAt
});
return new Response(
JSON.stringify({
success: false,
error: 'Rate limited. You can export your data once per hour.',
rate_limited: true,
next_available_at: nextAvailableAt
next_available_at: nextAvailableAt,
requestId: tracking.requestId
}),
{ status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
{
status: 429,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
@@ -87,7 +129,11 @@ serve(async (req) => {
format: 'json'
};
edgeLogger.info('Export options', { action: 'export_options', userId: user.id });
edgeLogger.info('Export options', {
action: 'export_options',
requestId: tracking.requestId,
userId: user.id
});
// Fetch profile data
const { data: profile, error: profileError } = await supabaseClient
@@ -97,7 +143,11 @@ serve(async (req) => {
.single();
if (profileError) {
edgeLogger.error('Profile fetch failed', { action: 'export_profile' });
edgeLogger.error('Profile fetch failed', {
action: 'export_profile',
requestId: tracking.requestId,
userId: user.id
});
throw new Error('Failed to fetch profile data');
}
@@ -260,33 +310,61 @@ serve(async (req) => {
changes: {
export_options: options,
timestamp: new Date().toISOString(),
data_size: JSON.stringify(exportData).length
data_size: JSON.stringify(exportData).length,
requestId: tracking.requestId
}
}]);
edgeLogger.info('Export completed successfully', { action: 'export_complete', userId: user.id });
const duration = endRequest(tracking);
edgeLogger.info('Export completed successfully', {
action: 'export_complete',
requestId: tracking.requestId,
traceId: tracking.traceId,
userId: user.id,
duration,
dataSize: JSON.stringify(exportData).length
});
return new Response(
JSON.stringify({ success: true, data: exportData }),
JSON.stringify({
success: true,
data: exportData,
requestId: tracking.requestId
}),
{
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json"`
'Content-Disposition': `attachment; filename="thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json"`,
'X-Request-ID': tracking.requestId
}
}
);
} catch (error) {
edgeLogger.error('Export error', { action: 'export_error', error: error instanceof Error ? error.message : String(error) });
const duration = endRequest(tracking);
edgeLogger.error('Export error', {
action: 'export_error',
requestId: tracking.requestId,
duration,
error: error instanceof Error ? error.message : String(error)
});
const sanitized = sanitizeError(error, 'export-user-data');
return new Response(
JSON.stringify({
...sanitized,
success: false
success: false,
requestId: tracking.requestId
}),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
{
status: 500,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
});