diff --git a/src/components/settings/PasswordUpdateDialog.tsx b/src/components/settings/PasswordUpdateDialog.tsx index 450162f6..858519f7 100644 --- a/src/components/settings/PasswordUpdateDialog.tsx +++ b/src/components/settings/PasswordUpdateDialog.tsx @@ -33,6 +33,15 @@ interface PasswordUpdateDialogProps { type Step = 'password' | 'mfa' | 'success'; +interface ErrorWithCode { + code?: string; + status?: number; +} + +function isErrorWithCode(error: unknown): error is Error & ErrorWithCode { + return error instanceof Error && ('code' in error || 'status' in error); +} + export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) { const { theme } = useTheme(); const [step, setStep] = useState('password'); @@ -135,16 +144,20 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password await updatePasswordWithNonce(data.newPassword, generatedNonce); } } catch (error) { + const errorDetails = isErrorWithCode(error) ? { + errorCode: error.code, + errorStatus: error.status + } : {}; + logger.error('Password change failed', { userId, action: 'password_change', error: getErrorMessage(error), - errorCode: error instanceof Error && 'code' in error ? (error as any).code : undefined, - errorStatus: error instanceof Error && 'status' in error ? (error as any).status : undefined + ...errorDetails }); const errorMessage = getErrorMessage(error); - const errorStatus = error instanceof Error && 'status' in error ? (error as any).status : undefined; + const errorStatus = isErrorWithCode(error) ? error.status : undefined; if (errorMessage?.includes('rate limit') || errorStatus === 429) { handleError( diff --git a/src/hooks/moderation/useEntityCache.ts b/src/hooks/moderation/useEntityCache.ts index a11fa0ae..98b1d826 100644 --- a/src/hooks/moderation/useEntityCache.ts +++ b/src/hooks/moderation/useEntityCache.ts @@ -3,6 +3,7 @@ import { supabase } from '@/integrations/supabase/client'; import { createTableQuery } from '@/lib/supabaseHelpers'; import { logger } from '@/lib/logger'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; +import type { Database } from '@/integrations/supabase/types'; /** * Entity types supported by the cache @@ -10,12 +11,34 @@ import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; type EntityType = 'rides' | 'parks' | 'companies'; /** - * Cache structure for entities + * Type definitions for cached entities (can be partial) + */ +type Ride = Database['public']['Tables']['rides']['Row']; +type Park = Database['public']['Tables']['parks']['Row']; +type Company = Database['public']['Tables']['companies']['Row']; + +/** + * Discriminated union for all cached entity types + */ +type CachedEntity = Ride | Park | Company; + +/** + * Map entity type strings to their corresponding types + * Cache stores partial entities with at least id and name + */ +interface EntityTypeMap { + rides: Partial & { id: string; name: string }; + parks: Partial & { id: string; name: string }; + companies: Partial & { id: string; name: string }; +} + +/** + * Cache structure for entities with flexible typing */ interface EntityCacheStructure { - rides: Map; - parks: Map; - companies: Map; + rides: Map & { id: string; name: string }>; + parks: Map & { id: string; name: string }>; + companies: Map & { id: string; name: string }>; } /** @@ -50,10 +73,13 @@ export function useEntityCache() { }); /** - * Get a cached entity by ID + * Get a cached entity by ID with type safety */ - const getCached = useCallback((type: EntityType, id: string): any | undefined => { - return cacheRef.current[type].get(id); + const getCached = useCallback(( + type: T, + id: string + ): EntityTypeMap[T] | undefined => { + return cacheRef.current[type].get(id) as EntityTypeMap[T] | undefined; }, []); /** @@ -64,9 +90,13 @@ export function useEntityCache() { }, []); /** - * Set a cached entity with LRU eviction + * Set a cached entity with LRU eviction and type safety */ - const setCached = useCallback((type: EntityType, id: string, data: any): void => { + const setCached = useCallback(( + type: T, + id: string, + data: EntityTypeMap[T] + ): void => { const cache = cacheRef.current[type]; // LRU eviction: remove oldest entry if cache is full @@ -92,10 +122,10 @@ export function useEntityCache() { * Bulk fetch entities from the database and cache them * Only fetches entities that aren't already cached */ - const bulkFetch = useCallback(async ( - type: EntityType, + const bulkFetch = useCallback(async ( + type: T, ids: string[] - ): Promise => { + ): Promise => { if (ids.length === 0) return []; // Filter to only uncached IDs @@ -148,7 +178,7 @@ export function useEntityCache() { // Cache the fetched entities if (data) { data.forEach((entity: any) => { - setCached(type, entity.id, entity); + setCached(type, entity.id, entity as EntityTypeMap[T]); }); } diff --git a/src/hooks/moderation/useRealtimeSubscriptions.ts b/src/hooks/moderation/useRealtimeSubscriptions.ts index 0df896b4..d8835f73 100644 --- a/src/hooks/moderation/useRealtimeSubscriptions.ts +++ b/src/hooks/moderation/useRealtimeSubscriptions.ts @@ -192,7 +192,7 @@ export function useRealtimeSubscriptions( } else { const { data: ride } = await supabase .from('rides') - .select('name, park_id') + .select('id, name, park_id') .eq('id', content.entity_id) .maybeSingle(); @@ -203,7 +203,7 @@ export function useRealtimeSubscriptions( if (ride.park_id) { const { data: park } = await supabase .from('parks') - .select('name') + .select('id, name') .eq('id', ride.park_id) .maybeSingle(); @@ -221,7 +221,7 @@ export function useRealtimeSubscriptions( } else { const { data: park } = await supabase .from('parks') - .select('name') + .select('id, name') .eq('id', content.entity_id) .maybeSingle(); @@ -237,7 +237,7 @@ export function useRealtimeSubscriptions( } else { const { data: company } = await supabase .from('companies') - .select('name') + .select('id, name') .eq('id', content.entity_id) .maybeSingle(); diff --git a/src/hooks/usePhotoSubmissionItems.ts b/src/hooks/usePhotoSubmissionItems.ts index 21afdf84..02945a74 100644 --- a/src/hooks/usePhotoSubmissionItems.ts +++ b/src/hooks/usePhotoSubmissionItems.ts @@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'; import { supabase } from '@/integrations/supabase/client'; +import { getErrorMessage } from '@/lib/errorHandler'; import type { PhotoSubmissionItem } from '@/types/photo-submissions'; interface UsePhotoSubmissionItemsResult { @@ -61,9 +62,10 @@ export function usePhotoSubmissionItems( if (itemsError) throw itemsError; setPhotos(data || []); - } catch (err: any) { - console.error('Error fetching photo submission items:', err); - setError(err.message || 'Failed to load photos'); + } catch (error) { + const errorMsg = getErrorMessage(error); + console.error('Error fetching photo submission items:', errorMsg); + setError(errorMsg); setPhotos([]); } finally { setLoading(false); diff --git a/src/lib/moderation/actions.ts b/src/lib/moderation/actions.ts index b38e2809..8e390098 100644 --- a/src/lib/moderation/actions.ts +++ b/src/lib/moderation/actions.ts @@ -10,6 +10,41 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { createTableQuery } from '@/lib/supabaseHelpers'; import type { ModerationItem } from '@/types/moderation'; +/** + * Type-safe update data for review moderation + * Note: These types document the expected structure. Type assertions (as any) are used + * during database updates due to Supabase's strict typed client, but the actual types + * are validated by the database schema and RLS policies. + */ +interface ReviewUpdateData { + moderation_status: string; + moderated_at: string; + moderated_by: string; + reviewer_notes?: string; + locked_until?: null; + locked_by?: null; +} + +/** + * Type-safe update data for submission moderation + * Note: These types document the expected structure. Type assertions (as any) are used + * during database updates due to Supabase's strict typed client, but the actual types + * are validated by the database schema and RLS policies. + */ +interface SubmissionUpdateData { + status: string; + reviewed_at: string; + reviewer_id: string; + reviewer_notes?: string; + locked_until?: null; + locked_by?: null; +} + +/** + * Discriminated union for moderation updates (documentation purposes) + */ +type ModerationUpdateData = ReviewUpdateData | SubmissionUpdateData; + /** * Result of a moderation action */ @@ -280,35 +315,35 @@ export async function performModerationAction( } } - // Standard moderation flow - const statusField = item.type === 'review' ? 'moderation_status' : 'status'; - const timestampField = item.type === 'review' ? 'moderated_at' : 'reviewed_at'; - const reviewerField = item.type === 'review' ? 'moderated_by' : 'reviewer_id'; - - const updateData: any = { - [statusField]: action, - [timestampField]: new Date().toISOString(), - [reviewerField]: moderatorId, - }; - - if (moderatorNotes) { - updateData.reviewer_notes = moderatorNotes; - } - + // Standard moderation flow - Build update object with type-appropriate fields let error: any = null; let data: any = null; // Use type-safe table queries based on item type if (item.type === 'review') { + const reviewUpdate = { + moderation_status: action, + moderated_at: new Date().toISOString(), + moderated_by: moderatorId, + ...(moderatorNotes && { reviewer_notes: moderatorNotes }), + }; + const result = await createTableQuery('reviews') - .update(updateData) + .update(reviewUpdate as any) // Type assertion needed for dynamic update .eq('id', item.id) .select(); error = result.error; data = result.data; } else { + const submissionUpdate = { + status: action, + reviewed_at: new Date().toISOString(), + reviewer_id: moderatorId, + ...(moderatorNotes && { reviewer_notes: moderatorNotes }), + }; + const result = await createTableQuery('content_submissions') - .update(updateData) + .update(submissionUpdate as any) // Type assertion needed for dynamic update .eq('id', item.id) .select(); error = result.error;