diff --git a/supabase/functions/cancel-account-deletion/index.ts b/supabase/functions/cancel-account-deletion/index.ts index b10b4710..96d1128d 100644 --- a/supabase/functions/cancel-account-deletion/index.ts +++ b/supabase/functions/cancel-account-deletion/index.ts @@ -1,17 +1,15 @@ -import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; -import { formatEdgeError } from '../_shared/errorFormatter.ts'; +import { edgeLogger } from '../_shared/logger.ts'; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -serve(async (req) => { - const tracking = startRequest(); - - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { +export default createEdgeFunction( + { + name: 'cancel-account-deletion', + requireAuth: true, + corsHeaders: corsHeaders + }, + async (req, context) => { const { cancellation_reason } = await req.json(); const supabaseClient = createClient( @@ -24,23 +22,14 @@ serve(async (req) => { } ); - // Get authenticated user - const { - data: { user }, - error: userError, - } = await supabaseClient.auth.getUser(); - - if (userError || !user) { - throw new Error('Unauthorized'); - } - - edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id, requestId: tracking.requestId }); + context.span.setAttribute('action', 'cancel_deletion'); + edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: context.userId, requestId: context.requestId }); // Find pending or confirmed deletion request const { data: deletionRequest, error: requestError } = await supabaseClient .from('account_deletion_requests') .select('*') - .eq('user_id', user.id) + .eq('user_id', context.userId) .in('status', ['pending', 'confirmed']) .maybeSingle(); @@ -81,7 +70,7 @@ serve(async (req) => { deactivated_at: null, deactivation_reason: null, }) - .eq('user_id', user.id); + .eq('user_id', context.userId); if (profileError) { throw profileError; @@ -90,8 +79,9 @@ serve(async (req) => { // Send cancellation email const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY'); const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com'; + const userEmail = (await supabaseClient.auth.getUser()).data.user?.email; - if (forwardEmailKey) { + if (forwardEmailKey && userEmail) { try { await fetch('https://api.forwardemail.net/v1/emails', { method: 'POST', @@ -101,7 +91,7 @@ serve(async (req) => { }, body: JSON.stringify({ from: fromEmail, - to: user.email, + to: userEmail, subject: 'Account Deletion Cancelled', html: `

Account Deletion Cancelled

@@ -112,35 +102,23 @@ serve(async (req) => { `, }), }); - edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId }); + edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: context.userId, requestId: context.requestId }); } catch (emailError) { - edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id, requestId: tracking.requestId }); + edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: context.userId, requestId: context.requestId }); } } - const duration = endRequest(tracking); - edgeLogger.info('Deletion cancelled successfully', { action: 'cancel_deletion_success', userId: user.id, requestId: tracking.requestId, duration }); + edgeLogger.info('Deletion cancelled successfully', { action: 'cancel_deletion_success', userId: context.userId, requestId: context.requestId }); return new Response( JSON.stringify({ success: true, message: 'Account deletion cancelled successfully', - requestId: tracking.requestId }), { status: 200, - headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, - } - ); - } catch (error) { - const duration = endRequest(tracking); - edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: formatEdgeError(error), requestId: tracking.requestId, duration }); - return new Response( - JSON.stringify({ error: error.message, requestId: tracking.requestId }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, + headers: { 'Content-Type': 'application/json' }, } ); } -}); +); diff --git a/supabase/functions/cancel-email-change/index.ts b/supabase/functions/cancel-email-change/index.ts index a1b52fd8..4b8f96f7 100644 --- a/supabase/functions/cancel-email-change/index.ts +++ b/supabase/functions/cancel-email-change/index.ts @@ -1,36 +1,21 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; -import { formatEdgeError } from '../_shared/errorFormatter.ts'; +import { edgeLogger } from '../_shared/logger.ts'; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -Deno.serve(async (req) => { - const tracking = startRequest(); - - // Handle CORS preflight requests - if (req.method === 'OPTIONS') { - return new Response(null, { - headers: { - ...corsHeaders, - 'X-Request-ID': tracking.requestId - } +export default createEdgeFunction( + { + name: 'cancel-email-change', + requireAuth: true, + corsHeaders: corsHeaders + }, + async (req, context) => { + context.span.setAttribute('action', 'cancel_email_change'); + edgeLogger.info('Cancelling email change for user', { + action: 'cancel_email_change', + requestId: context.requestId, + userId: context.userId }); - } - - try { - // Get the user from the authorization header - const authHeader = req.headers.get('Authorization'); - if (!authHeader) { - const duration = endRequest(tracking); - edgeLogger.error('Missing authorization header', { - action: 'cancel_email_change', - requestId: tracking.requestId, - duration - }); - throw new Error('No authorization header provided. Please ensure you are logged in.'); - } - - // Extract the JWT token from the Authorization header - const token = authHeader.replace('Bearer ', ''); // SECURITY: Service Role Key Usage // --------------------------------- @@ -38,7 +23,7 @@ Deno.serve(async (req) => { // This is required because: // 1. The cancel_user_email_change() database function has SECURITY DEFINER privileges // 2. It needs to modify auth.users table which is not accessible with regular user tokens - // 3. User authentication is still verified via JWT token (passed to getUser()) + // 3. User authentication is verified via the wrapper's requireAuth // Scope: Limited to cancelling the authenticated user's own email change const supabaseUrl = Deno.env.get('SUPABASE_URL'); const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'); @@ -54,50 +39,28 @@ Deno.serve(async (req) => { } }); - // Verify the user's JWT token by passing it explicitly to getUser() - // Note: verify_jwt = true in config.toml means Supabase has already validated the JWT - const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser(token); - - if (authError || !user) { - const duration = endRequest(tracking); - edgeLogger.error('Auth verification failed', { - action: 'cancel_email_change', - requestId: tracking.requestId, - duration, - error: authError - }); - throw new Error('Invalid session token. Please refresh the page and try again.'); - } - - const userId = user.id; - edgeLogger.info('Cancelling email change for user', { - action: 'cancel_email_change', - requestId: tracking.requestId, - userId - }); - // Call the database function to clear email change fields // This function has SECURITY DEFINER privileges to access auth.users const { data: cancelled, error: cancelError } = await supabaseAdmin - .rpc('cancel_user_email_change', { _user_id: userId }); + .rpc('cancel_user_email_change', { _user_id: context.userId }); if (cancelError || !cancelled) { edgeLogger.error('Error cancelling email change', { error: cancelError?.message, - userId, - requestId: tracking.requestId + userId: context.userId, + requestId: context.requestId }); throw new Error('Unable to cancel email change: ' + (cancelError?.message || 'Unknown error')); } - edgeLogger.info('Successfully cancelled email change', { userId, requestId: tracking.requestId }); + edgeLogger.info('Successfully cancelled email change', { userId: context.userId, requestId: context.requestId }); // Log the cancellation in admin_audit_log const { error: auditError } = await supabaseAdmin .from('admin_audit_log') .insert({ - admin_user_id: userId, - target_user_id: userId, + admin_user_id: context.userId, + target_user_id: context.userId, action: 'email_change_cancelled', details: { cancelled_at: new Date().toISOString(), @@ -107,17 +70,15 @@ Deno.serve(async (req) => { if (auditError) { edgeLogger.error('Error logging audit', { error: auditError.message, - requestId: tracking.requestId + requestId: context.requestId }); // Don't fail the request if audit logging fails } - const duration = endRequest(tracking); edgeLogger.info('Successfully cancelled email change', { action: 'cancel_email_change', - requestId: tracking.requestId, - userId, - duration + requestId: context.requestId, + userId: context.userId }); return new Response( @@ -125,43 +86,17 @@ Deno.serve(async (req) => { success: true, message: 'Email change cancelled successfully', user: { - id: userId, + id: context.userId, email: null, new_email: null, }, - requestId: tracking.requestId, }), { headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId + 'Content-Type': 'application/json' }, status: 200, } ); - } catch (error) { - const duration = endRequest(tracking); - edgeLogger.error('Error in cancel-email-change function', { - action: 'cancel_email_change', - requestId: tracking.requestId, - duration, - error: formatEdgeError(error) - }); - return new Response( - JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : 'An unknown error occurred', - requestId: tracking.requestId, - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 400, - } - ); } -}); +);