import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; 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, x-request-id', }; serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } const tracking = startRequest('process-scheduled-deletions'); try { // Use service role for admin operations const supabaseAdmin = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ); edgeLogger.info('Processing scheduled account deletions', { action: 'scheduled_deletions', requestId: tracking.requestId }); // Find confirmed deletion requests that are past their scheduled date const { data: pendingDeletions, error: fetchError } = await supabaseAdmin .from('account_deletion_requests') .select('*') .eq('status', 'confirmed') .lt('scheduled_deletion_at', new Date().toISOString()); if (fetchError) { throw fetchError; } if (!pendingDeletions || pendingDeletions.length === 0) { edgeLogger.info('No deletions to process', { action: 'scheduled_deletions', requestId: tracking.requestId }); endRequest(tracking, 200); return new Response( JSON.stringify({ success: true, message: 'No deletions to process', processed: 0, requestId: tracking.requestId }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, } ); } edgeLogger.info('Found deletions to process', { action: 'scheduled_deletions', count: pendingDeletions.length, requestId: tracking.requestId }); let successCount = 0; let errorCount = 0; for (const deletion of pendingDeletions) { try { edgeLogger.info('Processing deletion for user', { action: 'scheduled_deletions', userId: deletion.user_id }); // Get user email for confirmation email const { data: userData } = await supabaseAdmin.auth.admin.getUserById(deletion.user_id); const userEmail = userData?.user?.email; // Delete reviews (CASCADE will handle review_photos) await supabaseAdmin .from('reviews') .delete() .eq('user_id', deletion.user_id); // Anonymize submissions and photos await supabaseAdmin .rpc('anonymize_user_submissions', { target_user_id: deletion.user_id }); // Delete user roles await supabaseAdmin .from('user_roles') .delete() .eq('user_id', deletion.user_id); // Get profile to check for avatar before deletion const { data: profile } = await supabaseAdmin .from('profiles') .select('avatar_image_id') .eq('user_id', deletion.user_id) .maybeSingle(); // Delete avatar from Cloudflare Images if it exists if (profile?.avatar_image_id) { const cloudflareAccountId = Deno.env.get('VITE_CLOUDFLARE_ACCOUNT_ID'); const cloudflareApiToken = Deno.env.get('CLOUDFLARE_API_TOKEN'); if (cloudflareAccountId && cloudflareApiToken) { try { edgeLogger.info('Deleting avatar image', { action: 'scheduled_deletions', avatarId: profile.avatar_image_id }); const deleteResponse = await fetch( `https://api.cloudflare.com/client/v4/accounts/${cloudflareAccountId}/images/v1/${profile.avatar_image_id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${cloudflareApiToken}`, }, } ); if (!deleteResponse.ok) { edgeLogger.error('Failed to delete avatar from Cloudflare', { action: 'scheduled_deletions', error: await deleteResponse.text() }); } else { edgeLogger.info('Avatar deleted from Cloudflare successfully', { action: 'scheduled_deletions' }); } } catch (avatarError) { edgeLogger.error('Error deleting avatar from Cloudflare', { action: 'scheduled_deletions', error: avatarError }); } } } // Delete profile await supabaseAdmin .from('profiles') .delete() .eq('user_id', deletion.user_id); // Remove from Novu before deleting auth user try { edgeLogger.info('Removing Novu subscriber', { action: 'scheduled_deletions', userId: deletion.user_id }); const { error: novuError } = await supabaseAdmin.functions.invoke( 'remove-novu-subscriber', { body: { subscriberId: deletion.user_id, deleteSubscriber: true // Also delete the subscriber entirely } } ); if (novuError) { edgeLogger.error('Failed to remove Novu subscriber', { action: 'scheduled_deletions', error: novuError }); } else { edgeLogger.info('Novu subscriber removed successfully', { action: 'scheduled_deletions' }); } } catch (novuError) { // Non-blocking - log but continue with deletion edgeLogger.error('Error removing Novu subscriber', { action: 'scheduled_deletions', error: novuError }); } // Update deletion request status await supabaseAdmin .from('account_deletion_requests') .update({ status: 'completed', completed_at: new Date().toISOString(), }) .eq('id', deletion.id); // Delete auth user await supabaseAdmin.auth.admin.deleteUser(deletion.user_id); // Send final confirmation email if we have the email if (userEmail) { const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY'); const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com'; if (forwardEmailKey) { try { await fetch('https://api.forwardemail.net/v1/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${btoa(forwardEmailKey + ':')}`, }, body: JSON.stringify({ from: fromEmail, to: userEmail, subject: 'Account Deletion Completed', html: `
Your account has been automatically deleted as scheduled on ${new Date().toLocaleDateString()}.
Your profile and reviews have been removed, but your contributions to the database remain preserved.
Thank you for being part of our community.
`, }), }); } catch (emailError) { edgeLogger.error('Failed to send confirmation email', { action: 'scheduled_deletions', error: emailError }); } } } successCount++; edgeLogger.info('Successfully deleted account for user', { action: 'scheduled_deletions', userId: deletion.user_id }); } catch (error) { errorCount++; edgeLogger.error('Failed to delete account for user', { action: 'scheduled_deletions', userId: deletion.user_id, error }); } } edgeLogger.info('Processed deletions', { action: 'scheduled_deletions', successCount, errorCount, requestId: tracking.requestId }); endRequest(tracking, 200); return new Response( JSON.stringify({ success: true, message: `Processed ${successCount} deletion(s)`, processed: successCount, errors: errorCount, requestId: tracking.requestId }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, } ); } catch (error) { edgeLogger.error('Error processing scheduled deletions', { action: 'scheduled_deletions', error: error.message, requestId: tracking.requestId }); endRequest(tracking, 500, error.message); return new Response( JSON.stringify({ error: error.message, requestId: tracking.requestId }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, } ); } });