mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:51:13 -05:00
Refactor export-user-data, notify-user-submission-status, and resend-deletion-code to use createEdgeFunction wrapper. Remove manual CORS, auth, rate limiting boilerplate; adopt standardized EdgeFunctionContext (supabase, user, span, requestId), and integrate built-in tracing, rate limiting, and logging through the wrapper. Update handlers to rely on wrapper context and ensure consistent error handling and observability.
260 lines
7.9 KiB
TypeScript
260 lines
7.9 KiB
TypeScript
import { serve } from 'https://deno.land/std@0.190.0/http/server.ts';
|
|
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
|
import { corsHeaders } from '../_shared/cors.ts';
|
|
import { addSpanEvent } from '../_shared/logger.ts';
|
|
|
|
interface ExportOptions {
|
|
include_reviews: boolean;
|
|
include_lists: boolean;
|
|
include_activity_log: boolean;
|
|
include_preferences: boolean;
|
|
format: 'json';
|
|
}
|
|
|
|
const handler = async (req: Request, { supabase, user, span, requestId }: EdgeFunctionContext) => {
|
|
addSpanEvent(span, 'processing_export_request', { userId: user.id });
|
|
|
|
// Additional rate limiting - max 1 export per hour
|
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
const { data: recentExports, error: rateLimitError } = await supabase
|
|
.from('profile_audit_log')
|
|
.select('created_at')
|
|
.eq('user_id', user.id)
|
|
.eq('action', 'data_exported')
|
|
.gte('created_at', oneHourAgo)
|
|
.limit(1);
|
|
|
|
if (rateLimitError) {
|
|
addSpanEvent(span, 'rate_limit_check_failed', { error: rateLimitError.message });
|
|
}
|
|
|
|
if (recentExports && recentExports.length > 0) {
|
|
const nextAvailableAt = new Date(new Date(recentExports[0].created_at).getTime() + 60 * 60 * 1000).toISOString();
|
|
addSpanEvent(span, 'rate_limit_exceeded', { 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,
|
|
}),
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
|
|
// Parse export options
|
|
const body = await req.json();
|
|
const options: ExportOptions = {
|
|
include_reviews: body.include_reviews ?? true,
|
|
include_lists: body.include_lists ?? true,
|
|
include_activity_log: body.include_activity_log ?? true,
|
|
include_preferences: body.include_preferences ?? true,
|
|
format: 'json'
|
|
};
|
|
|
|
addSpanEvent(span, 'export_options_parsed', { options });
|
|
|
|
// Fetch profile data
|
|
const { data: profile, error: profileError } = await supabase
|
|
.from('profiles')
|
|
.select('username, display_name, bio, preferred_pronouns, personal_location, timezone, preferred_language, theme_preference, privacy_level, ride_count, coaster_count, park_count, review_count, reputation_score, created_at, updated_at')
|
|
.eq('user_id', user.id)
|
|
.single();
|
|
|
|
if (profileError) {
|
|
addSpanEvent(span, 'profile_fetch_failed', { error: profileError.message });
|
|
throw new Error('Failed to fetch profile data');
|
|
}
|
|
|
|
// Fetch statistics
|
|
const { count: photoCount } = await supabase
|
|
.from('photos')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('submitted_by', user.id);
|
|
|
|
const { count: listCount } = await supabase
|
|
.from('user_lists')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('user_id', user.id);
|
|
|
|
const { count: submissionCount } = await supabase
|
|
.from('content_submissions')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('user_id', user.id);
|
|
|
|
const statistics = {
|
|
ride_count: profile.ride_count || 0,
|
|
coaster_count: profile.coaster_count || 0,
|
|
park_count: profile.park_count || 0,
|
|
review_count: profile.review_count || 0,
|
|
reputation_score: profile.reputation_score || 0,
|
|
photo_count: photoCount || 0,
|
|
list_count: listCount || 0,
|
|
submission_count: submissionCount || 0,
|
|
account_created: profile.created_at,
|
|
last_updated: profile.updated_at
|
|
};
|
|
|
|
// Fetch reviews if requested
|
|
let reviews = [];
|
|
if (options.include_reviews) {
|
|
const { data: reviewsData, error: reviewsError } = await supabase
|
|
.from('reviews')
|
|
.select(`
|
|
id,
|
|
rating,
|
|
review_text,
|
|
created_at,
|
|
rides(name),
|
|
parks(name)
|
|
`)
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (!reviewsError && reviewsData) {
|
|
reviews = reviewsData.map(r => ({
|
|
id: r.id,
|
|
rating: r.rating,
|
|
review_text: r.review_text,
|
|
ride_name: r.rides?.name,
|
|
park_name: r.parks?.name,
|
|
created_at: r.created_at
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Fetch lists if requested
|
|
let lists = [];
|
|
if (options.include_lists) {
|
|
const { data: listsData, error: listsError } = await supabase
|
|
.from('user_lists')
|
|
.select('id, name, description, is_public, created_at')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (!listsError && listsData) {
|
|
lists = await Promise.all(
|
|
listsData.map(async (list) => {
|
|
const { count } = await supabase
|
|
.from('user_list_items')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('list_id', list.id);
|
|
|
|
return {
|
|
id: list.id,
|
|
name: list.name,
|
|
description: list.description,
|
|
is_public: list.is_public,
|
|
item_count: count || 0,
|
|
created_at: list.created_at
|
|
};
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
// Fetch activity log if requested
|
|
let activity_log = [];
|
|
if (options.include_activity_log) {
|
|
const { data: activityData, error: activityError } = await supabase
|
|
.from('profile_audit_log')
|
|
.select('id, action, changes, created_at, changed_by, ip_address_hash, user_agent')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(100);
|
|
|
|
if (!activityError && activityData) {
|
|
activity_log = activityData;
|
|
}
|
|
}
|
|
|
|
// Fetch preferences if requested
|
|
let preferences = {
|
|
unit_preferences: null,
|
|
accessibility_options: null,
|
|
notification_preferences: null,
|
|
privacy_settings: null
|
|
};
|
|
|
|
if (options.include_preferences) {
|
|
const { data: prefsData } = await supabase
|
|
.from('user_preferences')
|
|
.select('unit_preferences, accessibility_options, notification_preferences, privacy_settings')
|
|
.eq('user_id', user.id)
|
|
.single();
|
|
|
|
if (prefsData) {
|
|
preferences = prefsData;
|
|
}
|
|
}
|
|
|
|
// Build export data structure
|
|
const exportData = {
|
|
export_date: new Date().toISOString(),
|
|
user_id: user.id,
|
|
profile: {
|
|
username: profile.username,
|
|
display_name: profile.display_name,
|
|
bio: profile.bio,
|
|
preferred_pronouns: profile.preferred_pronouns,
|
|
personal_location: profile.personal_location,
|
|
timezone: profile.timezone,
|
|
preferred_language: profile.preferred_language,
|
|
theme_preference: profile.theme_preference,
|
|
privacy_level: profile.privacy_level,
|
|
created_at: profile.created_at,
|
|
updated_at: profile.updated_at
|
|
},
|
|
statistics,
|
|
reviews,
|
|
lists,
|
|
activity_log,
|
|
preferences,
|
|
metadata: {
|
|
export_version: '1.0.0',
|
|
data_retention_info: 'Your data is retained according to our privacy policy. You can request deletion at any time from your account settings.',
|
|
instructions: 'This file contains all your personal data stored in ThrillWiki. You can use this for backup purposes or to migrate to another service. For questions, contact support@thrillwiki.com'
|
|
}
|
|
};
|
|
|
|
// Log the export action
|
|
await supabase.from('profile_audit_log').insert([{
|
|
user_id: user.id,
|
|
changed_by: user.id,
|
|
action: 'data_exported',
|
|
changes: {
|
|
export_options: options,
|
|
timestamp: new Date().toISOString(),
|
|
data_size: JSON.stringify(exportData).length,
|
|
requestId
|
|
}
|
|
}]);
|
|
|
|
addSpanEvent(span, 'export_completed', {
|
|
dataSize: JSON.stringify(exportData).length
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
data: exportData,
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Disposition': `attachment; filename="thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json"`,
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
serve(createEdgeFunction({
|
|
name: 'export-user-data',
|
|
requireAuth: true,
|
|
corsHeaders,
|
|
enableTracing: true,
|
|
logRequests: true,
|
|
logResponses: true,
|
|
rateLimitTier: 'strict', // 5 requests per minute
|
|
}, handler));
|