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.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 03:03:26 +00:00
parent 7181fdbcac
commit e28dc97d71
8 changed files with 394 additions and 1471 deletions

View File

@@ -1,59 +1,26 @@
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
import { corsHeaders } from '../_shared/cors.ts';
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
import { formatEdgeError } from '../_shared/errorFormatter.ts';
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
import { validateUUID } from '../_shared/typeValidation.ts';
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
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')! } } }
{
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
});
context.span.setAttribute('action', 'mfa_unenroll');
// Phase 1: Check AAL level
const { data: { session } } = await supabaseClient.auth.getSession();
@@ -61,10 +28,9 @@ Deno.serve(async (req) => {
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' } }
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
@@ -72,58 +38,47 @@ Deno.serve(async (req) => {
const { data: roles } = await supabaseClient
.from('user_roles')
.select('role')
.eq('user_id', user.id);
.eq('user_id', context.userId);
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' } }
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
// Phase 4: Check rate limit (2 attempts per 24 hours)
// 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', user.id)
.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) {
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' } }
{ status: 429, headers: { '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' } }
);
}
validateUUID(factorId, 'factorId', { userId: context.userId, requestId: context.requestId });
// Phase 3: Proceed with unenrollment
// Phase 4: 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' } }
);
throw new Error(unenrollError.message);
}
// Audit log the action
const { error: auditError } = await supabaseClient.from('admin_audit_log').insert({
admin_user_id: user.id,
target_user_id: user.id,
await supabaseClient.from('admin_audit_log').insert({
admin_user_id: context.userId,
target_user_id: context.userId,
action: 'mfa_disabled',
details: {
factorId,
@@ -133,15 +88,11 @@ Deno.serve(async (req) => {
}
});
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,
userId: context.userId,
workflowId: 'security-alert',
payload: {
action: 'MFA Disabled',
@@ -151,53 +102,15 @@ Deno.serve(async (req) => {
}
});
} catch (notifError) {
edgeLogger.error('Notification failed', { action: 'mfa_unenroll_notification', userId: user.id });
// Non-blocking notification failure
}
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
}),
JSON.stringify({ success: true }),
{
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
}
headers: { 'Content-Type': 'application/json' }
}
);
}
});
);