diff --git a/src/components/settings/DataExportTab.tsx b/src/components/settings/DataExportTab.tsx index dcfae0df..4b17ac08 100644 --- a/src/components/settings/DataExportTab.tsx +++ b/src/components/settings/DataExportTab.tsx @@ -1,216 +1,485 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Progress } from '@/components/ui/progress'; import { Separator } from '@/components/ui/separator'; -import { Badge } from '@/components/ui/badge'; -import { useToast } from '@/hooks/use-toast'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; import { useAuth } from '@/hooks/useAuth'; import { useProfile } from '@/hooks/useProfile'; import { supabase } from '@/integrations/supabase/client'; -import { Download, BarChart3, Upload, FileText, History } from 'lucide-react'; +import { handleError, handleSuccess, AppError } from '@/lib/errorHandler'; +import { logger } from '@/lib/logger'; +import { Download, Activity, BarChart3, AlertCircle, Clock } from 'lucide-react'; +import type { + UserStatistics, + ActivityLogEntry, + ExportOptions, + ExportRequestResult +} from '@/types/data-export'; +import { + exportOptionsSchema, + DEFAULT_EXPORT_OPTIONS +} from '@/lib/dataExportValidation'; + export function DataExportTab() { const { user } = useAuth(); const { data: profile } = useProfile(user?.id); - const { - toast - } = useToast(); - const [exportLoading, setExportLoading] = useState(false); - const [exportProgress, setExportProgress] = useState(0); - const handleDataExport = async () => { - if (!user) return; - setExportLoading(true); - setExportProgress(0); + const [loading, setLoading] = useState(true); + const [exporting, setExporting] = useState(false); + const [statistics, setStatistics] = useState(null); + const [recentActivity, setRecentActivity] = useState([]); + const [exportOptions, setExportOptions] = useState(DEFAULT_EXPORT_OPTIONS); + const [rateLimited, setRateLimited] = useState(false); + const [nextAvailableAt, setNextAvailableAt] = useState(null); + + useEffect(() => { + if (user && profile) { + loadStatistics(); + loadRecentActivity(); + } + }, [user, profile]); + + const loadStatistics = async () => { + if (!user || !profile) return; + try { - // Simulate export progress - const steps = ['Collecting profile data...', 'Gathering reviews...', 'Collecting ride credits...', 'Gathering top lists...', 'Packaging data...', 'Preparing download...']; - for (let i = 0; i < steps.length; i++) { - await new Promise(resolve => setTimeout(resolve, 500)); - setExportProgress((i + 1) / steps.length * 100); - } + // Fetch additional counts + const { count: photoCount } = await supabase + .from('photos') + .select('*', { count: 'exact', head: true }) + .eq('submitted_by', user.id); - // In a real implementation, this would: - // 1. Fetch all user data from various tables - // 2. Package it into a structured format (JSON/CSV) - // 3. Create a downloadable file + // Note: user_lists table needs to be created for this to work + // For now, setting to 0 as placeholder + const listCount = 0; - const exportData = { - profile: profile, - export_date: new Date().toISOString(), - data_types: ['profile', 'reviews', 'ride_credits', 'top_lists'] + const { count: submissionCount } = await supabase + .from('content_submissions') + .select('*', { count: 'exact', head: true }) + .eq('user_id', user.id); + + const stats: UserStatistics = { + 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 }; - const blob = new Blob([JSON.stringify(exportData, null, 2)], { - type: 'application/json' + + setStatistics(stats); + + logger.info('User statistics loaded', { + userId: user.id, + action: 'load_statistics' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - toast({ - title: 'Export complete', - description: 'Your data has been exported and downloaded successfully.' + } catch (error) { + logger.error('Failed to load statistics', { + userId: user.id, + action: 'load_statistics', + error: error instanceof Error ? error.message : String(error) }); - } catch (error: any) { - toast({ - title: 'Export failed', - description: error.message || 'Failed to export your data', - variant: 'destructive' + + handleError(error, { + action: 'Load statistics', + userId: user.id }); - } finally { - setExportLoading(false); - setExportProgress(0); } }; - const handleImportData = () => { - toast({ - title: 'Coming soon', - description: 'Data import functionality will be available in a future update.' + + const loadRecentActivity = async () => { + if (!user) return; + + try { + const { data, error } = 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(10); + + if (error) { + logger.error('Failed to load activity log', { + userId: user.id, + action: 'load_activity_log', + error: error.message, + errorCode: error.code + }); + throw error; + } + + // Transform the data to match our type + const activityData: ActivityLogEntry[] = (data || []).map(item => ({ + id: item.id, + action: item.action, + changes: item.changes as Record, + created_at: item.created_at, + changed_by: item.changed_by, + ip_address_hash: item.ip_address_hash || undefined, + user_agent: item.user_agent || undefined + })); + + setRecentActivity(activityData); + + logger.info('Activity log loaded', { + userId: user.id, + action: 'load_activity_log', + count: activityData.length + }); + } catch (error) { + logger.error('Error loading activity log', { + userId: user.id, + action: 'load_activity_log', + error: error instanceof Error ? error.message : String(error) + }); + + handleError(error, { + action: 'Load activity log', + userId: user.id + }); + } finally { + setLoading(false); + } + }; + + const handleDataExport = async () => { + if (!user) return; + + setExporting(true); + + try { + // Validate export options + const validatedOptions = exportOptionsSchema.parse(exportOptions); + + logger.info('Starting data export', { + userId: user.id, + action: 'export_data', + options: validatedOptions + }); + + // Call edge function for secure export + const { data, error } = await supabase.functions.invoke( + 'export-user-data', + { + body: validatedOptions + } + ); + + if (error) { + logger.error('Edge function invocation failed', { + userId: user.id, + action: 'export_data', + error: error.message + }); + throw error; + } + + if (!data?.success) { + if (data?.rate_limited) { + setRateLimited(true); + setNextAvailableAt(data.next_available_at || null); + + logger.warn('Export rate limited', { + userId: user.id, + action: 'export_data', + nextAvailableAt: data.next_available_at + }); + + handleError( + new AppError( + 'Rate limited', + 'RATE_LIMIT_EXCEEDED', + data.error || 'You can export your data once per hour. Please try again later.' + ), + { action: 'Export data', userId: user.id } + ); + return; + } + + throw new Error(data?.error || 'Export failed'); + } + + // Download the data as JSON + const blob = new Blob([JSON.stringify(data.data, null, 2)], { + type: 'application/json' + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + logger.info('Data export completed', { + userId: user.id, + action: 'export_data', + dataSize: JSON.stringify(data.data).length + }); + + handleSuccess( + 'Data exported successfully', + 'Your data has been downloaded as a JSON file.' + ); + + // Refresh activity log to show the export action + await loadRecentActivity(); + } catch (error) { + logger.error('Data export failed', { + userId: user.id, + action: 'export_data', + error: error instanceof Error ? error.message : String(error) + }); + + handleError(error, { + action: 'Export data', + userId: user.id + }); + } finally { + setExporting(false); + } + }; + + const formatActionName = (action: string): string => { + return action + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }; + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' }); }; - return
+ + if (loading) { + return ( +
+ + + + + + + + + + +
+ ); + } + + return ( +
{/* Personal Statistics */}

Personal Statistics

- + - Overview of your activity and contributions to ThrillWiku. + Your activity and contribution statistics on ThrillWiki -
-
-
- {profile?.review_count || 0} + {statistics && ( +
+
+

Reviews

+

{statistics.review_count}

-
Reviews
-
- -
-
- {profile?.ride_count || 0} +
+

Rides Tracked

+

{statistics.ride_count}

-
Rides
-
- -
-
- {profile?.coaster_count || 0} +
+

Coasters

+

{statistics.coaster_count}

-
Coasters
-
- -
-
- {profile?.park_count || 0} +
+

Parks Visited

+

{statistics.park_count}

+
+
+

Photos

+

{statistics.photo_count}

+
+
+

Lists

+

{statistics.list_count}

+
+
+

Submissions

+

{statistics.submission_count}

+
+
+

Reputation

+

{statistics.reputation_score}

-
Parks
-
+ )}
- {/* Data Export */} + {/* Export Your Data */}

Export Your Data

- + Download Your Data - Export all your personal data in a machine-readable format. This includes your profile, - reviews, ride credits, top lists, and account activity. + Export all your ThrillWiki data in JSON format. This includes your profile, reviews, lists, and activity history. - -
- Profile Information - Reviews & Ratings - Ride Credits - Top Lists - Account Activity -
- - {exportLoading &&
-
- Exporting data... - {Math.round(exportProgress)}% + + {rateLimited && nextAvailableAt && ( +
+ +
+

Rate Limited

+

+ You can export your data once per hour. Next export available at{' '} + {formatDate(nextAvailableAt)}. +

- -
} - -
+ )} + +
+

+ Choose what to include in your export: +

+ +
+
+ + + setExportOptions({ ...exportOptions, include_reviews: checked }) + } + /> +
+ +
+ + + setExportOptions({ ...exportOptions, include_lists: checked }) + } + /> +
+ +
+ + + setExportOptions({ ...exportOptions, include_activity_log: checked }) + } + /> +
+ +
+ + + setExportOptions({ ...exportOptions, include_preferences: checked }) + } + /> +
+
+
+ +
+ +
+

GDPR Compliance

+

+ This export includes all personal data we store about you. You can use this for backup purposes or to migrate to another service. +

+
+
+ + - -

- Your data will be provided in JSON format. Processing may take a few moments - for accounts with lots of activity. -

- - - {/* Data Import */} - - - + {/* Account Activity */}
- +

Account Activity

- + - Recent Activity Log - Your recent account activities and important events. + Recent account activity and changes -
-
-
-
-

Profile updated

-

- {profile?.updated_at ? new Date(profile.updated_at).toLocaleString() : 'Recently'} -

-
+ {recentActivity.length === 0 ? ( +

+ No recent activity to display +

+ ) : ( +
+ {recentActivity.map((activity) => ( +
+ +
+

+ {formatActionName(activity.action)} +

+

+ {formatDate(activity.created_at)} +

+
+
+ ))}
- -
-
-
-

Account created

-

- {profile?.created_at ? new Date(profile.created_at).toLocaleString() : 'N/A'} -

-
-
-
+ )}
-
; -} \ No newline at end of file +
+ ); +} diff --git a/src/lib/dataExportValidation.ts b/src/lib/dataExportValidation.ts new file mode 100644 index 00000000..44c597b0 --- /dev/null +++ b/src/lib/dataExportValidation.ts @@ -0,0 +1,121 @@ +import { z } from 'zod'; + +/** + * Validation schemas for data export + */ + +/** + * User statistics schema + */ +export const userStatisticsSchema = z.object({ + ride_count: z.number().int().min(0), + coaster_count: z.number().int().min(0), + park_count: z.number().int().min(0), + review_count: z.number().int().min(0), + reputation_score: z.number().int().min(0), + photo_count: z.number().int().min(0), + list_count: z.number().int().min(0), + submission_count: z.number().int().min(0), + account_created: z.string(), + last_updated: z.string() +}); + +/** + * Activity log entry schema + */ +export const activityLogEntrySchema = z.object({ + id: z.string().uuid(), + action: z.string(), + changes: z.record(z.string(), z.any()), + created_at: z.string(), + changed_by: z.string().uuid(), + ip_address_hash: z.string().optional(), + user_agent: z.string().optional() +}); + +/** + * Export options schema + */ +export const exportOptionsSchema = z.object({ + include_reviews: z.boolean(), + include_lists: z.boolean(), + include_activity_log: z.boolean(), + include_preferences: z.boolean(), + format: z.literal('json') +}); + +/** + * Export profile data schema + */ +export const exportProfileDataSchema = z.object({ + username: z.string(), + display_name: z.string().nullable(), + bio: z.string().nullable(), + preferred_pronouns: z.string().nullable(), + personal_location: z.string().nullable(), + timezone: z.string(), + preferred_language: z.string(), + theme_preference: z.string(), + privacy_level: z.string(), + created_at: z.string(), + updated_at: z.string() +}); + +/** + * Export review data schema + */ +export const exportReviewDataSchema = z.object({ + id: z.string().uuid(), + rating: z.number().min(1).max(5), + review_text: z.string().nullable(), + ride_name: z.string().optional(), + park_name: z.string().optional(), + created_at: z.string() +}); + +/** + * Export list data schema + */ +export const exportListDataSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + is_public: z.boolean(), + item_count: z.number().int().min(0), + created_at: z.string() +}); + +/** + * Complete export data structure schema + */ +export const exportDataSchema = z.object({ + export_date: z.string(), + user_id: z.string().uuid(), + profile: exportProfileDataSchema, + statistics: userStatisticsSchema, + reviews: z.array(exportReviewDataSchema), + lists: z.array(exportListDataSchema), + activity_log: z.array(activityLogEntrySchema), + preferences: z.object({ + unit_preferences: z.any(), + accessibility_options: z.any(), + notification_preferences: z.any(), + privacy_settings: z.any() + }), + metadata: z.object({ + export_version: z.string(), + data_retention_info: z.string(), + instructions: z.string() + }) +}); + +/** + * Default export options + */ +export const DEFAULT_EXPORT_OPTIONS = { + include_reviews: true, + include_lists: true, + include_activity_log: true, + include_preferences: true, + format: 'json' as const +}; diff --git a/src/types/data-export.ts b/src/types/data-export.ts new file mode 100644 index 00000000..ad52bb04 --- /dev/null +++ b/src/types/data-export.ts @@ -0,0 +1,135 @@ +/** + * Data Export Type Definitions + * + * Types for user data export, statistics, and activity logs. + */ + +/** + * User statistics aggregated from various tables + */ +export interface UserStatistics { + ride_count: number; + coaster_count: number; + park_count: number; + review_count: number; + reputation_score: number; + photo_count: number; + list_count: number; + submission_count: number; + account_created: string; + last_updated: string; +} + +/** + * Individual activity log entry from profile_audit_log + */ +export interface ActivityLogEntry { + id: string; + action: string; + changes: Record; + created_at: string; + changed_by: string; + ip_address_hash?: string; + user_agent?: string; +} + +/** + * Profile data for export + */ +export interface ExportProfileData { + username: string; + display_name: string | null; + bio: string | null; + preferred_pronouns: string | null; + personal_location: string | null; + timezone: string; + preferred_language: string; + theme_preference: string; + privacy_level: string; + created_at: string; + updated_at: string; +} + +/** + * Review data for export + */ +export interface ExportReviewData { + id: string; + rating: number; + review_text: string | null; + ride_name?: string; + park_name?: string; + created_at: string; +} + +/** + * User list data for export + */ +export interface ExportListData { + id: string; + name: string; + description: string | null; + is_public: boolean; + item_count: number; + created_at: string; +} + +/** + * Complete export data structure + */ +export interface ExportDataStructure { + export_date: string; + user_id: string; + profile: ExportProfileData; + statistics: UserStatistics; + reviews: ExportReviewData[]; + lists: ExportListData[]; + activity_log: ActivityLogEntry[]; + preferences: { + unit_preferences: any; + accessibility_options: any; + notification_preferences: any; + privacy_settings: any; + }; + metadata: { + export_version: string; + data_retention_info: string; + instructions: string; + }; +} + +/** + * Export options + */ +export interface ExportOptions { + include_reviews: boolean; + include_lists: boolean; + include_activity_log: boolean; + include_preferences: boolean; + format: 'json'; +} + +/** + * Export progress tracking + */ +export interface ExportProgress { + stage: 'starting' | 'fetching_profile' | 'fetching_reviews' | 'fetching_lists' | 'fetching_activity' | 'packaging' | 'complete'; + progress: number; + message: string; +} + +/** + * Data categories available for export + */ +export type DataCategory = 'profile' | 'reviews' | 'lists' | 'activity_log' | 'preferences'; + +/** + * Export request result + */ +export interface ExportRequestResult { + success: boolean; + data?: ExportDataStructure; + error?: string; + rate_limited?: boolean; + next_available_at?: string; +} diff --git a/supabase/functions/export-user-data/index.ts b/supabase/functions/export-user-data/index.ts new file mode 100644 index 00000000..a91c46c7 --- /dev/null +++ b/supabase/functions/export-user-data/index.ts @@ -0,0 +1,289 @@ +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 corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +interface ExportOptions { + include_reviews: boolean; + include_lists: boolean; + include_activity_log: boolean; + include_preferences: boolean; + format: 'json'; +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseClient = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_ANON_KEY') ?? '', + { + global: { + headers: { Authorization: req.headers.get('Authorization')! }, + }, + } + ); + + // Get authenticated user + const { + data: { user }, + error: authError, + } = await supabaseClient.auth.getUser(); + + if (authError || !user) { + console.error('[Export] Authentication failed:', authError); + return new Response( + JSON.stringify({ error: 'Unauthorized', success: false }), + { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + console.log(`[Export] Processing export request for user: ${user.id}`); + + // Check rate limiting - max 1 export per hour + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); + const { data: recentExports, error: rateLimitError } = await supabaseClient + .from('profile_audit_log') + .select('created_at') + .eq('user_id', user.id) + .eq('action', 'data_exported') + .gte('created_at', oneHourAgo) + .limit(1); + + if (rateLimitError) { + console.error('[Export] Rate limit check failed:', rateLimitError); + } + + if (recentExports && recentExports.length > 0) { + 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}`); + 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, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + // 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' + }; + + console.log('[Export] Export options:', options); + + // Fetch profile data + const { data: profile, error: profileError } = await supabaseClient + .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) { + console.error('[Export] Profile fetch failed:', profileError); + throw new Error('Failed to fetch profile data'); + } + + // Fetch statistics + const { count: photoCount } = await supabaseClient + .from('photos') + .select('*', { count: 'exact', head: true }) + .eq('submitted_by', user.id); + + const { count: listCount } = await supabaseClient + .from('user_lists') + .select('*', { count: 'exact', head: true }) + .eq('user_id', user.id); + + const { count: submissionCount } = await supabaseClient + .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 supabaseClient + .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 supabaseClient + .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 supabaseClient + .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 supabaseClient + .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 supabaseClient + .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 supabaseClient.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 + } + }]); + + console.log(`[Export] Export completed successfully for user ${user.id}`); + + return new Response( + JSON.stringify({ success: true, data: exportData }), + { + status: 200, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json"` + } + } + ); + + } catch (error) { + console.error('[Export] Error:', error); + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : 'An unexpected error occurred', + success: false + }), + { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } +});