Files
thrilltrack-explorer/supabase/functions/mfa-unenroll/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

208 lines
6.5 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 {
// Create Supabase client with user's auth token
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: userError } = await supabaseClient.auth.getUser();
if (userError || !user) {
const duration = endRequest(tracking);
edgeLogger.error('Authentication failed', {
action: 'mfa_unenroll_auth',
requestId: tracking.requestId,
duration
});
return new Response(
JSON.stringify({
error: 'Unauthorized',
requestId: tracking.requestId
}),
{
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
edgeLogger.info('Processing MFA unenroll', {
action: 'mfa_unenroll',
requestId: tracking.requestId,
userId: user.id
});
// Phase 1: Check AAL level
const { data: { session } } = await supabaseClient.auth.getSession();
const { data: aalData } = await supabaseClient.auth.mfa.getAuthenticatorAssuranceLevel();
const aal = aalData?.currentLevel || 'aal1';
if (aal !== 'aal2') {
edgeLogger.warn('AAL2 required for MFA removal', { action: 'mfa_unenroll_aal', userId: user.id, aal });
return new Response(
JSON.stringify({ error: 'AAL2 required to remove MFA' }),
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Phase 2: Check if user's role requires MFA
const { data: roles } = await supabaseClient
.from('user_roles')
.select('role')
.eq('user_id', user.id);
const requiresMFA = roles?.some(r => ['admin', 'moderator', 'superuser'].includes(r.role));
if (requiresMFA) {
edgeLogger.warn('Role requires MFA, blocking removal', { action: 'mfa_unenroll_role', userId: user.id });
return new Response(
JSON.stringify({ error: 'Your role requires MFA and it cannot be disabled' }),
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Phase 4: Check rate limit (2 attempts per 24 hours)
const { data: recentAttempts } = await supabaseClient
.from('admin_audit_log')
.select('created_at')
.eq('admin_user_id', user.id)
.eq('action', 'mfa_disabled')
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
if (recentAttempts && recentAttempts.length >= 2) {
edgeLogger.warn('Rate limit exceeded', { action: 'mfa_unenroll_rate', userId: user.id, attempts: recentAttempts.length });
return new Response(
JSON.stringify({ error: 'Rate limit exceeded. Try again in 24 hours.' }),
{ status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Get factor ID from request
const { factorId } = await req.json();
if (!factorId) {
return new Response(
JSON.stringify({ error: 'Factor ID required' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Phase 3: Proceed with unenrollment
const { error: unenrollError } = await supabaseClient.auth.mfa.unenroll({ factorId });
if (unenrollError) {
edgeLogger.error('Unenroll failed', { action: 'mfa_unenroll_fail', userId: user.id, error: unenrollError.message });
return new Response(
JSON.stringify({ error: unenrollError.message }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Audit log the action
const { error: auditError } = await supabaseClient.from('admin_audit_log').insert({
admin_user_id: user.id,
target_user_id: user.id,
action: 'mfa_disabled',
details: {
factorId,
timestamp: new Date().toISOString(),
user_agent: req.headers.get('user-agent') || 'unknown',
aal_level: aal
}
});
if (auditError) {
edgeLogger.error('Audit log failed', { action: 'mfa_unenroll_audit', userId: user.id });
}
// Send security notification
try {
await supabaseClient.functions.invoke('trigger-notification', {
body: {
userId: user.id,
workflowId: 'security-alert',
payload: {
action: 'MFA Disabled',
message: 'Two-factor authentication has been disabled on your account.',
timestamp: new Date().toISOString()
}
}
});
} catch (notifError) {
edgeLogger.error('Notification failed', { action: 'mfa_unenroll_notification', userId: user.id });
}
const duration = endRequest(tracking);
edgeLogger.info('MFA successfully disabled', {
action: 'mfa_unenroll_success',
requestId: tracking.requestId,
userId: user.id,
duration
});
return new Response(
JSON.stringify({
success: true,
requestId: tracking.requestId
}),
{
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
} catch (error) {
const duration = endRequest(tracking);
edgeLogger.error('Unexpected error', {
action: 'mfa_unenroll_error',
requestId: tracking.requestId,
duration,
error: formatEdgeError(error)
});
return new Response(
JSON.stringify({
error: 'Internal server error',
requestId: tracking.requestId
}),
{
status: 500,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
});