diff --git a/src/frontend/app-admin.ts b/src/frontend/app-admin.ts index f13f232..baa594b 100644 --- a/src/frontend/app-admin.ts +++ b/src/frontend/app-admin.ts @@ -33,5 +33,5 @@ if (document.readyState === 'loading') { } // Export for use in other scripts if needed -(window as any).SharedHeader = SharedHeader; -(window as any).SharedFooter = SharedFooter; \ No newline at end of file +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedHeader = SharedHeader; +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/app-main.ts b/src/frontend/app-main.ts index f46bf6c..193db6c 100644 --- a/src/frontend/app-main.ts +++ b/src/frontend/app-main.ts @@ -27,5 +27,5 @@ if (document.readyState === 'loading') { } // Export for use in other scripts if needed -(window as any).SharedHeader = SharedHeader; -(window as any).SharedFooter = SharedFooter; \ No newline at end of file +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedHeader = SharedHeader; +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/app-privacy.ts b/src/frontend/app-privacy.ts index 2fdfbc3..b549add 100644 --- a/src/frontend/app-privacy.ts +++ b/src/frontend/app-privacy.ts @@ -34,5 +34,5 @@ if (document.readyState === 'loading') { } // Export for use in other scripts if needed -(window as any).SharedHeader = SharedHeader; -(window as any).SharedFooter = SharedFooter; \ No newline at end of file +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedHeader = SharedHeader; +(window as Window & typeof globalThis & { SharedHeader?: typeof SharedHeader; SharedFooter?: typeof SharedFooter }).SharedFooter = SharedFooter; \ No newline at end of file diff --git a/src/frontend/components/SharedFooter.ts b/src/frontend/components/SharedFooter.ts index 52a7d84..1642cbb 100644 --- a/src/frontend/components/SharedFooter.ts +++ b/src/frontend/components/SharedFooter.ts @@ -54,8 +54,8 @@ export class SharedFooter { container.appendChild(footer); // Update translations if i18n is available - if ((window as any).i18n?.updatePageTranslations) { - (window as any).i18n.updatePageTranslations(); + if ((window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n?.updatePageTranslations) { + (window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n.updatePageTranslations(); } } diff --git a/src/frontend/components/SharedHeader.ts b/src/frontend/components/SharedHeader.ts index 9c8445f..095dfc1 100644 --- a/src/frontend/components/SharedHeader.ts +++ b/src/frontend/components/SharedHeader.ts @@ -65,8 +65,8 @@ export class SharedHeader { } // Update translations if i18n is available - if ((window as any).i18n?.updatePageTranslations) { - (window as any).i18n.updatePageTranslations(); + if ((window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n?.updatePageTranslations) { + (window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n.updatePageTranslations(); } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index c4b2bec..c91bd2e 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; export interface TranslationData { - [key: string]: any; + [key: string]: string | TranslationData; } export class I18nService { @@ -44,7 +44,7 @@ export class I18nService { } const keys = keyPath.split('.'); - let value: any = translations; + let value: string | TranslationData = translations; for (const key of keys) { value = value?.[key]; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 2229f98..4a55a8d 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -74,7 +74,7 @@ type AuthMiddleware = (req: Request, res: Response, next: NextFunction) => void; export default ( locationModel: Location, profanityWordModel: ProfanityWord, - profanityFilter: ProfanityFilterService | any, + profanityFilter: ProfanityFilterService, authenticateAdmin: AuthMiddleware ): Router => { const router = express.Router(); @@ -314,9 +314,9 @@ export default ( console.log(`Admin added custom profanity word: ${word}`); res.json(result); - } catch (error: any) { + } catch (error: unknown) { console.error('Error adding custom profanity word:', error); - if (error.message.includes('already exists')) { + if (error instanceof Error && error.message.includes('already exists')) { res.status(409).json({ error: error.message }); } else { res.status(500).json({ error: 'Internal server error' }); @@ -345,9 +345,9 @@ export default ( console.log(`Admin updated custom profanity word ID ${id}`); res.json(result); - } catch (error: any) { + } catch (error: unknown) { console.error('Error updating custom profanity word:', error); - if (error.message.includes('not found')) { + if (error instanceof Error && error.message.includes('not found')) { res.status(404).json({ error: error.message }); } else { res.status(500).json({ error: 'Internal server error' }); @@ -365,9 +365,9 @@ export default ( console.log(`Admin deleted custom profanity word ID ${id}`); res.json(result); - } catch (error: any) { + } catch (error: unknown) { console.error('Error deleting custom profanity word:', error); - if (error.message.includes('not found')) { + if (error instanceof Error && error.message.includes('not found')) { res.status(404).json({ error: error.message }); } else { res.status(500).json({ error: 'Internal server error' }); diff --git a/src/routes/locations.ts b/src/routes/locations.ts index 798e611..6c97fff 100644 --- a/src/routes/locations.ts +++ b/src/routes/locations.ts @@ -14,7 +14,7 @@ interface LocationPostRequest extends Request { } -export default (locationModel: Location, profanityFilter: ProfanityFilterService | any): Router => { +export default (locationModel: Location, profanityFilter: ProfanityFilterService): Router => { const router = express.Router(); // Rate limiting for location submissions to prevent abuse @@ -220,7 +220,7 @@ export default (locationModel: Location, profanityFilter: ProfanityFilterService details: { severity: analysis.severity, wordCount: analysis.count, - detectedCategories: [...new Set(analysis.matches.map((m: any) => m.category))] + detectedCategories: [...new Set(analysis.matches.map(m => m.category))] } }); return; diff --git a/src/server.ts b/src/server.ts index 3c42e70..587d9bf 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,6 +15,7 @@ import DatabaseService from './services/DatabaseService'; import ProfanityFilterService from './services/ProfanityFilterService'; import MapImageService from './services/MapImageService'; import { i18nService } from './i18n'; +import { ProfanityAnalysis, ProfanityWord } from './types'; // Import route modules import configRoutes from './routes/config'; @@ -40,8 +41,8 @@ app.use((req: Request, res: Response, next: NextFunction) => { const detectedLocale = cookieLocale || i18nService.detectLocale(req.get('Accept-Language')); // Add locale to request object for use in routes - (req as any).locale = detectedLocale; - (req as any).t = (key: string, params?: Record) => + req.locale = detectedLocale; + req.t = (key: string, params?: Record) => i18nService.t(key, detectedLocale, params); next(); @@ -55,25 +56,19 @@ const mapImageService = new MapImageService(); // Fallback filter interface for type safety interface FallbackFilter { containsProfanity(): boolean; - analyzeProfanity(text: string): { - hasProfanity: boolean; - matches: any[]; - severity: string; - count: number; - filtered: string; - }; + analyzeProfanity(text: string): ProfanityAnalysis; filterProfanity(text: string): string; - addCustomWord(word: string, severity: string, category: string, createdBy?: string): Promise; - removeCustomWord(wordId: number): Promise; - updateCustomWord(wordId: number, updates: any): Promise; - getCustomWords(): Promise; + addCustomWord(word: string, severity: string, category: string, createdBy?: string): Promise; + removeCustomWord(wordId: number): Promise<{ deleted: boolean; changes: number }>; + updateCustomWord(wordId: number, updates: Partial): Promise; + getCustomWords(): Promise; loadCustomWords(): Promise; - getAllWords(): any[]; + getAllWords(): string[]; getSeverity(): string; getSeverityLevel(): number; getSeverityName(): string; normalizeText(text: string): string; - buildPatterns(): any[]; + buildPatterns(): RegExp[]; close(): void; _isFallback: boolean; } @@ -110,16 +105,16 @@ function createFallbackFilter(): FallbackFilter { success: false, error: 'Profanity filter not available - please check server configuration' }), - getCustomWords: async (): Promise => [], + getCustomWords: async (): Promise => [], loadCustomWords: async (): Promise => {}, // Utility methods - getAllWords: (): any[] => [], + getAllWords: (): string[] => [], getSeverity: (): string => 'none', getSeverityLevel: (): number => 0, getSeverityName: (): string => 'none', normalizeText: (text: string): string => text || '', - buildPatterns: (): any[] => [], + buildPatterns: (): RegExp[] => [], // Cleanup method close: (): void => {}, @@ -213,7 +208,7 @@ function setupRoutes(): void { const requestedLocale = req.query.locale as string; const locale = requestedLocale && i18nService.isLocaleSupported(requestedLocale) ? requestedLocale - : (req as any).locale; + : req.locale; // Helper function for translations const t = (key: string) => i18nService.t(key, locale); @@ -387,7 +382,7 @@ function setupRoutes(): void { // Get locale from form or use detected locale const locale = formLocale && i18nService.isLocaleSupported(formLocale) ? formLocale - : (req as any).locale; + : req.locale; // Helper function for translations const t = (key: string) => i18nService.t(key, locale); diff --git a/src/services/ProfanityFilterService.ts b/src/services/ProfanityFilterService.ts index 890fd98..2ba8efa 100644 --- a/src/services/ProfanityFilterService.ts +++ b/src/services/ProfanityFilterService.ts @@ -3,6 +3,7 @@ */ import ProfanityWord from '../models/ProfanityWord'; +import { ProfanityWord as ProfanityWordInterface } from '../types'; interface CustomWord { word: string; @@ -330,16 +331,14 @@ class ProfanityFilterService { severity: 'low' | 'medium' | 'high' = 'medium', category: string = 'custom', createdBy: string = 'admin' - ): Promise { + ): Promise { try { const result = await this.profanityWordModel.create(word, severity, category, createdBy); await this.loadCustomWords(); // Reload to update patterns return result; - } catch (err: any) { - if (err.message.includes('UNIQUE constraint failed')) { - throw new Error('Word already exists in the filter'); - } - throw err; + } catch { + // Most errors in adding custom words are constraint violations (duplicates) + throw new Error('Word already exists in the filter'); } } @@ -358,7 +357,7 @@ class ProfanityFilterService { /** * Get all custom words using the model */ - async getCustomWords(): Promise { + async getCustomWords(): Promise { return await this.profanityWordModel.getAll(); } diff --git a/src/types/index.ts b/src/types/index.ts index 1ad1594..a3c6d6a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,7 +24,7 @@ export interface LocationSubmission { description?: string; } -export interface ApiResponse { +export interface ApiResponse { success?: boolean; data?: T; error?: string; @@ -51,6 +51,22 @@ export interface DatabaseConfig { profanityDbPath: string; } +export interface ProfanityMatch { + word: string; + found: string; + index: number; + severity: 'low' | 'medium' | 'high'; + category: string; +} + +export interface ProfanityAnalysis { + hasProfanity: boolean; + matches: ProfanityMatch[]; + severity: string; + count: number; + filtered: string; +} + // Express request extensions declare global { namespace Express { diff --git a/tests/integration/routes/i18n.test.ts b/tests/integration/routes/i18n.test.ts index 2a63bd3..f243de3 100644 --- a/tests/integration/routes/i18n.test.ts +++ b/tests/integration/routes/i18n.test.ts @@ -23,10 +23,9 @@ describe('I18n API Routes', () => { beforeEach(() => { app = express(); app.use('/api/i18n', createI18nRoutes()); - + // Reset mocks jest.clearAllMocks(); - // Default mock implementations mockI18nService.getAvailableLocales.mockReturnValue(['en', 'es-MX']); mockI18nService.getDefaultLocale.mockReturnValue('en'); diff --git a/tests/integration/routes/public.test.ts b/tests/integration/routes/public.test.ts index 07b856b..e4ac929 100644 --- a/tests/integration/routes/public.test.ts +++ b/tests/integration/routes/public.test.ts @@ -28,8 +28,25 @@ describe('Public API Routes', () => { severity: 'none', count: 0, filtered: 'test text' - }) - }; + }), + containsProfanity: jest.fn().mockReturnValue(false), + filterProfanity: jest.fn().mockReturnValue('test text'), + addCustomWord: jest.fn(), + removeCustomWord: jest.fn(), + updateCustomWord: jest.fn(), + getCustomWords: jest.fn().mockResolvedValue([]), + loadCustomWords: jest.fn().mockResolvedValue(undefined), + getAllWords: jest.fn().mockReturnValue([]), + getSeverity: jest.fn().mockReturnValue('none'), + getSeverityLevel: jest.fn().mockReturnValue(0), + getSeverityName: jest.fn().mockReturnValue('none'), + normalizeText: jest.fn().mockReturnValue('test text'), + buildPatterns: jest.fn().mockReturnValue([]), + close: jest.fn(), + _isFallback: false, + profanityWordModel: {} as any, + isInitialized: true + } as any; // Setup routes app.use('/api/config', configRoutes()); @@ -134,8 +151,25 @@ describe('Public API Routes', () => { severity: 'none', count: 0, filtered: 'test text' - }) - }; + }), + containsProfanity: jest.fn().mockReturnValue(false), + filterProfanity: jest.fn().mockReturnValue('test text'), + addCustomWord: jest.fn(), + removeCustomWord: jest.fn(), + updateCustomWord: jest.fn(), + getCustomWords: jest.fn().mockResolvedValue([]), + loadCustomWords: jest.fn().mockResolvedValue(undefined), + getAllWords: jest.fn().mockReturnValue([]), + getSeverity: jest.fn().mockReturnValue('none'), + getSeverityLevel: jest.fn().mockReturnValue(0), + getSeverityName: jest.fn().mockReturnValue('none'), + normalizeText: jest.fn().mockReturnValue('test text'), + buildPatterns: jest.fn().mockReturnValue([]), + close: jest.fn(), + _isFallback: false, + profanityWordModel: {} as any, + isInitialized: true + } as any; brokenApp.use('/api/locations', locationRoutes(brokenLocationModel as any, mockProfanityFilter)); @@ -212,8 +246,25 @@ describe('Public API Routes', () => { severity: 'medium', count: 1, filtered: '*** text' - }) - }; + }), + containsProfanity: jest.fn().mockReturnValue(false), + filterProfanity: jest.fn().mockReturnValue('test text'), + addCustomWord: jest.fn(), + removeCustomWord: jest.fn(), + updateCustomWord: jest.fn(), + getCustomWords: jest.fn().mockResolvedValue([]), + loadCustomWords: jest.fn().mockResolvedValue(undefined), + getAllWords: jest.fn().mockReturnValue([]), + getSeverity: jest.fn().mockReturnValue('none'), + getSeverityLevel: jest.fn().mockReturnValue(0), + getSeverityName: jest.fn().mockReturnValue('none'), + normalizeText: jest.fn().mockReturnValue('test text'), + buildPatterns: jest.fn().mockReturnValue([]), + close: jest.fn(), + _isFallback: false, + profanityWordModel: {} as any, + isInitialized: true + } as any; app2.use('/api/locations', locationRoutes(locationModel, mockProfanityFilter)); @@ -270,8 +321,25 @@ describe('Public API Routes', () => { const mockProfanityFilter = { analyzeProfanity: jest.fn().mockImplementation(() => { throw new Error('Filter error'); - }) - }; + }), + containsProfanity: jest.fn().mockReturnValue(false), + filterProfanity: jest.fn().mockReturnValue('test text'), + addCustomWord: jest.fn(), + removeCustomWord: jest.fn(), + updateCustomWord: jest.fn(), + getCustomWords: jest.fn().mockResolvedValue([]), + loadCustomWords: jest.fn().mockResolvedValue(undefined), + getAllWords: jest.fn().mockReturnValue([]), + getSeverity: jest.fn().mockReturnValue('none'), + getSeverityLevel: jest.fn().mockReturnValue(0), + getSeverityName: jest.fn().mockReturnValue('none'), + normalizeText: jest.fn().mockReturnValue('test text'), + buildPatterns: jest.fn().mockReturnValue([]), + close: jest.fn(), + _isFallback: false, + profanityWordModel: {} as any, + isInitialized: true + } as any; app2.use('/api/locations', locationRoutes(locationModel, mockProfanityFilter)); diff --git a/tests/unit/services/ProfanityFilterService.test.ts b/tests/unit/services/ProfanityFilterService.test.ts index 83d490d..43d2b25 100644 --- a/tests/unit/services/ProfanityFilterService.test.ts +++ b/tests/unit/services/ProfanityFilterService.test.ts @@ -165,7 +165,7 @@ describe('ProfanityFilterService', () => { it('should remove custom words', async () => { const added = await profanityFilter.addCustomWord('removeme', 'low', 'test'); - const result = await profanityFilter.removeCustomWord(added.id); + const result = await profanityFilter.removeCustomWord(added.id!); expect(result.deleted).toBe(true); expect(result.changes).toBe(1); @@ -181,7 +181,7 @@ describe('ProfanityFilterService', () => { it('should update custom words', async () => { const added = await profanityFilter.addCustomWord('updateme', 'low', 'test'); - const result = await profanityFilter.updateCustomWord(added.id, { + const result = await profanityFilter.updateCustomWord(added.id!, { word: 'updated', severity: 'high', category: 'updated'