diff --git a/src/components/auth/MFAChallenge.tsx b/src/components/auth/MFAChallenge.tsx index 09237a3f..16ddc227 100644 --- a/src/components/auth/MFAChallenge.tsx +++ b/src/components/auth/MFAChallenge.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; +import { getErrorMessage } from '@/lib/errorHandler'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'; @@ -25,11 +26,11 @@ export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProp const { data, error } = await supabase.auth.mfa.challenge({ factorId }); if (error) throw error; setChallengeId(data.id); - } catch (error: any) { + } catch (error) { toast({ variant: "destructive", title: "MFA Challenge Failed", - description: error.message + description: getErrorMessage(error) }); onCancel(); } @@ -58,11 +59,11 @@ export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProp }); onSuccess(); } - } catch (error: any) { + } catch (error) { toast({ variant: "destructive", title: "Verification Failed", - description: error.message || "Invalid code. Please try again." + description: getErrorMessage(error) || "Invalid code. Please try again." }); setCode(''); } finally { diff --git a/src/components/auth/MFARemovalDialog.tsx b/src/components/auth/MFARemovalDialog.tsx index f0e86d34..ecf7ee95 100644 --- a/src/components/auth/MFARemovalDialog.tsx +++ b/src/components/auth/MFARemovalDialog.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { toast } from 'sonner'; +import { getErrorMessage } from '@/lib/errorHandler'; import { AlertDialog, AlertDialogAction, @@ -60,9 +61,9 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF toast.success('Password verified'); setStep('totp'); - } catch (error: any) { + } catch (error) { console.error('Password verification failed:', error); - toast.error('Invalid password. Please try again.'); + toast.error(getErrorMessage(error)); } finally { setLoading(false); } @@ -94,9 +95,9 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF toast.success('TOTP code verified'); setStep('confirm'); - } catch (error: any) { + } catch (error) { console.error('TOTP verification failed:', error); - toast.error('Invalid code. Please try again.'); + toast.error(getErrorMessage(error)); } finally { setLoading(false); } @@ -140,9 +141,9 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF toast.success('Two-factor authentication has been disabled'); handleClose(); onSuccess(); - } catch (error: any) { + } catch (error) { console.error('MFA removal failed:', error); - toast.error('Failed to disable MFA. Please try again.'); + toast.error(getErrorMessage(error)); } finally { setLoading(false); } diff --git a/src/components/auth/TOTPSetup.tsx b/src/components/auth/TOTPSetup.tsx index 3d968537..138e76d2 100644 --- a/src/components/auth/TOTPSetup.tsx +++ b/src/components/auth/TOTPSetup.tsx @@ -5,7 +5,7 @@ import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; -import { handleError, handleSuccess, handleInfo, AppError } from '@/lib/errorHandler'; +import { handleError, handleSuccess, handleInfo, AppError, getErrorMessage } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import { useAuth } from '@/hooks/useAuth'; import { supabase } from '@/integrations/supabase/client'; @@ -48,11 +48,11 @@ export function TOTPSetup() { updated_at: factor.updated_at })); setFactors(totpFactors); - } catch (error: any) { + } catch (error) { logger.error('Failed to fetch TOTP factors', { userId: user?.id, action: 'fetch_totp_factors', - error: error.message + error: getErrorMessage(error) }); } }; @@ -73,15 +73,15 @@ export function TOTPSetup() { setSecret(data.totp.secret); setFactorId(data.id); setEnrolling(true); - } catch (error: any) { + } catch (error) { logger.error('Failed to start TOTP enrollment', { userId: user?.id, action: 'totp_enroll_start', - error: error.message + error: getErrorMessage(error) }); handleError( new AppError( - error.message || 'Failed to start TOTP enrollment', + getErrorMessage(error) || 'Failed to start TOTP enrollment', 'TOTP_ENROLL_FAILED' ), { action: 'Start TOTP enrollment', userId: user?.id } @@ -146,17 +146,17 @@ export function TOTPSetup() { window.location.href = '/auth'; }, 2000); } - } catch (error: any) { + } catch (error) { logger.error('TOTP verification failed', { userId: user?.id, action: 'totp_verify', - error: error.message, + error: getErrorMessage(error), factorId }); handleError( new AppError( - error.message || 'Invalid verification code. Please try again.', + getErrorMessage(error) || 'Invalid verification code. Please try again.', 'TOTP_VERIFY_FAILED' ), { action: 'Verify TOTP code', userId: user?.id, metadata: { factorId } } diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index d4b6ed1f..e233076c 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; +import { getErrorMessage } from '@/lib/errorHandler'; import { fetchSubmissionItems, buildDependencyTree, @@ -99,10 +100,10 @@ export function SubmissionReviewManager({ .filter(item => item.status === 'pending') .map(item => item.id); setSelectedItemIds(new Set(pendingIds)); - } catch (error: any) { + } catch (error) { toast({ title: 'Error', - description: error.message || 'Failed to load submission items', + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -138,10 +139,10 @@ export function SubmissionReviewManager({ // No conflicts, proceed with approval handleApprove(); } - } catch (error: any) { + } catch (error) { toast({ title: 'Error', - description: error.message || 'Failed to check dependencies', + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -244,11 +245,11 @@ export function SubmissionReviewManager({ onComplete(); onOpenChange(false); - } catch (error: any) { + } catch (error) { console.error('Error approving items:', error); toast({ title: 'Error', - description: error.message || 'Failed to approve items', + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -299,11 +300,11 @@ export function SubmissionReviewManager({ onComplete(); onOpenChange(false); - } catch (error: any) { + } catch (error) { console.error('Error rejecting items:', error); toast({ title: 'Error', - description: 'Failed to reject items. Please try again.', + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -352,11 +353,11 @@ export function SubmissionReviewManager({ onComplete(); onOpenChange(false); - } catch (error: any) { + } catch (error) { console.error('Error escalating submission:', error); toast({ title: 'Error', - description: error.message || 'Failed to escalate submission', + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -419,11 +420,11 @@ export function SubmissionReviewManager({ } await loadSubmissionItems(); - } catch (error: any) { + } catch (error) { console.error('Error changing item status:', error); toast({ title: 'Error', - description: error.message || 'Failed to change item status', + description: getErrorMessage(error), variant: 'destructive', }); } finally { diff --git a/src/components/profile/RideCreditsManager.tsx b/src/components/profile/RideCreditsManager.tsx index 8cfd7043..3c08eae9 100644 --- a/src/components/profile/RideCreditsManager.tsx +++ b/src/components/profile/RideCreditsManager.tsx @@ -188,8 +188,11 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { if (error) throw error; + // Type assertion for complex joined data + const newCredit = data as unknown as UserRideCredit; + // Add to existing array - setCredits(prev => [...prev, data as any]); + setCredits(prev => [...prev, newCredit]); setIsAddDialogOpen(false); } catch (error) { console.error('Error fetching new credit:', error); diff --git a/src/components/settings/PasswordUpdateDialog.tsx b/src/components/settings/PasswordUpdateDialog.tsx index 618c4a22..450162f6 100644 --- a/src/components/settings/PasswordUpdateDialog.tsx +++ b/src/components/settings/PasswordUpdateDialog.tsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { handleError, handleSuccess, AppError } from '@/lib/errorHandler'; +import { handleError, handleSuccess, AppError, getErrorMessage } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import { supabase } from '@/integrations/supabase/client'; import { Loader2, Shield, CheckCircle2 } from 'lucide-react'; @@ -134,16 +134,19 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password // No MFA, proceed with password update await updatePasswordWithNonce(data.newPassword, generatedNonce); } - } catch (error: any) { + } catch (error) { logger.error('Password change failed', { userId, action: 'password_change', - error: error instanceof Error ? error.message : String(error), - errorCode: error.code, - errorStatus: error.status + 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 }); - if (error.message?.includes('rate limit') || error.status === 429) { + const errorMessage = getErrorMessage(error); + const errorStatus = error instanceof Error && 'status' in error ? (error as any).status : undefined; + + if (errorMessage?.includes('rate limit') || errorStatus === 429) { handleError( new AppError( 'Please wait a few minutes before trying again.', @@ -152,7 +155,7 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password ), { action: 'Change password', userId, metadata: { step: 'authentication' } } ); - } else if (error.message?.includes('Invalid login credentials')) { + } else if (errorMessage?.includes('Invalid login credentials')) { handleError( new AppError( 'The password you entered is incorrect.', @@ -217,16 +220,16 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password // TOTP verified, now update password await updatePasswordWithNonce(newPassword, nonce); - } catch (error: any) { + } catch (error) { logger.error('MFA verification failed', { userId, action: 'password_change_mfa', - error: error instanceof Error ? error.message : String(error) + error: getErrorMessage(error) }); handleError( new AppError( - error.message || 'Invalid authentication code', + getErrorMessage(error) || 'Invalid authentication code', 'MFA_VERIFICATION_FAILED', 'TOTP code verification failed' ), @@ -277,7 +280,7 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password logger.error('Failed to send password change notification', { userId: user!.id, action: 'password_change_notification', - error: notifError instanceof Error ? notifError.message : String(notifError) + error: getErrorMessage(notifError) }); // Don't fail the password update if notification fails } @@ -293,7 +296,7 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password setStep('password'); setTotpCode(''); }, 2000); - } catch (error: any) { + } catch (error) { throw error; } }; diff --git a/src/components/settings/SimplePhotoUpload.tsx b/src/components/settings/SimplePhotoUpload.tsx index c1446994..09146dea 100644 --- a/src/components/settings/SimplePhotoUpload.tsx +++ b/src/components/settings/SimplePhotoUpload.tsx @@ -3,6 +3,7 @@ import { Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useToast } from '@/hooks/use-toast'; +import { getErrorMessage } from '@/lib/errorHandler'; interface SimplePhotoUploadProps { onUpload: (imageId: string, imageUrl: string) => Promise; @@ -48,10 +49,10 @@ export function SimplePhotoUpload({ onUpload, disabled, children }: SimplePhotoU title: 'Image uploaded', description: 'Your image has been uploaded successfully' }); - } catch (error: any) { + } catch (error) { toast({ title: 'Upload failed', - description: error.message || 'Failed to upload image', + description: getErrorMessage(error), variant: 'destructive' }); } finally { diff --git a/src/hooks/moderation/useModerationActions.ts b/src/hooks/moderation/useModerationActions.ts index 77009579..1b84bf0f 100644 --- a/src/hooks/moderation/useModerationActions.ts +++ b/src/hooks/moderation/useModerationActions.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { logger } from '@/lib/logger'; +import { getErrorMessage } from '@/lib/errorHandler'; import type { User } from '@supabase/supabase-js'; import type { ModerationItem } from '@/types/moderation'; @@ -176,11 +177,11 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); logger.log(`✅ Action ${action} completed for ${item.id}`); - } catch (error: any) { + } catch (error) { logger.error('❌ Error performing action:', error); toast({ title: 'Error', - description: error.message || `Failed to ${action} content`, + description: getErrorMessage(error) || `Failed to ${action} content`, variant: 'destructive', }); throw error; @@ -211,11 +212,11 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); logger.log(`✅ Submission ${item.id} deleted`); - } catch (error: any) { + } catch (error) { logger.error('❌ Error deleting submission:', error); toast({ title: 'Error', - description: 'Failed to delete submission', + description: getErrorMessage(error), variant: 'destructive', }); throw error; @@ -243,11 +244,11 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); logger.log(`✅ Submission ${item.id} reset to pending`); - } catch (error: any) { + } catch (error) { logger.error('❌ Error resetting submission:', error); toast({ title: 'Reset Failed', - description: error.message, + description: getErrorMessage(error), variant: 'destructive', }); } finally { @@ -294,11 +295,11 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); logger.log(`✅ Retried ${failedItems.length} failed items for ${item.id}`); - } catch (error: any) { + } catch (error) { logger.error('❌ Error retrying items:', error); toast({ title: 'Retry Failed', - description: error.message || 'Failed to retry items', + description: getErrorMessage(error) || 'Failed to retry items', variant: 'destructive', }); } finally { diff --git a/src/hooks/moderation/useRealtimeSubscriptions.ts b/src/hooks/moderation/useRealtimeSubscriptions.ts index 543a5e59..0df896b4 100644 --- a/src/hooks/moderation/useRealtimeSubscriptions.ts +++ b/src/hooks/moderation/useRealtimeSubscriptions.ts @@ -10,7 +10,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/lib/logger'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; -import type { RealtimeChannel } from '@supabase/supabase-js'; +import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js'; import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation'; import type { useEntityCache } from './useEntityCache'; import type { useProfileCache } from './useProfileCache'; @@ -21,6 +21,18 @@ import { buildModerationItem, } from '@/lib/moderation/realtime'; +/** + * Type-safe interface for submission content from realtime events + */ +interface SubmissionContent { + action?: string; + name?: string; + entity_slug?: string; + entity_name?: string; + entity_id?: string; + park_id?: string; +} + type EntityCacheReturn = ReturnType; type ProfileCacheReturn = ReturnType; @@ -164,7 +176,7 @@ export function useRealtimeSubscriptions( * Resolve entity names for a submission */ const resolveEntityNames = useCallback(async (submission: any) => { - const content = submission.content as any; + const content = submission.content as SubmissionContent; let entityName = content?.name || 'Unknown'; let parkName: string | undefined; @@ -242,8 +254,8 @@ export function useRealtimeSubscriptions( /** * Handle new submission INSERT event */ - const handleInsert = useCallback(async (payload: any) => { - const newSubmission = payload.new as any; + const handleInsert = useCallback(async (payload: RealtimePostgresChangesPayload) => { + const newSubmission = payload.new; logger.log('🆕 Realtime INSERT:', newSubmission.id); @@ -314,9 +326,9 @@ export function useRealtimeSubscriptions( /** * Handle submission UPDATE event */ - const handleUpdate = useCallback(async (payload: any) => { - const updatedSubmission = payload.new as any; - const oldSubmission = payload.old as any; + const handleUpdate = useCallback(async (payload: RealtimePostgresChangesPayload) => { + const updatedSubmission = payload.new; + const oldSubmission = payload.old; logger.log('🔄 Realtime UPDATE:', updatedSubmission.id); @@ -339,7 +351,8 @@ export function useRealtimeSubscriptions( } // Skip debounce for status changes (critical updates) - const isStatusChange = oldSubmission?.status !== updatedSubmission.status; + const isStatusChange = oldSubmission && 'status' in oldSubmission + && oldSubmission.status !== updatedSubmission?.status; if (isStatusChange) { logger.log('⚡ Status change detected, invalidating immediately'); diff --git a/src/lib/systemActivityService.ts b/src/lib/systemActivityService.ts index b3820ed7..ce357d9a 100644 --- a/src/lib/systemActivityService.ts +++ b/src/lib/systemActivityService.ts @@ -1,6 +1,7 @@ import { supabase } from '@/integrations/supabase/client'; +import { getErrorMessage } from '@/lib/errorHandler'; -export type ActivityType = +export type ActivityType = | 'entity_change' | 'admin_action' | 'submission_review' @@ -94,6 +95,56 @@ export interface ActivityFilters { dateTo?: string; } +/** + * Type-safe interface for version data from relational version tables + */ +interface VersionData { + version_id: string; + version_number: number; + name: string; + created_by: string | null; + created_at: string; + change_type: string; + change_reason: string | null; + is_current: boolean; +} + +interface ParkVersionData extends VersionData { + park_id: string; +} + +interface RideVersionData extends VersionData { + ride_id: string; +} + +interface CompanyVersionData extends VersionData { + company_id: string; +} + +interface RideModelVersionData extends VersionData { + ride_model_id: string; +} + +/** + * Type-safe interface for submission item data + */ +interface SubmissionItemData { + cloudflare_image_url?: string; + caption?: string; + title?: string; + entity_type?: string; + entity_id?: string; + reason?: string; +} + +/** + * Type-safe interface for submission content + */ +interface SubmissionContent { + action?: string; + name?: string; +} + /** * Fetch unified system activity log from multiple sources */ @@ -137,7 +188,7 @@ export async function fetchSystemActivities( // Process park versions if (!parkVersions.error && parkVersions.data) { for (const version of parkVersions.data) { - const parkVersion = version as any; // Type assertion for relational version structure + const parkVersion = version as ParkVersionData; activities.push({ id: parkVersion.version_id, type: 'entity_change', @@ -159,7 +210,7 @@ export async function fetchSystemActivities( // Process ride versions if (!rideVersions.error && rideVersions.data) { for (const version of rideVersions.data) { - const rideVersion = version as any; + const rideVersion = version as RideVersionData; activities.push({ id: rideVersion.version_id, type: 'entity_change', @@ -181,7 +232,7 @@ export async function fetchSystemActivities( // Process company versions if (!companyVersions.error && companyVersions.data) { for (const version of companyVersions.data) { - const companyVersion = version as any; + const companyVersion = version as CompanyVersionData; activities.push({ id: companyVersion.version_id, type: 'entity_change', @@ -203,7 +254,7 @@ export async function fetchSystemActivities( // Process ride model versions if (!modelVersions.error && modelVersions.data) { for (const version of modelVersions.data) { - const modelVersion = version as any; + const modelVersion = version as RideModelVersionData; activities.push({ id: modelVersion.version_id, type: 'entity_change', @@ -267,9 +318,9 @@ export async function fetchSystemActivities( const itemsMap = new Map(submissionItems?.map(item => [item.submission_id, item]) || []); for (const submission of submissions) { - const contentData = submission.content as any; + const contentData = submission.content as SubmissionContent; const submissionItem = itemsMap.get(submission.id); - const itemData = submissionItem?.item_data as any; + const itemData = submissionItem?.item_data as SubmissionItemData; // Build base details const details: SubmissionReviewDetails = {