Files
thrilltrack-explorer/supabase/functions/cancel-email-change/index.ts
gpt-engineer-app[bot] 2d65f13b85 Connect to Lovable Cloud
Add centralized errorFormatter to convert various error types into readable messages, and apply it across edge functions. Replace String(error) usage with formatEdgeError, update relevant imports, fix a throw to use toError, and enhance logger to log formatted errors. Includes new errorFormatter.ts and widespread updates to 18+ edge functions plus logger integration.
2025-11-10 18:09:15 +00:00

172 lines
5.4 KiB
TypeScript

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
import { formatEdgeError } from '../_shared/errorFormatter.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
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
}
});
}
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
// ---------------------------------
// This function uses the service role key to bypass RLS and access auth.users table.
// 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())
// 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');
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('Missing Supabase configuration');
}
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
// 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 });
if (cancelError || !cancelled) {
edgeLogger.error('Error cancelling email change', {
error: cancelError?.message,
userId,
requestId: tracking.requestId
});
throw new Error('Unable to cancel email change: ' + (cancelError?.message || 'Unknown error'));
}
edgeLogger.info('Successfully cancelled email change', { userId, requestId: tracking.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,
action: 'email_change_cancelled',
details: {
cancelled_at: new Date().toISOString(),
},
});
if (auditError) {
edgeLogger.error('Error logging audit', {
error: auditError.message,
requestId: tracking.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
});
return new Response(
JSON.stringify({
success: true,
message: 'Email change cancelled successfully',
user: {
id: userId,
email: null,
new_email: null,
},
requestId: tracking.requestId,
}),
{
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
},
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,
}
);
}
});