Fix: Enable TypeScript strict mode

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 00:26:49 +00:00
parent 84188b94f2
commit d9a912f443
11 changed files with 174 additions and 29 deletions

View File

@@ -20,12 +20,19 @@ export default tseslint.config(
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "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-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-return": "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,
}],
}, },
}, },
); );

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getSessionAAL } from '@/types/supabase-session';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage } from '@/lib/errorHandler';
import { useRequireMFA } from '@/hooks/useRequireMFA'; import { useRequireMFA } from '@/hooks/useRequireMFA';
import { import {
@@ -36,9 +37,9 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF
// Phase 1: Check AAL2 requirement on dialog open // Phase 1: Check AAL2 requirement on dialog open
useEffect(() => { useEffect(() => {
if (open) { if (open) {
const checkAalLevel = async () => { const checkAalLevel = async (): Promise<void> => {
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
const currentAal = (session as any)?.aal || 'aal1'; const currentAal = getSessionAAL(session);
if (currentAal !== 'aal2') { if (currentAal !== 'aal2') {
toast.error('Please verify your identity with MFA before making security changes'); 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 // Phase 1: Verify session is at AAL2 after TOTP verification
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
const currentAal = (session as any)?.aal || 'aal1'; const currentAal = getSessionAAL(session);
if (currentAal !== 'aal2') { if (currentAal !== 'aal2') {
throw new Error('Session must be at AAL2 to remove MFA'); 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'); toast.success('TOTP code verified');
setStep('confirm'); setStep('confirm');
} catch (error) { } catch (error: unknown) {
console.error('TOTP verification failed:', error); console.error('TOTP verification failed:', error);
toast.error(getErrorMessage(error)); toast.error(getErrorMessage(error));
} finally { } finally {

View File

@@ -220,7 +220,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
// Show error toast when query fails // Show error toast when query fails
useEffect(() => { useEffect(() => {
if (queueQuery.error) { if (queueQuery.error) {
logger.error('❌ Queue query error:', queueQuery.error); logger.error('❌ Queue query error:', { error: getErrorMessage(queueQuery.error) });
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: 'Failed to Load Queue', title: 'Failed to Load Queue',

View File

@@ -1,6 +1,7 @@
import { useRef, useCallback } from 'react'; import { useRef, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
/** /**
@@ -105,7 +106,7 @@ export function useProfileCache() {
.in('user_id', uncachedIds); .in('user_id', uncachedIds);
if (error) { if (error) {
logger.error('Error fetching profiles:', error); logger.error('Error fetching profiles:', { error: getErrorMessage(error) });
return []; return [];
} }

View File

@@ -9,6 +9,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries'; import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
import type { import type {
ModerationItem, ModerationItem,
@@ -126,7 +127,7 @@ export function useQueueQuery(config: UseQueueQueryConfig): UseQueueQueryReturn
const result = await fetchSubmissions(supabase, queryConfig); const result = await fetchSubmissions(supabase, queryConfig);
if (result.error) { if (result.error) {
logger.error('❌ [TanStack Query] Error:', result.error); logger.error('❌ [TanStack Query] Error:', { error: getErrorMessage(result.error) });
throw result.error; throw result.error;
} }

View File

@@ -9,6 +9,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js'; import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation'; import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation';
@@ -165,7 +166,7 @@ export function useRealtimeSubscriptions(
.single(); .single();
if (error || !submission) { if (error || !submission) {
logger.error('Error fetching submission details:', error); logger.error('Error fetching submission details:', { error: getErrorMessage(error) });
return null; return null;
} }

View File

@@ -4,6 +4,7 @@ import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
import { uploadPendingImages } from './imageUploadHelper'; import { uploadPendingImages } from './imageUploadHelper';
import type { ProcessedImage } from './supabaseHelpers'; import type { ProcessedImage } from './supabaseHelpers';
import { extractChangedFields } from './submissionChangeDetection'; import { extractChangedFields } from './submissionChangeDetection';
import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types/company-data';
/** /**
* ═══════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════
@@ -723,7 +724,7 @@ export async function submitManufacturerUpdate(
item_type: 'manufacturer', item_type: 'manufacturer',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingCompany as any), ...extractChangedFields(data, existingCompany as Partial<CompanyDatabaseRecord>),
company_id: companyId, // Always include for relational integrity company_id: companyId, // Always include for relational integrity
company_type: 'manufacturer', // Always include for entity type discrimination company_type: 'manufacturer', // Always include for entity type discrimination
images: processedImages as unknown as Json images: processedImages as unknown as Json
@@ -831,7 +832,7 @@ export async function submitDesignerUpdate(
item_type: 'designer', item_type: 'designer',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingCompany as any), ...extractChangedFields(data, existingCompany as Partial<CompanyDatabaseRecord>),
company_id: companyId, // Always include for relational integrity company_id: companyId, // Always include for relational integrity
company_type: 'designer', // Always include for entity type discrimination company_type: 'designer', // Always include for entity type discrimination
images: processedImages as unknown as Json images: processedImages as unknown as Json
@@ -939,7 +940,7 @@ export async function submitOperatorUpdate(
item_type: 'operator', item_type: 'operator',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingCompany as any), ...extractChangedFields(data, existingCompany as Partial<CompanyDatabaseRecord>),
company_id: companyId, // Always include for relational integrity company_id: companyId, // Always include for relational integrity
company_type: 'operator', // Always include for entity type discrimination company_type: 'operator', // Always include for entity type discrimination
images: processedImages as unknown as Json images: processedImages as unknown as Json
@@ -1047,7 +1048,7 @@ export async function submitPropertyOwnerUpdate(
item_type: 'property_owner', item_type: 'property_owner',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingCompany as any), ...extractChangedFields(data, existingCompany as Partial<CompanyDatabaseRecord>),
company_id: companyId, // Always include for relational integrity company_id: companyId, // Always include for relational integrity
company_type: 'property_owner', // Always include for entity type discrimination company_type: 'property_owner', // Always include for entity type discrimination
images: processedImages as unknown as Json images: processedImages as unknown as Json
@@ -1128,7 +1129,7 @@ export async function submitTimelineEvent(
p_user_id: userId, p_user_id: userId,
p_submission_type: 'milestone', p_submission_type: 'milestone',
p_content: content, p_content: content,
p_items: items as any, p_items: items as unknown as Json[],
}); });
if (error || !submissionId) { if (error || !submissionId) {
@@ -1169,9 +1170,9 @@ export async function submitTimelineEventUpdate(
} }
// Extract only changed fields from form data // Extract only changed fields from form data
const changedFields = extractChangedFields(data, originalEvent as any); const changedFields = extractChangedFields(data, originalEvent as Partial<Record<string, unknown>>);
const itemData: Record<string, any> = { const itemData: Record<string, unknown> = {
...changedFields, ...changedFields,
// Always include entity reference (for FK integrity) // Always include entity reference (for FK integrity)
entity_type: originalEvent.entity_type, entity_type: originalEvent.entity_type,

View File

@@ -4,7 +4,7 @@ import { logger } from './logger';
export type ErrorContext = { export type ErrorContext = {
action: string; action: string;
userId?: string; userId?: string;
metadata?: Record<string, any>; metadata?: Record<string, unknown>;
}; };
export class AppError extends Error { export class AppError extends Error {
@@ -66,11 +66,27 @@ export const handleInfo = (
* Type-safe error message extraction utility * Type-safe error message extraction utility
* Use this instead of `error: any` in catch blocks * Use this instead of `error: any` in catch blocks
*/ */
export const getErrorMessage = (error: unknown): string => { export function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message; if (error instanceof Error) {
if (typeof error === 'string') return error; return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object' && 'message' in error) { if (error && typeof error === 'object' && 'message' in error) {
return String(error.message); return String(error.message);
} }
return 'An unexpected error occurred'; 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'
);
}

View File

@@ -7,24 +7,22 @@
const isDev = import.meta.env.DEV; const isDev = import.meta.env.DEV;
type LogContext = { type LogContext = Record<string, unknown>;
[key: string]: any;
};
export const logger = { export const logger = {
log: (...args: any[]) => { log: (...args: unknown[]): void => {
if (isDev) console.log(...args); if (isDev) console.log(...args);
}, },
error: (message: string, context?: LogContext) => { error: (message: string, context?: LogContext): void => {
console.error(message, context); // Always log errors console.error(message, context); // Always log errors
}, },
warn: (...args: any[]) => { warn: (...args: unknown[]): void => {
if (isDev) console.warn(...args); if (isDev) console.warn(...args);
}, },
info: (...args: any[]) => { info: (...args: unknown[]): void => {
if (isDev) console.info(...args); if (isDev) console.info(...args);
}, },
debug: (...args: any[]) => { debug: (...args: unknown[]): void => {
if (isDev) console.debug(...args); if (isDev) console.debug(...args);
} }
}; };

84
src/types/company-data.ts Normal file
View File

@@ -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
);
}

View File

@@ -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';
}