ice/server.js
Deco Vander c0dc1f3c6d Fix critical error handling for ProfanityFilter initialization
- Add proper error handling to prevent undefined profanityFilter from being passed to routes
- Implement fallback no-op profanity filter strategy when initialization fails
- Add validation check before setupRoutes() to ensure profanityFilter is defined
- Provide clear error messages and security warnings when fallback is used
- Update graceful shutdown to safely handle both real and fallback profanity filters

Fallback profanity filter:
- Allows all content to pass through (security risk but prevents crash)
- Provides proper method signatures for API compatibility
- Logs prominent security warnings about disabled filtering
- Returns appropriate error messages for admin operations

This prevents runtime errors while maintaining service availability, with clear warnings about the security implications.
2025-07-04 13:16:33 -04:00

180 lines
6.4 KiB
JavaScript

require('dotenv').config({ path: '.env.local' });
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const cron = require('node-cron');
const ProfanityFilter = require('./profanity-filter');
// Import route modules
const configRoutes = require('./routes/config');
const locationRoutes = require('./routes/locations');
const adminRoutes = require('./routes/admin');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// Database setup
const db = new sqlite3.Database('icewatch.db');
console.log('Database connection established');
// Initialize profanity filter with its own database
let profanityFilter;
try {
profanityFilter = new ProfanityFilter();
console.log('Profanity filter initialized successfully with separate database');
} 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.');
// Create a fallback no-op profanity filter
profanityFilter = {
checkText: () => ({ isProfane: false, reason: null }),
addWord: () => Promise.resolve({ success: false, error: 'Profanity filter not available' }),
removeWord: () => Promise.resolve({ success: false, error: 'Profanity filter not available' }),
getWords: () => Promise.resolve([]),
testText: () => Promise.resolve({ isProfane: false, detectedWords: [], filteredText: '' }),
close: () => {}
};
console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!');
}
// Initialize database
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
latitude REAL,
longitude REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
description TEXT,
persistent INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
console.error('Error initializing database:', err);
} else {
console.log('Database initialized successfully');
// Add persistent column to existing tables if it doesn't exist
db.run(`ALTER TABLE locations ADD COLUMN persistent INTEGER DEFAULT 0`, (alterErr) => {
if (alterErr && !alterErr.message.includes('duplicate column name')) {
console.error('Error adding persistent column:', alterErr);
} else if (!alterErr) {
console.log('Added persistent column to existing table');
}
});
}
});
});
// Clean up expired locations (older than 48 hours, but not persistent ones)
const cleanupExpiredLocations = () => {
console.log('Running cleanup of expired locations');
const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
db.run('DELETE FROM locations WHERE created_at < ? AND persistent = 0', [fortyEightHoursAgo], function(err) {
if (err) {
console.error('Error cleaning up expired locations:', err);
} else {
console.log(`Cleaned up ${this.changes} expired locations (persistent reports preserved)`);
}
});
};
// Run cleanup every hour
console.log('Scheduling hourly cleanup task');
cron.schedule('0 * * * *', cleanupExpiredLocations);
// Configuration
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123'; // Change this!
// Authentication middleware
const authenticateAdmin = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.substring(7);
if (token !== ADMIN_PASSWORD) {
return res.status(401).json({ error: 'Invalid credentials' });
}
next();
};
// Setup routes after database and profanity filter are initialized
function setupRoutes() {
// API Routes
app.use('/api/config', configRoutes());
app.use('/api/locations', locationRoutes(db, profanityFilter));
app.use('/api/admin', adminRoutes(db, profanityFilter, authenticateAdmin));
// Static page routes
app.get('/', (req, res) => {
console.log('Serving the main page');
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/admin', (req, res) => {
console.log('Serving the admin page');
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
});
app.get('/privacy', (req, res) => {
console.log('Serving the privacy policy page');
res.sendFile(path.join(__dirname, 'public', 'privacy.html'));
});
}
// Validate profanity filter is properly initialized before setting up routes
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, () => {
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`);
console.log('===========================================');
});
// Graceful shutdown
process.on('SIGINT', () => {
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 main database
db.close((err) => {
if (err) {
console.error('Error closing database:', err);
} else {
console.log('Main database connection closed.');
}
process.exit(0);
});
});