mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 17:31:12 -05:00
Add edge function logging
This commit is contained in:
87
supabase/functions/_shared/logger.ts
Normal file
87
supabase/functions/_shared/logger.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Structured logging utility for edge functions
|
||||||
|
* Prevents sensitive data exposure and provides consistent log format
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
|
||||||
|
interface LogContext {
|
||||||
|
userId?: string;
|
||||||
|
action?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields that should never be logged
|
||||||
|
const SENSITIVE_FIELDS = [
|
||||||
|
'password',
|
||||||
|
'token',
|
||||||
|
'secret',
|
||||||
|
'api_key',
|
||||||
|
'apikey',
|
||||||
|
'authorization',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'ssn',
|
||||||
|
'credit_card',
|
||||||
|
'ip_address',
|
||||||
|
'session_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize context to remove sensitive data
|
||||||
|
*/
|
||||||
|
function sanitizeContext(context: LogContext): LogContext {
|
||||||
|
const sanitized: LogContext = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(context)) {
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
|
||||||
|
// Skip sensitive fields
|
||||||
|
if (SENSITIVE_FIELDS.some(field => lowerKey.includes(field))) {
|
||||||
|
sanitized[key] = '[REDACTED]';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively sanitize objects
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
sanitized[key] = sanitizeContext(value as LogContext);
|
||||||
|
} else {
|
||||||
|
sanitized[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format log message with context
|
||||||
|
*/
|
||||||
|
function formatLog(level: LogLevel, message: string, context?: LogContext): string {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const sanitizedContext = context ? sanitizeContext(context) : {};
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
timestamp,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
...sanitizedContext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const edgeLogger = {
|
||||||
|
info: (message: string, context?: LogContext): void => {
|
||||||
|
console.info(formatLog('info', message, context));
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: (message: string, context?: LogContext): void => {
|
||||||
|
console.warn(formatLog('warn', message, context));
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (message: string, context?: LogContext): void => {
|
||||||
|
console.error(formatLog('error', message, context));
|
||||||
|
},
|
||||||
|
|
||||||
|
debug: (message: string, context?: LogContext): void => {
|
||||||
|
console.debug(formatLog('debug', message, context));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||||
|
import { edgeLogger } from '../_shared/logger.ts';
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -34,7 +35,7 @@ serve(async (req) => {
|
|||||||
throw new Error('Unauthorized');
|
throw new Error('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Cancelling deletion request for user: ${user.id}`);
|
edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id });
|
||||||
|
|
||||||
// Find pending deletion request
|
// Find pending deletion request
|
||||||
const { data: deletionRequest, error: requestError } = await supabaseClient
|
const { data: deletionRequest, error: requestError } = await supabaseClient
|
||||||
@@ -100,9 +101,9 @@ serve(async (req) => {
|
|||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
console.log('Cancellation confirmation email sent');
|
edgeLogger.info('Cancellation confirmation email sent', { action: 'cancel_deletion_email', userId: user.id });
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
console.error('Failed to send email:', emailError);
|
edgeLogger.error('Failed to send email', { action: 'cancel_deletion_email', userId: user.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ serve(async (req) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cancelling deletion:', error);
|
edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error) });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: error.message }),
|
JSON.stringify({ error: error.message }),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||||
import { sanitizeError } from '../_shared/errorSanitizer.ts';
|
import { sanitizeError } from '../_shared/errorSanitizer.ts';
|
||||||
|
import { edgeLogger } from '../_shared/logger.ts';
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -39,14 +40,14 @@ serve(async (req) => {
|
|||||||
} = await supabaseClient.auth.getUser();
|
} = await supabaseClient.auth.getUser();
|
||||||
|
|
||||||
if (authError || !user) {
|
if (authError || !user) {
|
||||||
console.error('[Export] Authentication failed:', authError);
|
edgeLogger.error('Authentication failed', { action: 'export_auth' });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Unauthorized', success: false }),
|
JSON.stringify({ error: 'Unauthorized', success: false }),
|
||||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Export] Processing export request for user: ${user.id}`);
|
edgeLogger.info('Processing export request', { action: 'export_start', userId: user.id });
|
||||||
|
|
||||||
// Check rate limiting - max 1 export per hour
|
// Check rate limiting - max 1 export per hour
|
||||||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
||||||
@@ -86,7 +87,7 @@ serve(async (req) => {
|
|||||||
format: 'json'
|
format: 'json'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Export] Export options:', options);
|
edgeLogger.info('Export options', { action: 'export_options', userId: user.id });
|
||||||
|
|
||||||
// Fetch profile data
|
// Fetch profile data
|
||||||
const { data: profile, error: profileError } = await supabaseClient
|
const { data: profile, error: profileError } = await supabaseClient
|
||||||
@@ -96,7 +97,7 @@ serve(async (req) => {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (profileError) {
|
if (profileError) {
|
||||||
console.error('[Export] Profile fetch failed:', profileError);
|
edgeLogger.error('Profile fetch failed', { action: 'export_profile' });
|
||||||
throw new Error('Failed to fetch profile data');
|
throw new Error('Failed to fetch profile data');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +264,7 @@ serve(async (req) => {
|
|||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
console.log(`[Export] Export completed successfully for user ${user.id}`);
|
edgeLogger.info('Export completed successfully', { action: 'export_complete', userId: user.id });
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ success: true, data: exportData }),
|
JSON.stringify({ success: true, data: exportData }),
|
||||||
@@ -278,7 +279,7 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Export] Error:', error);
|
edgeLogger.error('Export error', { action: 'export_error', error: error instanceof Error ? error.message : String(error) });
|
||||||
const sanitized = sanitizeError(error, 'export-user-data');
|
const sanitized = sanitizeError(error, 'export-user-data');
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||||
|
import { edgeLogger } from '../_shared/logger.ts';
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -22,21 +23,21 @@ Deno.serve(async (req) => {
|
|||||||
// Get authenticated user
|
// Get authenticated user
|
||||||
const { data: { user }, error: userError } = await supabaseClient.auth.getUser();
|
const { data: { user }, error: userError } = await supabaseClient.auth.getUser();
|
||||||
if (userError || !user) {
|
if (userError || !user) {
|
||||||
console.error('Authentication failed:', userError);
|
edgeLogger.error('Authentication failed', { action: 'mfa_unenroll_auth' });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Unauthorized' }),
|
JSON.stringify({ error: 'Unauthorized' }),
|
||||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[mfa-unenroll] Processing request for user:', user.id);
|
edgeLogger.info('Processing MFA unenroll', { action: 'mfa_unenroll', userId: user.id });
|
||||||
|
|
||||||
// Phase 1: Check AAL level
|
// Phase 1: Check AAL level
|
||||||
const { data: { session } } = await supabaseClient.auth.getSession();
|
const { data: { session } } = await supabaseClient.auth.getSession();
|
||||||
const aal = session?.aal || 'aal1';
|
const aal = session?.aal || 'aal1';
|
||||||
|
|
||||||
if (aal !== 'aal2') {
|
if (aal !== 'aal2') {
|
||||||
console.warn('[mfa-unenroll] AAL2 required, current:', aal);
|
edgeLogger.warn('AAL2 required for MFA removal', { action: 'mfa_unenroll_aal', userId: user.id, aal });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'AAL2 required to remove MFA' }),
|
JSON.stringify({ error: 'AAL2 required to remove MFA' }),
|
||||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
@@ -52,7 +53,7 @@ Deno.serve(async (req) => {
|
|||||||
const requiresMFA = roles?.some(r => ['admin', 'moderator', 'superuser'].includes(r.role));
|
const requiresMFA = roles?.some(r => ['admin', 'moderator', 'superuser'].includes(r.role));
|
||||||
|
|
||||||
if (requiresMFA) {
|
if (requiresMFA) {
|
||||||
console.warn('[mfa-unenroll] Role requires MFA, blocking removal');
|
edgeLogger.warn('Role requires MFA, blocking removal', { action: 'mfa_unenroll_role', userId: user.id });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Your role requires MFA and it cannot be disabled' }),
|
JSON.stringify({ error: 'Your role requires MFA and it cannot be disabled' }),
|
||||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
@@ -68,7 +69,7 @@ Deno.serve(async (req) => {
|
|||||||
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
|
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
|
||||||
|
|
||||||
if (recentAttempts && recentAttempts.length >= 2) {
|
if (recentAttempts && recentAttempts.length >= 2) {
|
||||||
console.warn('[mfa-unenroll] Rate limit exceeded:', recentAttempts.length, 'attempts');
|
edgeLogger.warn('Rate limit exceeded', { action: 'mfa_unenroll_rate', userId: user.id, attempts: recentAttempts.length });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Rate limit exceeded. Try again in 24 hours.' }),
|
JSON.stringify({ error: 'Rate limit exceeded. Try again in 24 hours.' }),
|
||||||
{ status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
@@ -88,7 +89,7 @@ Deno.serve(async (req) => {
|
|||||||
const { error: unenrollError } = await supabaseClient.auth.mfa.unenroll({ factorId });
|
const { error: unenrollError } = await supabaseClient.auth.mfa.unenroll({ factorId });
|
||||||
|
|
||||||
if (unenrollError) {
|
if (unenrollError) {
|
||||||
console.error('[mfa-unenroll] Unenroll failed:', unenrollError);
|
edgeLogger.error('Unenroll failed', { action: 'mfa_unenroll_fail', userId: user.id, error: unenrollError.message });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: unenrollError.message }),
|
JSON.stringify({ error: unenrollError.message }),
|
||||||
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
@@ -109,7 +110,7 @@ Deno.serve(async (req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (auditError) {
|
if (auditError) {
|
||||||
console.error('[mfa-unenroll] Audit log failed:', auditError);
|
edgeLogger.error('Audit log failed', { action: 'mfa_unenroll_audit', userId: user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send security notification
|
// Send security notification
|
||||||
@@ -126,10 +127,10 @@ Deno.serve(async (req) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (notifError) {
|
} catch (notifError) {
|
||||||
console.error('[mfa-unenroll] Notification failed:', notifError);
|
edgeLogger.error('Notification failed', { action: 'mfa_unenroll_notification', userId: user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[mfa-unenroll] MFA successfully disabled for user:', user.id);
|
edgeLogger.info('MFA successfully disabled', { action: 'mfa_unenroll_success', userId: user.id });
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ success: true }),
|
JSON.stringify({ success: true }),
|
||||||
@@ -137,7 +138,7 @@ Deno.serve(async (req) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[mfa-unenroll] Unexpected error:', error);
|
edgeLogger.error('Unexpected error', { action: 'mfa_unenroll_error', error: error instanceof Error ? error.message : String(error) });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Internal server error' }),
|
JSON.stringify({ error: 'Internal server error' }),
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
|
import { edgeLogger } from '../_shared/logger.ts';
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -18,7 +19,7 @@ serve(async (req) => {
|
|||||||
|
|
||||||
const event = await req.json();
|
const event = await req.json();
|
||||||
|
|
||||||
console.log('Received Novu webhook event:', event.type);
|
edgeLogger.info('Received Novu webhook event', { action: 'novu_webhook', eventType: event.type });
|
||||||
|
|
||||||
// Handle different webhook events
|
// Handle different webhook events
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -35,7 +36,7 @@ serve(async (req) => {
|
|||||||
await handleNotificationFailed(supabase, event);
|
await handleNotificationFailed(supabase, event);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unhandled event type:', event.type);
|
edgeLogger.warn('Unhandled Novu event type', { action: 'novu_webhook', eventType: event.type });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -46,7 +47,7 @@ serve(async (req) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error processing webhook:', error);
|
edgeLogger.error('Error processing webhook', { action: 'novu_webhook', error: error?.message });
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
import { validateEntityData, validateEntityDataStrict } from "./validation.ts";
|
import { validateEntityData, validateEntityDataStrict } from "./validation.ts";
|
||||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||||
|
import { edgeLogger } from "../_shared/logger.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -70,16 +71,12 @@ serve(async (req) => {
|
|||||||
// Verify JWT and get authenticated user
|
// Verify JWT and get authenticated user
|
||||||
const { data: { user }, error: authError } = await supabaseAuth.auth.getUser();
|
const { data: { user }, error: authError } = await supabaseAuth.auth.getUser();
|
||||||
|
|
||||||
console.log('[AUTH] User auth result:', {
|
edgeLogger.info('User auth check', { action: 'approval_auth', hasUser: !!user, userId: user?.id });
|
||||||
hasUser: !!user,
|
|
||||||
userId: user?.id,
|
|
||||||
hasError: !!authError
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authError || !user) {
|
if (authError || !user) {
|
||||||
console.error('[AUTH] Auth verification failed:', {
|
edgeLogger.error('Auth verification failed', {
|
||||||
error: authError?.message,
|
action: 'approval_auth',
|
||||||
code: authError?.code
|
error: authError?.message
|
||||||
});
|
});
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -91,7 +88,7 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AUTH] Authentication successful:', user.id);
|
edgeLogger.info('Authentication successful', { action: 'approval_auth_success', userId: user.id });
|
||||||
|
|
||||||
// SECURITY NOTE: Service role key used later in this function
|
// SECURITY NOTE: Service role key used later in this function
|
||||||
// Reason: Need to bypass RLS to write approved changes to entity tables
|
// Reason: Need to bypass RLS to write approved changes to entity tables
|
||||||
@@ -112,13 +109,10 @@ serve(async (req) => {
|
|||||||
.select('role')
|
.select('role')
|
||||||
.eq('user_id', authenticatedUserId);
|
.eq('user_id', authenticatedUserId);
|
||||||
|
|
||||||
console.log('[ROLE_CHECK] Query result:', {
|
edgeLogger.info('Role check query result', { action: 'approval_role_check', userId: authenticatedUserId, rolesCount: roles?.length });
|
||||||
rolesCount: roles?.length,
|
|
||||||
error: rolesError?.message
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rolesError) {
|
if (rolesError) {
|
||||||
console.error('[ROLE_CHECK] Failed:', { error: rolesError.message });
|
edgeLogger.error('Role check failed', { action: 'approval_role_check', error: rolesError.message });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Failed to verify user permissions.' }),
|
JSON.stringify({ error: 'Failed to verify user permissions.' }),
|
||||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
@@ -130,17 +124,17 @@ serve(async (req) => {
|
|||||||
userRoles.includes('admin') ||
|
userRoles.includes('admin') ||
|
||||||
userRoles.includes('superuser');
|
userRoles.includes('superuser');
|
||||||
|
|
||||||
console.log('[ROLE_CHECK] Result:', { isModerator, userId: authenticatedUserId });
|
edgeLogger.info('Role check result', { action: 'approval_role_result', userId: authenticatedUserId, isModerator });
|
||||||
|
|
||||||
if (!isModerator) {
|
if (!isModerator) {
|
||||||
console.error('[ROLE_CHECK] Insufficient permissions');
|
edgeLogger.error('Insufficient permissions', { action: 'approval_role_insufficient', userId: authenticatedUserId });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }),
|
JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }),
|
||||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ROLE_CHECK] User is moderator');
|
edgeLogger.info('User is moderator', { action: 'approval_role_verified', userId: authenticatedUserId });
|
||||||
|
|
||||||
// Phase 2: AAL2 Enforcement - Check if user has MFA enrolled and requires AAL2
|
// Phase 2: AAL2 Enforcement - Check if user has MFA enrolled and requires AAL2
|
||||||
// Parse JWT directly from Authorization header to get AAL level
|
// Parse JWT directly from Authorization header to get AAL level
|
||||||
@@ -148,17 +142,17 @@ serve(async (req) => {
|
|||||||
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
||||||
const aal = payload.aal || 'aal1';
|
const aal = payload.aal || 'aal1';
|
||||||
|
|
||||||
console.log('[AAL_CHECK] Session AAL level:', { aal, userId: authenticatedUserId });
|
edgeLogger.info('Session AAL level', { action: 'approval_aal_check', userId: authenticatedUserId, aal });
|
||||||
|
|
||||||
// Check if user has MFA enrolled
|
// Check if user has MFA enrolled
|
||||||
const { data: factorsData } = await supabaseAuth.auth.mfa.listFactors();
|
const { data: factorsData } = await supabaseAuth.auth.mfa.listFactors();
|
||||||
const hasMFA = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
const hasMFA = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
||||||
|
|
||||||
console.log('[MFA_CHECK] MFA status:', { hasMFA, userId: authenticatedUserId });
|
edgeLogger.info('MFA status', { action: 'approval_mfa_check', userId: authenticatedUserId, hasMFA });
|
||||||
|
|
||||||
// Enforce AAL2 if MFA is enrolled
|
// Enforce AAL2 if MFA is enrolled
|
||||||
if (hasMFA && aal !== 'aal2') {
|
if (hasMFA && aal !== 'aal2') {
|
||||||
console.error('[AAL_CHECK] AAL2 required but session is at AAL1');
|
edgeLogger.error('AAL2 required but session is at AAL1', { action: 'approval_aal_violation', userId: authenticatedUserId });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: 'MFA verification required',
|
error: 'MFA verification required',
|
||||||
@@ -169,7 +163,7 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AAL_CHECK] AAL2 check passed:', { userId: authenticatedUserId, hasMFA, aal });
|
edgeLogger.info('AAL2 check passed', { action: 'approval_aal_pass', userId: authenticatedUserId, hasMFA, aal });
|
||||||
|
|
||||||
const { itemIds, submissionId }: ApprovalRequest = await req.json();
|
const { itemIds, submissionId }: ApprovalRequest = await req.json();
|
||||||
|
|
||||||
@@ -206,7 +200,7 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[APPROVAL] Processing selective approval:', { itemIds, userId: authenticatedUserId, submissionId });
|
edgeLogger.info('Processing selective approval', { action: 'approval_start', itemCount: itemIds.length, userId: authenticatedUserId, submissionId });
|
||||||
|
|
||||||
// Fetch all items for the submission
|
// Fetch all items for the submission
|
||||||
const { data: items, error: fetchError } = await supabase
|
const { data: items, error: fetchError } = await supabase
|
||||||
@@ -237,11 +231,12 @@ serve(async (req) => {
|
|||||||
sortedItems = topologicalSort(items);
|
sortedItems = topologicalSort(items);
|
||||||
} catch (sortError: unknown) {
|
} catch (sortError: unknown) {
|
||||||
const errorMessage = sortError instanceof Error ? sortError.message : 'Failed to sort items';
|
const errorMessage = sortError instanceof Error ? sortError.message : 'Failed to sort items';
|
||||||
console.error('[APPROVAL ERROR] Topological sort failed:', {
|
edgeLogger.error('Topological sort failed', {
|
||||||
|
action: 'approval_sort_fail',
|
||||||
submissionId,
|
submissionId,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
error: errorMessage,
|
userId: authenticatedUserId,
|
||||||
userId: authenticatedUserId
|
error: errorMessage
|
||||||
});
|
});
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -266,13 +261,14 @@ serve(async (req) => {
|
|||||||
// Process items in order
|
// Process items in order
|
||||||
for (const item of sortedItems) {
|
for (const item of sortedItems) {
|
||||||
try {
|
try {
|
||||||
console.log('[APPROVAL] Processing item:', { itemId: item.id, itemType: item.item_type });
|
edgeLogger.info('Processing item', { action: 'approval_process_item', itemId: item.id, itemType: item.item_type });
|
||||||
|
|
||||||
// Validate entity data with strict validation, passing original_data for edits
|
// Validate entity data with strict validation, passing original_data for edits
|
||||||
const validation = validateEntityDataStrict(item.item_type, item.item_data, item.original_data);
|
const validation = validateEntityDataStrict(item.item_type, item.item_data, item.original_data);
|
||||||
|
|
||||||
if (validation.blockingErrors.length > 0) {
|
if (validation.blockingErrors.length > 0) {
|
||||||
console.error('[APPROVAL] Blocking validation errors:', {
|
edgeLogger.error('Blocking validation errors', {
|
||||||
|
action: 'approval_validation_fail',
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
errors: validation.blockingErrors
|
errors: validation.blockingErrors
|
||||||
});
|
});
|
||||||
@@ -291,7 +287,8 @@ serve(async (req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validation.warnings.length > 0) {
|
if (validation.warnings.length > 0) {
|
||||||
console.warn('[APPROVAL] Validation warnings:', {
|
edgeLogger.warn('Validation warnings', {
|
||||||
|
action: 'approval_validation_warning',
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
warnings: validation.warnings
|
warnings: validation.warnings
|
||||||
});
|
});
|
||||||
@@ -378,15 +375,16 @@ serve(async (req) => {
|
|||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[APPROVAL SUCCESS]', { itemId: item.id, entityId, itemType: item.item_type });
|
edgeLogger.info('Item approval success', { action: 'approval_item_success', itemId: item.id, entityId, itemType: item.item_type });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
console.error('[APPROVAL ERROR] Item approval failed:', {
|
edgeLogger.error('Item approval failed', {
|
||||||
|
action: 'approval_item_fail',
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
itemType: item.item_type,
|
itemType: item.item_type,
|
||||||
error: errorMessage,
|
|
||||||
userId: authenticatedUserId,
|
userId: authenticatedUserId,
|
||||||
submissionId
|
submissionId,
|
||||||
|
error: errorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDependencyError = error instanceof Error && (
|
const isDependencyError = error instanceof Error && (
|
||||||
@@ -428,7 +426,8 @@ serve(async (req) => {
|
|||||||
.eq('id', update.id);
|
.eq('id', update.id);
|
||||||
|
|
||||||
if (batchApproveError) {
|
if (batchApproveError) {
|
||||||
console.error('[APPROVAL] Failed to approve item:', {
|
edgeLogger.error('Failed to approve item', {
|
||||||
|
action: 'approval_batch_approve',
|
||||||
itemId: update.id,
|
itemId: update.id,
|
||||||
error: batchApproveError.message
|
error: batchApproveError.message
|
||||||
});
|
});
|
||||||
@@ -459,7 +458,8 @@ serve(async (req) => {
|
|||||||
.eq('id', update.id);
|
.eq('id', update.id);
|
||||||
|
|
||||||
if (batchRejectError) {
|
if (batchRejectError) {
|
||||||
console.error('[APPROVAL] Failed to reject item:', {
|
edgeLogger.error('Failed to reject item', {
|
||||||
|
action: 'approval_batch_reject',
|
||||||
itemId: update.id,
|
itemId: update.id,
|
||||||
error: batchRejectError.message
|
error: batchRejectError.message
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||||
|
import { edgeLogger } from '../_shared/logger.ts'
|
||||||
|
|
||||||
// Environment-aware CORS configuration
|
// Environment-aware CORS configuration
|
||||||
const getAllowedOrigin = (requestOrigin: string | null): string | null => {
|
const getAllowedOrigin = (requestOrigin: string | null): string | null => {
|
||||||
@@ -74,7 +75,7 @@ serve(async (req) => {
|
|||||||
|
|
||||||
// Check if this is a CORS request with a disallowed origin
|
// Check if this is a CORS request with a disallowed origin
|
||||||
if (requestOrigin && !allowedOrigin) {
|
if (requestOrigin && !allowedOrigin) {
|
||||||
console.error(`[CORS] Request rejected for disallowed origin: ${requestOrigin}`);
|
edgeLogger.warn('CORS request rejected', { action: 'cors_validation', origin: requestOrigin });
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: 'Origin not allowed',
|
error: 'Origin not allowed',
|
||||||
@@ -124,7 +125,7 @@ serve(async (req) => {
|
|||||||
|
|
||||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||||
if (authError || !user) {
|
if (authError || !user) {
|
||||||
console.error('Auth verification failed:', authError)
|
edgeLogger.error('Auth verification failed', { action: 'delete_auth', error: authError?.message })
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: 'Invalid authentication',
|
error: 'Invalid authentication',
|
||||||
@@ -145,7 +146,7 @@ serve(async (req) => {
|
|||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (profileError || !profile) {
|
if (profileError || !profile) {
|
||||||
console.error('Failed to fetch user profile:', profileError)
|
edgeLogger.error('Failed to fetch user profile', { action: 'delete_profile_check', userId: user.id })
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: 'User profile not found',
|
error: 'User profile not found',
|
||||||
|
|||||||
Reference in New Issue
Block a user