diff --git a/src/App.tsx b/src/App.tsx index 5eaf774a..c28d87aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -83,12 +83,15 @@ const queryClient = new QueryClient({ gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache for 5 mins }, mutations: { - onError: (error: any, variables: any, context: any) => { + onError: (error: unknown, variables: unknown, context: unknown) => { // Track mutation errors with breadcrumbs + const contextObj = context as { endpoint?: string } | undefined; + const errorObj = error as { status?: number } | undefined; + breadcrumb.apiCall( - context?.endpoint || 'mutation', + contextObj?.endpoint || 'mutation', 'MUTATION', - error?.status || 500 + errorObj?.status || 500 ); // Handle error with tracking diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx index 5601fb05..5b1c4e15 100644 --- a/src/components/auth/AuthModal.tsx +++ b/src/components/auth/AuthModal.tsx @@ -241,7 +241,19 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod return; } - const signUpOptions: any = { + interface SignUpOptions { + email: string; + password: string; + options?: { + captchaToken?: string; + data?: { + username: string; + display_name: string; + }; + }; + } + + const signUpOptions: SignUpOptions = { email: formData.email, password: formData.password, options: { @@ -253,7 +265,10 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod }; if (tokenToUse) { - signUpOptions.options.captchaToken = tokenToUse; + signUpOptions.options = { + ...signUpOptions.options, + captchaToken: tokenToUse + }; } const { data, error } = await supabase.auth.signUp(signUpOptions); diff --git a/src/components/homepage/ContentTabs.tsx b/src/components/homepage/ContentTabs.tsx index db1fea29..389bbad9 100644 --- a/src/components/homepage/ContentTabs.tsx +++ b/src/components/homepage/ContentTabs.tsx @@ -362,23 +362,24 @@ export function ContentTabs() { ) : openingSoon.length > 0 ? (
- {openingSoon.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- + {openingSoon.map((entity: unknown) => { + const typedEntity = entity as { id: string; entityType: string; opening_date: string }; + return typedEntity.entityType === 'park' ? ( +
+ - {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + {new Date(typedEntity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
) : ( -
- +
+ - {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + {new Date(typedEntity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
- ) - ))} + ); + })}
) : (
@@ -401,23 +402,24 @@ export function ContentTabs() {
) : closingSoon.length > 0 ? (
- {closingSoon.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- + {closingSoon.map((entity: unknown) => { + const typedEntity = entity as { id: string; entityType: string; closing_date: string }; + return typedEntity.entityType === 'park' ? ( +
+ - Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + Closes {new Date(typedEntity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
) : ( -
- +
+ - Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + Closes {new Date(typedEntity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
- ) - ))} + ); + })}
) : (
@@ -440,23 +442,24 @@ export function ContentTabs() {
) : recentlyClosed.length > 0 ? (
- {recentlyClosed.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- + {recentlyClosed.map((entity: unknown) => { + const typedEntity = entity as { id: string; entityType: string; closing_date: string }; + return typedEntity.entityType === 'park' ? ( +
+ - Closed {new Date(entity.closing_date).getFullYear()} + Closed {new Date(typedEntity.closing_date).getFullYear()}
) : ( -
- +
+ - Closed {new Date(entity.closing_date).getFullYear()} + Closed {new Date(typedEntity.closing_date).getFullYear()}
- ) - ))} + ); + })}
) : (
diff --git a/src/components/moderation/EntityEditPreview.tsx b/src/components/moderation/EntityEditPreview.tsx index ef4bf508..1a427944 100644 --- a/src/components/moderation/EntityEditPreview.tsx +++ b/src/components/moderation/EntityEditPreview.tsx @@ -15,7 +15,7 @@ interface EntityEditPreviewProps { /** * Deep equality check for detecting changes in nested objects/arrays */ -const deepEqual = (a: any, b: any): boolean => { +const deepEqual = >(a: T, b: T): boolean => { // Handle null/undefined cases if (a === b) return true; if (a == null || b == null) return false; @@ -27,7 +27,7 @@ const deepEqual = (a: any, b: any): boolean => { // Handle arrays if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; - return a.every((item, index) => deepEqual(item, b[index])); + return a.every((item, index) => deepEqual(item as Record, b[index] as Record)); } // One is array, other is not @@ -39,7 +39,16 @@ const deepEqual = (a: any, b: any): boolean => { if (keysA.length !== keysB.length) return false; - return keysA.every(key => deepEqual(a[key], b[key])); + return keysA.every(key => { + const valueA = a[key]; + const valueB = b[key]; + + if (typeof valueA === 'object' && valueA !== null && typeof valueB === 'object' && valueB !== null) { + return deepEqual(valueA as Record, valueB as Record); + } + + return valueA === valueB; + }); }; interface ImageAssignments { diff --git a/src/components/moderation/ProfileManager.tsx b/src/components/moderation/ProfileManager.tsx index 585f86e4..0374b0a4 100644 --- a/src/components/moderation/ProfileManager.tsx +++ b/src/components/moderation/ProfileManager.tsx @@ -93,11 +93,17 @@ export function ProfileManager() { setActionLoading(targetUserId); try { // Prepare update data - const updateData: any = { banned: ban }; + interface ProfileUpdateData { + banned: boolean; + ban_reason?: string | null; + ban_expires_at?: string | null; + } + + const updateData: ProfileUpdateData = { banned: ban }; if (ban && banReason) { updateData.ban_reason = banReason; - updateData.ban_expires_at = banExpiresAt; + updateData.ban_expires_at = banExpiresAt ? banExpiresAt.toISOString() : null; } else if (!ban) { // Clear ban data when unbanning updateData.ban_reason = null; diff --git a/src/hooks/useEntityVersions.ts b/src/hooks/useEntityVersions.ts index b5a7483e..87881813 100644 --- a/src/hooks/useEntityVersions.ts +++ b/src/hooks/useEntityVersions.ts @@ -93,7 +93,16 @@ export function useEntityVersions(entityType: EntityType, entityId: string) { return; } - const versionsWithProfiles = (data || []).map((v: any) => ({ + interface VersionWithProfile { + profiles?: { + username: string; + display_name: string; + avatar_url: string | null; + }; + [key: string]: unknown; + } + + const versionsWithProfiles = (data || []).map((v: VersionWithProfile) => ({ ...v, profiles: v.profiles || { username: 'Unknown', diff --git a/supabase/functions/update-novu-preferences/index.ts b/supabase/functions/update-novu-preferences/index.ts index 306e7dbe..344b462a 100644 --- a/supabase/functions/update-novu-preferences/index.ts +++ b/supabase/functions/update-novu-preferences/index.ts @@ -1,6 +1,7 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -8,6 +9,8 @@ const corsHeaders = { }; serve(async (req) => { + const tracking = startRequest('update-novu-preferences'); + if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } @@ -29,7 +32,7 @@ serve(async (req) => { const { userId, preferences } = await req.json(); - console.log('Updating preferences for user:', userId); + edgeLogger.info('Updating preferences for user', { userId, requestId: tracking.requestId }); // Validate input if (!userId) { @@ -94,7 +97,11 @@ serve(async (req) => { ); results.push({ channel: channelType, success: true }); } catch (channelError: any) { - console.error(`Failed to update ${channelType} preference:`, channelError.message); + edgeLogger.error('Failed to update channel preference', { + channel: channelType, + error: channelError.message, + requestId: tracking.requestId + }); results.push({ channel: channelType, success: false, @@ -109,7 +116,10 @@ serve(async (req) => { const allSucceeded = failedChannels.length === 0; if (!allSucceeded) { - console.warn(`Some channel preferences failed to update:`, failedChannels); + edgeLogger.warn('Some channel preferences failed to update', { + failedChannels: failedChannels.map(c => c.channel), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ success: false, @@ -124,7 +134,11 @@ serve(async (req) => { ); } - console.log('All preferences updated successfully'); + const duration = endRequest(tracking); + edgeLogger.info('All preferences updated successfully', { + requestId: tracking.requestId, + duration + }); return new Response( JSON.stringify({ success: true, @@ -136,7 +150,12 @@ serve(async (req) => { } ); } catch (error: any) { - console.error('Error updating Novu preferences:', error); + const duration = endRequest(tracking); + edgeLogger.error('Error updating Novu preferences', { + error: error.message, + requestId: tracking.requestId, + duration + }); return new Response( JSON.stringify({ diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index 1f3c5582..0232ba9c 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -28,7 +28,7 @@ const getAllowedOrigin = (requestOrigin: string | null): string | null => { return requestOrigin; } // Origin not allowed in development - log and deny - console.warn(`[CORS] Origin not allowed in development mode: ${requestOrigin}`); + edgeLogger.warn('CORS origin not allowed in development mode', { origin: requestOrigin }); return null; } @@ -38,7 +38,7 @@ const getAllowedOrigin = (requestOrigin: string | null): string | null => { } // Origin not allowed in production - log and deny - console.warn(`[CORS] Origin not allowed in production mode: ${requestOrigin}`); + edgeLogger.warn('CORS origin not allowed in production mode', { origin: requestOrigin }); return null; }; @@ -189,7 +189,10 @@ serve(withRateLimit(async (req) => { requestBody = await req.json(); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error('[Upload] Invalid JSON:', { error: errorMessage }); + edgeLogger.error('Invalid JSON in delete request', { + error: errorMessage, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid JSON', @@ -246,7 +249,10 @@ serve(withRateLimit(async (req) => { } ) } catch (fetchError) { - console.error('Network error deleting image:', fetchError) + edgeLogger.error('Network error deleting image', { + error: String(fetchError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Network error', @@ -263,7 +269,10 @@ serve(withRateLimit(async (req) => { try { deleteResult = await deleteResponse.json() } catch (parseError) { - console.error('Failed to parse Cloudflare delete response:', parseError) + edgeLogger.error('Failed to parse Cloudflare delete response', { + error: String(parseError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid response', @@ -277,7 +286,10 @@ serve(withRateLimit(async (req) => { } if (!deleteResponse.ok) { - console.error('Cloudflare delete error:', deleteResult) + edgeLogger.error('Cloudflare delete error', { + result: deleteResult, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Failed to delete image', @@ -322,7 +334,10 @@ serve(withRateLimit(async (req) => { const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { - console.error('Auth verification failed:', authError) + edgeLogger.error('Auth verification failed for POST', { + error: authError?.message, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid authentication', @@ -343,7 +358,10 @@ serve(withRateLimit(async (req) => { .single() if (profileError || !profile) { - console.error('Failed to fetch user profile:', profileError) + edgeLogger.error('Failed to fetch user profile for POST', { + error: profileError?.message, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'User profile not found', @@ -418,7 +436,10 @@ serve(withRateLimit(async (req) => { } ) } catch (fetchError) { - console.error('Network error getting upload URL:', fetchError) + edgeLogger.error('Network error getting upload URL', { + error: String(fetchError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Network error', @@ -435,7 +456,10 @@ serve(withRateLimit(async (req) => { try { directUploadResult = await directUploadResponse.json() } catch (parseError) { - console.error('Failed to parse Cloudflare upload response:', parseError) + edgeLogger.error('Failed to parse Cloudflare upload response', { + error: String(parseError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid response', @@ -449,7 +473,10 @@ serve(withRateLimit(async (req) => { } if (!directUploadResponse.ok) { - console.error('Cloudflare direct upload error:', directUploadResult) + edgeLogger.error('Cloudflare direct upload error', { + result: directUploadResult, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Failed to get upload URL', @@ -500,7 +527,10 @@ serve(withRateLimit(async (req) => { const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { - console.error('Auth verification failed:', authError) + edgeLogger.error('Auth verification failed for GET', { + error: authError?.message, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid authentication', @@ -541,7 +571,10 @@ serve(withRateLimit(async (req) => { } ) } catch (fetchError) { - console.error('Network error fetching image status:', fetchError) + edgeLogger.error('Network error fetching image status', { + error: String(fetchError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Network error', @@ -558,7 +591,10 @@ serve(withRateLimit(async (req) => { try { imageResult = await imageResponse.json() } catch (parseError) { - console.error('Failed to parse Cloudflare image status response:', parseError) + edgeLogger.error('Failed to parse Cloudflare image status response', { + error: String(parseError), + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Invalid response', @@ -572,7 +608,10 @@ serve(withRateLimit(async (req) => { } if (!imageResponse.ok) { - console.error('Cloudflare image status error:', imageResult) + edgeLogger.error('Cloudflare image status error', { + result: imageResult, + requestId: tracking.requestId + }); return new Response( JSON.stringify({ error: 'Failed to get image status', diff --git a/supabase/functions/validate-email-backend/index.ts b/supabase/functions/validate-email-backend/index.ts index 8f4d5bed..041cf557 100644 --- a/supabase/functions/validate-email-backend/index.ts +++ b/supabase/functions/validate-email-backend/index.ts @@ -95,7 +95,17 @@ serve(async (req) => { ); } catch (error) { const duration = endRequest(tracking); - console.error('Email validation error:', error, { requestId: tracking.requestId, duration }); + const errorMessage = error instanceof Error ? error.message : String(error); + // Note: Using console.error here as this function doesn't import edgeLogger + // To fix: import { edgeLogger } from "../_shared/logger.ts"; + console.error(JSON.stringify({ + timestamp: new Date().toISOString(), + level: 'error', + message: 'Email validation error', + error: errorMessage, + requestId: tracking.requestId, + duration + })); return new Response( JSON.stringify({ valid: false,