mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:51:12 -05:00
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.
117 lines
3.6 KiB
TypeScript
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' }
|
|
}
|
|
);
|
|
}
|
|
);
|