From d9a912f44314972bd1713d00b7efbd1312e92a3f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:26:49 +0000 Subject: [PATCH] Fix: Enable TypeScript strict mode --- eslint.config.js | 9 +- src/components/auth/MFARemovalDialog.tsx | 9 +- .../moderation/useModerationQueueManager.ts | 2 +- src/hooks/moderation/useProfileCache.ts | 3 +- src/hooks/moderation/useQueueQuery.ts | 3 +- .../moderation/useRealtimeSubscriptions.ts | 3 +- src/lib/entitySubmissionHelpers.ts | 15 ++-- src/lib/errorHandler.ts | 26 ++++-- src/lib/logger.ts | 14 ++-- src/types/company-data.ts | 84 +++++++++++++++++++ src/types/supabase-session.ts | 35 ++++++++ 11 files changed, 174 insertions(+), 29 deletions(-) create mode 100644 src/types/company-data.ts create mode 100644 src/types/supabase-session.ts diff --git a/eslint.config.js b/eslint.config.js index a9c5fa28..db71a928 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,12 +20,19 @@ export default tseslint.config( rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-return": "error", + "@typescript-eslint/no-unsafe-argument": "error", + "@typescript-eslint/explicit-function-return-type": ["error", { + allowExpressions: true, + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowDirectConstAssertionInArrowFunctions: true, + }], }, }, ); diff --git a/src/components/auth/MFARemovalDialog.tsx b/src/components/auth/MFARemovalDialog.tsx index f0192f45..bf9e41fe 100644 --- a/src/components/auth/MFARemovalDialog.tsx +++ b/src/components/auth/MFARemovalDialog.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { toast } from 'sonner'; +import { getSessionAAL } from '@/types/supabase-session'; import { getErrorMessage } from '@/lib/errorHandler'; import { useRequireMFA } from '@/hooks/useRequireMFA'; import { @@ -36,9 +37,9 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF // Phase 1: Check AAL2 requirement on dialog open useEffect(() => { if (open) { - const checkAalLevel = async () => { + const checkAalLevel = async (): Promise => { const { data: { session } } = await supabase.auth.getSession(); - const currentAal = (session as any)?.aal || 'aal1'; + const currentAal = getSessionAAL(session); if (currentAal !== 'aal2') { toast.error('Please verify your identity with MFA before making security changes'); @@ -114,7 +115,7 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF // Phase 1: Verify session is at AAL2 after TOTP verification const { data: { session } } = await supabase.auth.getSession(); - const currentAal = (session as any)?.aal || 'aal1'; + const currentAal = getSessionAAL(session); if (currentAal !== 'aal2') { throw new Error('Session must be at AAL2 to remove MFA'); @@ -122,7 +123,7 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF toast.success('TOTP code verified'); setStep('confirm'); - } catch (error) { + } catch (error: unknown) { console.error('TOTP verification failed:', error); toast.error(getErrorMessage(error)); } finally { diff --git a/src/hooks/moderation/useModerationQueueManager.ts b/src/hooks/moderation/useModerationQueueManager.ts index 675c4195..85974a9a 100644 --- a/src/hooks/moderation/useModerationQueueManager.ts +++ b/src/hooks/moderation/useModerationQueueManager.ts @@ -220,7 +220,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): // Show error toast when query fails useEffect(() => { if (queueQuery.error) { - logger.error('❌ Queue query error:', queueQuery.error); + logger.error('❌ Queue query error:', { error: getErrorMessage(queueQuery.error) }); toast({ variant: 'destructive', title: 'Failed to Load Queue', diff --git a/src/hooks/moderation/useProfileCache.ts b/src/hooks/moderation/useProfileCache.ts index 3d3eca89..cd6c29f6 100644 --- a/src/hooks/moderation/useProfileCache.ts +++ b/src/hooks/moderation/useProfileCache.ts @@ -1,6 +1,7 @@ import { useRef, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/lib/logger'; +import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; /** @@ -105,7 +106,7 @@ export function useProfileCache() { .in('user_id', uncachedIds); if (error) { - logger.error('Error fetching profiles:', error); + logger.error('Error fetching profiles:', { error: getErrorMessage(error) }); return []; } diff --git a/src/hooks/moderation/useQueueQuery.ts b/src/hooks/moderation/useQueueQuery.ts index 65b41569..05a3b2fa 100644 --- a/src/hooks/moderation/useQueueQuery.ts +++ b/src/hooks/moderation/useQueueQuery.ts @@ -9,6 +9,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/lib/logger'; +import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import type { ModerationItem, @@ -126,7 +127,7 @@ export function useQueueQuery(config: UseQueueQueryConfig): UseQueueQueryReturn const result = await fetchSubmissions(supabase, queryConfig); if (result.error) { - logger.error('❌ [TanStack Query] Error:', result.error); + logger.error('❌ [TanStack Query] Error:', { error: getErrorMessage(result.error) }); throw result.error; } diff --git a/src/hooks/moderation/useRealtimeSubscriptions.ts b/src/hooks/moderation/useRealtimeSubscriptions.ts index d8835f73..df7496cc 100644 --- a/src/hooks/moderation/useRealtimeSubscriptions.ts +++ b/src/hooks/moderation/useRealtimeSubscriptions.ts @@ -9,6 +9,7 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/lib/logger'; +import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js'; import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation'; @@ -165,7 +166,7 @@ export function useRealtimeSubscriptions( .single(); if (error || !submission) { - logger.error('Error fetching submission details:', error); + logger.error('Error fetching submission details:', { error: getErrorMessage(error) }); return null; } diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 41d02ab8..cd742bea 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -4,6 +4,7 @@ import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { uploadPendingImages } from './imageUploadHelper'; import type { ProcessedImage } from './supabaseHelpers'; import { extractChangedFields } from './submissionChangeDetection'; +import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types/company-data'; /** * ═══════════════════════════════════════════════════════════════════ @@ -723,7 +724,7 @@ export async function submitManufacturerUpdate( item_type: 'manufacturer', action_type: 'edit', item_data: { - ...extractChangedFields(data, existingCompany as any), + ...extractChangedFields(data, existingCompany as Partial), company_id: companyId, // Always include for relational integrity company_type: 'manufacturer', // Always include for entity type discrimination images: processedImages as unknown as Json @@ -831,7 +832,7 @@ export async function submitDesignerUpdate( item_type: 'designer', action_type: 'edit', item_data: { - ...extractChangedFields(data, existingCompany as any), + ...extractChangedFields(data, existingCompany as Partial), company_id: companyId, // Always include for relational integrity company_type: 'designer', // Always include for entity type discrimination images: processedImages as unknown as Json @@ -939,7 +940,7 @@ export async function submitOperatorUpdate( item_type: 'operator', action_type: 'edit', item_data: { - ...extractChangedFields(data, existingCompany as any), + ...extractChangedFields(data, existingCompany as Partial), company_id: companyId, // Always include for relational integrity company_type: 'operator', // Always include for entity type discrimination images: processedImages as unknown as Json @@ -1047,7 +1048,7 @@ export async function submitPropertyOwnerUpdate( item_type: 'property_owner', action_type: 'edit', item_data: { - ...extractChangedFields(data, existingCompany as any), + ...extractChangedFields(data, existingCompany as Partial), company_id: companyId, // Always include for relational integrity company_type: 'property_owner', // Always include for entity type discrimination images: processedImages as unknown as Json @@ -1128,7 +1129,7 @@ export async function submitTimelineEvent( p_user_id: userId, p_submission_type: 'milestone', p_content: content, - p_items: items as any, + p_items: items as unknown as Json[], }); if (error || !submissionId) { @@ -1169,9 +1170,9 @@ export async function submitTimelineEventUpdate( } // Extract only changed fields from form data - const changedFields = extractChangedFields(data, originalEvent as any); + const changedFields = extractChangedFields(data, originalEvent as Partial>); - const itemData: Record = { + const itemData: Record = { ...changedFields, // Always include entity reference (for FK integrity) entity_type: originalEvent.entity_type, diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index f8e1a0d1..ed28a625 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -4,7 +4,7 @@ import { logger } from './logger'; export type ErrorContext = { action: string; userId?: string; - metadata?: Record; + metadata?: Record; }; export class AppError extends Error { @@ -66,11 +66,27 @@ export const handleInfo = ( * Type-safe error message extraction utility * Use this instead of `error: any` in catch blocks */ -export const getErrorMessage = (error: unknown): string => { - if (error instanceof Error) return error.message; - if (typeof error === 'string') return error; +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } if (error && typeof error === 'object' && 'message' in error) { return String(error.message); } return 'An unexpected error occurred'; -}; +} + +/** + * Type guard to check if error has a code property + */ +export function hasErrorCode(error: unknown): error is { code: string } { + return ( + error !== null && + typeof error === 'object' && + 'code' in error && + typeof (error as { code: unknown }).code === 'string' + ); +} diff --git a/src/lib/logger.ts b/src/lib/logger.ts index c2559158..28d9a2ee 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -7,24 +7,22 @@ const isDev = import.meta.env.DEV; -type LogContext = { - [key: string]: any; -}; +type LogContext = Record; export const logger = { - log: (...args: any[]) => { + log: (...args: unknown[]): void => { if (isDev) console.log(...args); }, - error: (message: string, context?: LogContext) => { + error: (message: string, context?: LogContext): void => { console.error(message, context); // Always log errors }, - warn: (...args: any[]) => { + warn: (...args: unknown[]): void => { if (isDev) console.warn(...args); }, - info: (...args: any[]) => { + info: (...args: unknown[]): void => { if (isDev) console.info(...args); }, - debug: (...args: any[]) => { + debug: (...args: unknown[]): void => { if (isDev) console.debug(...args); } }; diff --git a/src/types/company-data.ts b/src/types/company-data.ts new file mode 100644 index 00000000..402fb387 --- /dev/null +++ b/src/types/company-data.ts @@ -0,0 +1,84 @@ +/** + * Company Data Types + * + * Type-safe interfaces for company database records and form data. + * These types prevent `as any` casts when working with company entities. + */ + +export interface CompanyDatabaseRecord { + id: string; + name: string; + slug: string; + description?: string | null; + company_type: 'manufacturer' | 'designer' | 'operator' | 'property_owner'; + person_type: 'company' | 'individual' | 'firm' | 'organization'; + website_url?: string | null; + founded_year?: number | null; + headquarters_location?: string | null; + logo_url?: string | null; + banner_image_url?: string | null; + card_image_url?: string | null; + created_at: string; + updated_at: string; + // Stats fields + total_rides?: number; + average_rating?: number; + review_count?: number; +} + +export interface TimelineEventDatabaseRecord { + id: string; + entity_id: string; + entity_type: 'park' | 'ride' | 'company' | 'ride_model'; + event_type: string; + event_date: string; + event_date_precision: 'day' | 'month' | 'year'; + title: string; + description?: string | null; + from_value?: string | null; + to_value?: string | null; + from_entity_id?: string | null; + to_entity_id?: string | null; + from_location_id?: string | null; + to_location_id?: string | null; + display_order: number; + created_by?: string | null; + approved_by?: string | null; + submission_id?: string | null; + created_at: string; + updated_at: string; +} + +export interface LocationData { + country?: string; + state_province?: string; + city?: string; + latitude?: number; + longitude?: number; +} + +/** + * Type guard for company database records + */ +export function isCompanyRecord(data: unknown): data is CompanyDatabaseRecord { + return ( + typeof data === 'object' && + data !== null && + 'id' in data && + 'name' in data && + 'company_type' in data + ); +} + +/** + * Type guard for timeline event records + */ +export function isTimelineEventRecord(data: unknown): data is TimelineEventDatabaseRecord { + return ( + typeof data === 'object' && + data !== null && + 'id' in data && + 'entity_type' in data && + 'event_type' in data + ); +} diff --git a/src/types/supabase-session.ts b/src/types/supabase-session.ts new file mode 100644 index 00000000..059677d9 --- /dev/null +++ b/src/types/supabase-session.ts @@ -0,0 +1,35 @@ +/** + * Extended Supabase Session Types + * + * Supabase session objects contain additional properties not in the official types. + * These extended types provide type safety for AAL (Authentication Assurance Level) and other fields. + */ + +import type { Session } from '@supabase/supabase-js'; + +export type AALLevel = 'aal1' | 'aal2'; + +/** + * Extended session type with AAL property + * Supabase auth sessions include this property but it's not in the official types + */ +export interface ExtendedSession extends Session { + aal?: AALLevel; +} + +/** + * Type guard to check if session has AAL property + */ +export function hasAAL(session: Session | null): session is ExtendedSession { + return session !== null && 'aal' in session; +} + +/** + * Safely extract AAL level from session + */ +export function getSessionAAL(session: Session | null): AALLevel { + if (hasAAL(session) && session.aal) { + return session.aal; + } + return 'aal1'; +}