mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 05:51:12 -05:00
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:
@@ -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' }
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user