Files
thrilltrack-explorer/supabase/functions/mfa-unenroll/index.ts
2025-10-17 19:50:29 +00:00

147 lines
5.0 KiB
TypeScript

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',
};
Deno.serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
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) {
console.error('Authentication failed:', userError);
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
console.log('[mfa-unenroll] Processing request for user:', user.id);
// Phase 1: Check AAL level
const { data: { session } } = await supabaseClient.auth.getSession();
const aal = session?.aal || 'aal1';
if (aal !== 'aal2') {
console.warn('[mfa-unenroll] AAL2 required, current:', 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) {
console.warn('[mfa-unenroll] Role requires MFA, blocking removal');
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) {
console.warn('[mfa-unenroll] Rate limit exceeded:', recentAttempts.length, 'attempts');
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) {
console.error('[mfa-unenroll] Unenroll failed:', unenrollError);
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) {
console.error('[mfa-unenroll] Audit log failed:', auditError);
}
// 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) {
console.error('[mfa-unenroll] Notification failed:', notifError);
}
console.log('[mfa-unenroll] MFA successfully disabled for user:', user.id);
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[mfa-unenroll] Unexpected error:', error);
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});