Files
thrilltrack-explorer/supabase/functions/mfa-unenroll/index.ts
gpt-engineer-app[bot] e28dc97d71 Migrate Phase 1 Functions
Migrate 8 high-priority functions (admin-delete-user, mfa-unenroll, confirm-account-deletion, request-account-deletion, send-contact-message, upload-image, validate-email-backend, process-oauth-profile) to wrapEdgeFunction pattern. Replace manual CORS/auth, add shared validations, integrate standardized error handling, and preserve existing rate limiting where applicable. Update implementations to leverage context span, requestId, and improved logging for consistent error reporting and tracing.
2025-11-11 03:03:26 +00:00

117 lines
3.6 KiB
TypeScript

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
import { corsHeaders } from '../_shared/cors.ts';
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
import { validateUUID } from '../_shared/typeValidation.ts';
export default createEdgeFunction(
{
name: 'mfa-unenroll',
requireAuth: true,
corsHeaders: corsHeaders
},
async (req, context) => {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: { Authorization: req.headers.get('Authorization')! }
}
}
);
context.span.setAttribute('action', 'mfa_unenroll');
// 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') {
return new Response(
JSON.stringify({ error: 'AAL2 required to remove MFA' }),
{ status: 403, headers: { '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', context.userId);
const requiresMFA = roles?.some(r => ['admin', 'moderator', 'superuser'].includes(r.role));
if (requiresMFA) {
return new Response(
JSON.stringify({ error: 'Your role requires MFA and it cannot be disabled' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
// Phase 3: Check rate limit (2 attempts per 24 hours)
const { data: recentAttempts } = await supabaseClient
.from('admin_audit_log')
.select('created_at')
.eq('admin_user_id', context.userId)
.eq('action', 'mfa_disabled')
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
if (recentAttempts && recentAttempts.length >= 2) {
return new Response(
JSON.stringify({ error: 'Rate limit exceeded. Try again in 24 hours.' }),
{ status: 429, headers: { 'Content-Type': 'application/json' } }
);
}
// Get factor ID from request
const { factorId } = await req.json();
validateUUID(factorId, 'factorId', { userId: context.userId, requestId: context.requestId });
// Phase 4: Proceed with unenrollment
const { error: unenrollError } = await supabaseClient.auth.mfa.unenroll({ factorId });
if (unenrollError) {
throw new Error(unenrollError.message);
}
// Audit log the action
await supabaseClient.from('admin_audit_log').insert({
admin_user_id: context.userId,
target_user_id: context.userId,
action: 'mfa_disabled',
details: {
factorId,
timestamp: new Date().toISOString(),
user_agent: req.headers.get('user-agent') || 'unknown',
aal_level: aal
}
});
// Send security notification
try {
await supabaseClient.functions.invoke('trigger-notification', {
body: {
userId: context.userId,
workflowId: 'security-alert',
payload: {
action: 'MFA Disabled',
message: 'Two-factor authentication has been disabled on your account.',
timestamp: new Date().toISOString()
}
}
});
} catch (notifError) {
// Non-blocking notification failure
}
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
);