- 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.
180 lines
6.4 KiB
JavaScript
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);
|
|
});
|
|
});
|