- 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>
257 lines
No EOL
9.2 KiB
TypeScript
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);
|
|
}); |