Fix remaining production readiness issues

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 12:30:09 +00:00
parent 640fdb11db
commit 4983960138
7 changed files with 158 additions and 91 deletions

View File

@@ -2,6 +2,8 @@ import { supabase } from '@/integrations/supabase/client';
import type { Json } from '@/integrations/supabase/types'; import type { Json } from '@/integrations/supabase/types';
import { uploadPendingImages } from './imageUploadHelper'; import { uploadPendingImages } from './imageUploadHelper';
import { CompanyFormData, TempCompanyData } from '@/types/company'; import { CompanyFormData, TempCompanyData } from '@/types/company';
import { logger } from './logger';
import { getErrorMessage } from './errorHandler';
export type { CompanyFormData, TempCompanyData }; export type { CompanyFormData, TempCompanyData };
@@ -19,8 +21,12 @@ export async function submitCompanyCreation(
...data.images, ...data.images,
uploaded: uploadedImages uploaded: uploadedImages
}; };
} catch (error) { } catch (error: unknown) {
console.error(`Failed to upload images for ${companyType} creation:`, error); const errorMsg = getErrorMessage(error);
logger.error('Failed to upload images for company', {
action: `${companyType}_creation`,
error: errorMsg
});
throw new Error('Failed to upload images. Please check your connection and try again.'); throw new Error('Failed to upload images. Please check your connection and try again.');
} }
} }
@@ -91,8 +97,13 @@ export async function submitCompanyUpdate(
...data.images, ...data.images,
uploaded: uploadedImages uploaded: uploadedImages
}; };
} catch (error) { } catch (error: unknown) {
console.error(`Failed to upload images for ${existingCompany.company_type} update:`, error); const errorMsg = getErrorMessage(error);
logger.error('Failed to upload images for company update', {
action: `${existingCompany.company_type}_update`,
companyId,
error: errorMsg
});
throw new Error('Failed to upload images. Please check your connection and try again.'); throw new Error('Failed to upload images. Please check your connection and try again.');
} }
} }

View File

@@ -1,5 +1,6 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
import { updateSubmissionItem, type SubmissionItemWithDeps, type DependencyConflict } from './submissionItemsService'; import { updateSubmissionItem, type SubmissionItemWithDeps, type DependencyConflict } from './submissionItemsService';
export interface ResolutionResult { export interface ResolutionResult {
@@ -84,9 +85,13 @@ export async function resolveConflicts(
success: true, success: true,
updatedSelections, updatedSelections,
}; };
} catch (error) { } catch (error: unknown) {
const errorMsg = getErrorMessage(error); const errorMsg = getErrorMessage(error);
console.error('Conflict resolution error:', errorMsg); logger.error('Conflict resolution error', {
action: 'resolve_conflicts',
conflictCount: conflicts.length,
error: errorMsg
});
return { return {
success: false, success: false,
error: errorMsg, error: errorMsg,
@@ -230,8 +235,13 @@ export async function findMatchingEntities(
} }
return []; return [];
} catch (error) { } catch (error: unknown) {
console.error('Error finding matching entities:', error); const errorMsg = getErrorMessage(error);
logger.error('Error finding matching entities', {
action: 'find_matching_entities',
itemType,
error: errorMsg
});
return []; return [];
} }
} }

View File

@@ -1,4 +1,6 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
interface EmailValidationResult { interface EmailValidationResult {
valid: boolean; valid: boolean;
@@ -17,7 +19,10 @@ export async function validateEmailNotDisposable(email: string): Promise<EmailVa
}); });
if (error) { if (error) {
console.error('Email validation error:', error); logger.error('Email validation error from backend', {
action: 'validate_email_backend',
error: error.message
});
return { return {
valid: false, valid: false,
reason: 'Unable to validate email address. Please try again.' reason: 'Unable to validate email address. Please try again.'
@@ -25,8 +30,12 @@ export async function validateEmailNotDisposable(email: string): Promise<EmailVa
} }
return data as EmailValidationResult; return data as EmailValidationResult;
} catch (error) { } catch (error: unknown) {
console.error('Email validation exception:', error); const errorMsg = getErrorMessage(error);
logger.error('Email validation error', {
action: 'validate_email_disposable',
error: errorMsg
});
return { return {
valid: false, valid: false,
reason: 'Unable to validate email address. Please try again.' reason: 'Unable to validate email address. Please try again.'

View File

@@ -1,5 +1,6 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { getErrorMessage } from './errorHandler'; import { getErrorMessage } from './errorHandler';
import { logger } from './logger';
export interface SubmissionItemWithDeps { export interface SubmissionItemWithDeps {
id: string; id: string;
@@ -225,9 +226,14 @@ export async function approveSubmissionItems(
// Add to dependency map for child items // Add to dependency map for child items
dependencyMap.set(item.id, entityId); dependencyMap.set(item.id, entityId);
} catch (error) { } catch (error: unknown) {
const errorMsg = getErrorMessage(error); const errorMsg = getErrorMessage(error);
console.error(`Error approving ${item.item_type} item ${item.id}:`, errorMsg); logger.error('Error approving items', {
action: 'approve_submission_items',
error: errorMsg,
userId,
itemCount: items.length
});
// Update item with error status // Update item with error status
await updateSubmissionItem(item.id, { await updateSubmissionItem(item.id, {
@@ -377,10 +383,14 @@ async function createPark(data: any, dependencyMap: Map<string, string>): Promis
.update(updateData) .update(updateData)
.eq('id', data.park_id); .eq('id', data.park_id);
if (error) { if (error) {
console.error('Error updating park:', error); logger.error('Error updating park', {
throw new Error(`Database error: ${error.message}`); action: 'update_park',
} parkId: data.park_id,
error: error.message
});
throw new Error(`Database error: ${error.message}`);
}
return data.park_id; return data.park_id;
} }
@@ -417,7 +427,11 @@ async function createPark(data: any, dependencyMap: Map<string, string>): Promis
.single(); .single();
if (error) { if (error) {
console.error('Error creating park:', error); logger.error('Error creating park', {
action: 'create_park',
parkName: resolvedData.name,
error: error.message
});
throw new Error(`Database error: ${error.message}`); throw new Error(`Database error: ${error.message}`);
} }
@@ -462,7 +476,11 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
.single(); .single();
if (error) { if (error) {
console.error('Error creating location:', error); logger.error('Error creating location', {
action: 'create_location',
locationData,
error: error.message
});
throw new Error(`Failed to create location: ${error.message}`); throw new Error(`Failed to create location: ${error.message}`);
} }

View File

@@ -46,17 +46,20 @@ function cleanupExpiredEntries() {
cleanupFailureCount = 0; cleanupFailureCount = 0;
} }
} catch (error) { } catch (error: unknown) {
// CRITICAL: Increment failure counter and log detailed error information // CRITICAL: Increment failure counter and log detailed error information
cleanupFailureCount++; cleanupFailureCount++;
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : 'No stack trace available'; const errorStack = error instanceof Error ? error.stack : 'No stack trace available';
console.error(`[Cleanup Error] Cleanup failed (attempt ${cleanupFailureCount}/${MAX_CLEANUP_FAILURES})`); console.error('[Cleanup Error]', {
console.error(`[Cleanup Error] Error message: ${errorMessage}`); attempt: cleanupFailureCount,
console.error(`[Cleanup Error] Stack trace: ${errorStack}`); maxAttempts: MAX_CLEANUP_FAILURES,
console.error(`[Cleanup Error] Current map size: ${rateLimitMap.size}`); error: errorMessage,
stack: errorStack,
mapSize: rateLimitMap.size
});
// FALLBACK MECHANISM: If cleanup fails repeatedly, force clear to prevent memory leak // FALLBACK MECHANISM: If cleanup fails repeatedly, force clear to prevent memory leak
if (cleanupFailureCount >= MAX_CLEANUP_FAILURES) { if (cleanupFailureCount >= MAX_CLEANUP_FAILURES) {
@@ -204,7 +207,8 @@ serve(async (req) => {
); );
} }
console.log('Detecting location for IP:', clientIP); // PII Note: Do not log full IP addresses in production
console.log('[Location] Detecting location for request');
// Use configurable geolocation service with proper error handling // Use configurable geolocation service with proper error handling
// Defaults to ip-api.com if not configured // Defaults to ip-api.com if not configured
@@ -245,7 +249,11 @@ serve(async (req) => {
measurementSystem measurementSystem
}; };
console.log('Location detected:', result); console.log('[Location] Location detected:', {
country: result.country,
countryCode: result.countryCode,
measurementSystem: result.measurementSystem
});
return new Response( return new Response(
JSON.stringify(result), JSON.stringify(result),
@@ -257,14 +265,16 @@ serve(async (req) => {
} }
); );
} catch (error) { } catch (error: unknown) {
// Enhanced error logging for better visibility and debugging // Enhanced error logging for better visibility and debugging
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : 'No stack trace available'; const errorStack = error instanceof Error ? error.stack : 'No stack trace available';
console.error('[Location Detection Error] Request failed'); console.error('[Location Detection Error]', {
console.error(`[Location Detection Error] Message: ${errorMessage}`); error: errorMessage,
console.error(`[Location Detection Error] Stack: ${errorStack}`); stack: errorStack,
hasIP: true // IP removed for PII protection
});
// Return default (metric) with 500 status to indicate error occurred // Return default (metric) with 500 status to indicate error occurred
// This allows proper error monitoring while still providing fallback data // This allows proper error monitoring while still providing fallback data

View File

@@ -68,33 +68,18 @@ serve(async (req) => {
}); });
// Verify JWT and get authenticated user // Verify JWT and get authenticated user
console.log('🔍 [AUTH DEBUG] Attempting getUser()...', {
hasAuthHeader: !!authHeader,
authHeaderLength: authHeader?.length,
authHeaderPrefix: authHeader?.substring(0, 20) + '...',
supabaseUrl,
timestamp: new Date().toISOString()
});
const { data: { user }, error: authError } = await supabaseAuth.auth.getUser(); const { data: { user }, error: authError } = await supabaseAuth.auth.getUser();
console.log('🔍 [AUTH DEBUG] getUser() result:', { console.log('[AUTH] User auth result:', {
hasUser: !!user, hasUser: !!user,
userId: user?.id, userId: user?.id,
userEmail: user?.email, hasError: !!authError
hasError: !!authError,
errorMessage: authError?.message,
errorName: authError?.name,
errorStatus: authError?.status,
errorCode: authError?.code
}); });
if (authError || !user) { if (authError || !user) {
console.error('[AUTH DEBUG] Auth verification failed:', { console.error('[AUTH] Auth verification failed:', {
error: authError, error: authError?.message,
errorDetails: JSON.stringify(authError), code: authError?.code
authHeaderPresent: !!authHeader,
authHeaderSample: authHeader?.substring(0, 30) + '...'
}); });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
@@ -106,7 +91,7 @@ serve(async (req) => {
); );
} }
console.log('[AUTH DEBUG] Authentication successful for user:', user.id); console.log('[AUTH] Authentication successful:', 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
@@ -122,22 +107,18 @@ serve(async (req) => {
); );
// Check if user has moderator permissions using service role to bypass RLS // Check if user has moderator permissions using service role to bypass RLS
console.log('🔍 [ROLE CHECK] Fetching roles for user:', authenticatedUserId);
const { data: roles, error: rolesError } = await supabase const { data: roles, error: rolesError } = await supabase
.from('user_roles') .from('user_roles')
.select('role') .select('role')
.eq('user_id', authenticatedUserId); .eq('user_id', authenticatedUserId);
console.log('🔍 [ROLE CHECK] Query result:', { console.log('[ROLE_CHECK] Query result:', {
roles,
error: rolesError,
rolesCount: roles?.length, rolesCount: roles?.length,
userId: authenticatedUserId error: rolesError?.message
}); });
if (rolesError) { if (rolesError) {
console.error('[ROLE CHECK] Failed:', rolesError); console.error('[ROLE_CHECK] Failed:', { 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' } }
@@ -149,21 +130,17 @@ serve(async (req) => {
userRoles.includes('admin') || userRoles.includes('admin') ||
userRoles.includes('superuser'); userRoles.includes('superuser');
console.log('🔍 [ROLE CHECK] Result:', { console.log('[ROLE_CHECK] Result:', { isModerator, userId: authenticatedUserId });
userRoles,
isModerator,
userId: authenticatedUserId
});
if (!isModerator) { if (!isModerator) {
console.error('[ROLE CHECK] Insufficient permissions'); console.error('[ROLE_CHECK] Insufficient permissions');
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'); console.log('[ROLE_CHECK] User is moderator');
// 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
@@ -171,17 +148,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 }); console.log('[AAL_CHECK] Session AAL level:', { aal, userId: authenticatedUserId });
// 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 }); console.log('[MFA_CHECK] MFA status:', { hasMFA, userId: authenticatedUserId });
// 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', { userId: authenticatedUserId }); console.error('[AAL_CHECK] AAL2 required but session is at AAL1');
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'MFA verification required', error: 'MFA verification required',
@@ -192,7 +169,7 @@ serve(async (req) => {
); );
} }
console.log('[AAL CHECK] AAL2 check passed', { userId: authenticatedUserId, hasMFA, aal }); console.log('[AAL_CHECK] AAL2 check passed:', { userId: authenticatedUserId, hasMFA, aal });
const { itemIds, submissionId }: ApprovalRequest = await req.json(); const { itemIds, submissionId }: ApprovalRequest = await req.json();
@@ -229,7 +206,7 @@ serve(async (req) => {
); );
} }
console.log('Processing selective approval:', { itemIds, userId: authenticatedUserId, submissionId }); console.log('[APPROVAL] Processing selective approval:', { itemIds, 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
@@ -258,9 +235,14 @@ serve(async (req) => {
let sortedItems; let sortedItems;
try { try {
sortedItems = topologicalSort(items); sortedItems = topologicalSort(items);
} catch (sortError) { } 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('Topological sort failed:', errorMessage); console.error('[APPROVAL ERROR] Topological sort failed:', {
submissionId,
itemCount: items.length,
error: errorMessage,
userId: authenticatedUserId
});
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Invalid submission structure', error: 'Invalid submission structure',
@@ -284,13 +266,16 @@ 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(`Processing item ${item.id} of type ${item.item_type}`); console.log('[APPROVAL] Processing 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(`❌ Blocking errors for item ${item.id}:`, validation.blockingErrors); console.error('[APPROVAL] Blocking validation errors:', {
itemId: item.id,
errors: validation.blockingErrors
});
// Fail the entire batch if ANY item has blocking errors // Fail the entire batch if ANY item has blocking errors
return new Response(JSON.stringify({ return new Response(JSON.stringify({
@@ -306,7 +291,10 @@ serve(async (req) => {
} }
if (validation.warnings.length > 0) { if (validation.warnings.length > 0) {
console.warn(`⚠️ Warnings for item ${item.id}:`, validation.warnings); console.warn('[APPROVAL] Validation warnings:', {
itemId: item.id,
warnings: validation.warnings
});
// Continue processing - warnings don't block approval // Continue processing - warnings don't block approval
} }
@@ -319,7 +307,7 @@ serve(async (req) => {
}); });
if (setUserIdError) { if (setUserIdError) {
console.error('Failed to set user context:', setUserIdError); console.error('[APPROVAL] Failed to set user context:', { error: setUserIdError.message });
} }
// Set submission ID for version tracking // Set submission ID for version tracking
@@ -330,7 +318,7 @@ serve(async (req) => {
}); });
if (setSubmissionIdError) { if (setSubmissionIdError) {
console.error('Failed to set submission context:', setSubmissionIdError); console.error('[APPROVAL] Failed to set submission context:', { error: setSubmissionIdError.message });
} }
// Resolve dependencies in item data // Resolve dependencies in item data
@@ -390,9 +378,16 @@ serve(async (req) => {
success: true success: true
}); });
console.log(`Successfully approved item ${item.id} -> entity ${entityId}`); console.log('[APPROVAL SUCCESS]', { itemId: item.id, entityId, itemType: item.item_type });
} catch (error) { } catch (error: unknown) {
console.error(`Error processing item ${item.id}:`, error); const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('[APPROVAL ERROR] Item approval failed:', {
itemId: item.id,
itemType: item.item_type,
error: errorMessage,
userId: authenticatedUserId,
submissionId
});
const isDependencyError = error instanceof Error && ( const isDependencyError = error instanceof Error && (
error.message.includes('Missing dependency') || error.message.includes('Missing dependency') ||
@@ -404,7 +399,7 @@ serve(async (req) => {
itemId: item.id, itemId: item.id,
itemType: item.item_type, itemType: item.item_type,
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error', error: errorMessage,
isDependencyFailure: isDependencyError isDependencyFailure: isDependencyError
}); });
} }
@@ -433,7 +428,10 @@ serve(async (req) => {
.eq('id', update.id); .eq('id', update.id);
if (batchApproveError) { if (batchApproveError) {
console.error(`Failed to approve item ${update.id}:`, batchApproveError); console.error('[APPROVAL] Failed to approve item:', {
itemId: update.id,
error: batchApproveError.message
});
} }
} }
} }
@@ -461,7 +459,10 @@ serve(async (req) => {
.eq('id', update.id); .eq('id', update.id);
if (batchRejectError) { if (batchRejectError) {
console.error(`Failed to reject item ${update.id}:`, batchRejectError); console.error('[APPROVAL] Failed to reject item:', {
itemId: update.id,
error: batchRejectError.message
});
} }
} }
} }
@@ -504,7 +505,7 @@ serve(async (req) => {
.eq('id', submissionId); .eq('id', submissionId);
if (updateError) { if (updateError) {
console.error('Failed to update submission status:', updateError); console.error('[APPROVAL] Failed to update submission status:', { error: updateError.message });
} }
return new Response( return new Response(
@@ -515,8 +516,13 @@ serve(async (req) => {
}), }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } } { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
); );
} catch (error) { } catch (error: unknown) {
console.error('Error in process-selective-approval:', error); const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
console.error('[APPROVAL ERROR] Process failed:', {
error: errorMessage,
userId: authenticatedUserId,
timestamp: new Date().toISOString()
});
return createErrorResponse( return createErrorResponse(
error, error,
500, 500,

View File

@@ -175,7 +175,9 @@ serve(async (req) => {
let requestBody; let requestBody;
try { try {
requestBody = await req.json(); requestBody = await req.json();
} catch (error) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('[Upload] Invalid JSON:', { error: errorMessage });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Invalid JSON', error: 'Invalid JSON',
@@ -357,7 +359,7 @@ serve(async (req) => {
let requestBody; let requestBody;
try { try {
requestBody = await req.json(); requestBody = await req.json();
} catch (error) { } catch (error: unknown) {
requestBody = {}; requestBody = {};
} }
@@ -603,12 +605,13 @@ serve(async (req) => {
} }
) )
} catch (error) { } catch (error: unknown) {
console.error('Upload error:', error) const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
console.error('[Upload] Error:', { error: errorMessage });
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Internal server error', error: 'Internal server error',
message: error instanceof Error ? error.message : 'An unexpected error occurred' message: errorMessage
}), }),
{ {
status: 500, status: 500,