ice/src/server.ts
Claude Code c4cf921a54 Add comprehensive TypeScript support and conversion
- Convert entire backend to TypeScript with strict type checking
- Add comprehensive type definitions and interfaces
- Create typed models for Location and ProfanityWord with database operations
- Convert all services to TypeScript (DatabaseService, ProfanityFilterService)
- Convert all API routes with proper request/response typing
- Add TypeScript build system and development scripts
- Update package.json with TypeScript dependencies and scripts
- Configure tsconfig.json with strict typing and build settings
- Update CLAUDE.md documentation for TypeScript development
- Add .gitignore rules for TypeScript build artifacts

Architecture improvements:
- Full type safety throughout the application
- Typed database operations and API endpoints
- Proper error handling with typed exceptions
- Strict optional property handling
- Type-safe dependency injection for routes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 21:15:29 -04:00

257 lines
No EOL
9.2 KiB
TypeScript

import dotenv from 'dotenv';
import express, { Request, Response, NextFunction, Application } from 'express';
import cors from 'cors';
import path from 'path';
import cron from 'node-cron';
// Load environment variables
dotenv.config({ path: '.env.local' });
dotenv.config();
// Import services and models
import DatabaseService from './services/DatabaseService';
import ProfanityFilterService from './services/ProfanityFilterService';
// Import route modules
import configRoutes from './routes/config';
import locationRoutes from './routes/locations';
import adminRoutes from './routes/admin';
// Import types
import { Location, ProfanityWord } from './types';
const app: Application = express();
const PORT: number = parseInt(process.env.PORT || '3000', 10);
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// Database and services setup
const databaseService = new DatabaseService();
let profanityFilter: ProfanityFilterService | FallbackFilter;
// Fallback filter interface for type safety
interface FallbackFilter {
containsProfanity(): boolean;
analyzeProfanity(text: string): {
hasProfanity: boolean;
matches: any[];
severity: string;
count: number;
filtered: string;
};
filterProfanity(text: string): string;
addCustomWord(word: string, severity: string, category: string, createdBy?: string): Promise<any>;
removeCustomWord(wordId: number): Promise<any>;
updateCustomWord(wordId: number, updates: any): Promise<any>;
getCustomWords(): Promise<any[]>;
loadCustomWords(): Promise<void>;
getAllWords(): any[];
getSeverity(): string;
getSeverityLevel(): number;
getSeverityName(): string;
normalizeText(text: string): string;
buildPatterns(): any[];
close(): void;
_isFallback: boolean;
}
// Create fallback filter function
function createFallbackFilter(): FallbackFilter {
return {
// Core profanity checking methods
containsProfanity: (): boolean => false,
analyzeProfanity: (text: string) => ({
hasProfanity: false,
matches: [],
severity: 'none',
count: 0,
filtered: text || ''
}),
filterProfanity: (text: string): string => text || '',
// Database management methods used by admin routes
addCustomWord: async (word: string, severity: string, category: string, createdBy?: string) => ({
id: null,
word: word || null,
severity: severity || null,
category: category || null,
createdBy: createdBy || null,
success: false,
error: 'Profanity filter not available - please check server configuration'
}),
removeCustomWord: async (wordId: number) => ({
success: false,
error: 'Profanity filter not available - please check server configuration'
}),
updateCustomWord: async (wordId: number, updates: any) => ({
success: false,
error: 'Profanity filter not available - please check server configuration'
}),
getCustomWords: async (): Promise<any[]> => [],
loadCustomWords: async (): Promise<void> => {},
// Utility methods
getAllWords: (): any[] => [],
getSeverity: (): string => 'none',
getSeverityLevel: (): number => 0,
getSeverityName: (): string => 'none',
normalizeText: (text: string): string => text || '',
buildPatterns: (): any[] => [],
// Cleanup method
close: (): void => {},
// Special property to identify this as a fallback filter
_isFallback: true
};
}
// Initialize profanity filter asynchronously
async function initializeProfanityFilter(): Promise<void> {
try {
const profanityWordModel = databaseService.getProfanityWordModel();
profanityFilter = new ProfanityFilterService(profanityWordModel);
await profanityFilter.initialize();
console.log('Profanity filter initialized successfully');
} catch (error) {
console.error('WARNING: Failed to initialize profanity filter:', error);
console.error('Creating fallback no-op profanity filter. ALL CONTENT WILL BE ALLOWED!');
console.error('This is a security risk - please fix the profanity filter configuration.');
profanityFilter = createFallbackFilter();
console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!');
}
}
// Clean up expired locations (older than 48 hours, but not persistent ones)
const cleanupExpiredLocations = async (): Promise<void> => {
console.log('Running cleanup of expired locations');
try {
const locationModel = databaseService.getLocationModel();
const result = await locationModel.cleanupExpired();
console.log(`Cleaned up ${result.changes} expired locations (persistent reports preserved)`);
} catch (err) {
console.error('Error cleaning up expired locations:', err);
}
};
// Run cleanup every hour
console.log('Scheduling hourly cleanup task');
cron.schedule('0 * * * *', cleanupExpiredLocations);
// Configuration
const ADMIN_PASSWORD: string = process.env.ADMIN_PASSWORD || 'admin123'; // Change this!
// Authentication middleware
const authenticateAdmin = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const token = authHeader.substring(7);
if (token !== ADMIN_PASSWORD) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
next();
};
// Setup routes after database and profanity filter are initialized
function setupRoutes(): void {
const locationModel = databaseService.getLocationModel();
const profanityWordModel = databaseService.getProfanityWordModel();
// API Routes
app.use('/api/config', configRoutes());
app.use('/api/locations', locationRoutes(locationModel, profanityFilter));
app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, profanityFilter, authenticateAdmin));
// Static page routes
app.get('/', (req: Request, res: Response): void => {
console.log('Serving the main page');
res.sendFile(path.join(__dirname, '../../public', 'index.html'));
});
app.get('/admin', (req: Request, res: Response): void => {
console.log('Serving the admin page');
res.sendFile(path.join(__dirname, '../../public', 'admin.html'));
});
app.get('/privacy', (req: Request, res: Response): void => {
console.log('Serving the privacy policy page');
res.sendFile(path.join(__dirname, '../../public', 'privacy.html'));
});
}
// Async server startup function
async function startServer(): Promise<void> {
try {
// Initialize database service first
await databaseService.initialize();
console.log('Database service initialized successfully');
// Initialize profanity filter
await initializeProfanityFilter();
// Validate profanity filter is properly initialized
if (!profanityFilter) {
console.error('CRITICAL ERROR: profanityFilter is undefined after initialization attempt.');
console.error('Cannot start server without a functional profanity filter.');
process.exit(1);
}
// Initialize routes after everything is set up
setupRoutes();
// Start server
app.listen(PORT, (): void => {
console.log('===========================================');
console.log('Great Lakes Ice Report server started');
console.log(`Listening on port ${PORT}`);
console.log(`Visit http://localhost:${PORT} to view the website`);
// Display profanity filter status
if ('_isFallback' in profanityFilter && profanityFilter._isFallback) {
console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)');
console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!');
} else {
console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL');
}
console.log('===========================================');
});
} catch (error) {
console.error('CRITICAL ERROR: Failed to start server:', error);
process.exit(1);
}
}
// Start the server
startServer();
// Graceful shutdown
process.on('SIGINT', (): void => {
console.log('\nShutting down server...');
// Close profanity filter database first
if (profanityFilter && typeof profanityFilter.close === 'function') {
try {
profanityFilter.close();
console.log('Profanity filter database closed.');
} catch (error) {
console.error('Error closing profanity filter:', error);
}
}
// Close database service
databaseService.close();
console.log('Database connections closed.');
process.exit(0);
});