- Add async initialize() method for reliable initialization waiting - Add static create() factory method for easy async creation - Add initialization state tracking with isInitialized flag - Add warning system for methods called before full initialization - Update server.js to use proper async initialization pattern - Maintain backward compatibility with constructor-only usage - Add accessibility improvement for reduced motion preferences in CSS Fixes the race condition issue where consumers relied on arbitrary timeouts instead of properly waiting for async initialization to complete.
251 lines
8.7 KiB
JavaScript
251 lines
8.7 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 (async)
|
|
let profanityFilter;
|
|
|
|
// Create fallback filter function
|
|
function createFallbackFilter() {
|
|
return {
|
|
// Core profanity checking methods
|
|
checkText: () => ({ isProfane: false, reason: null }),
|
|
containsProfanity: () => false,
|
|
analyzeProfanity: (text) => ({
|
|
hasProfanity: false,
|
|
matches: [],
|
|
severity: 'none',
|
|
count: 0,
|
|
filtered: text || ''
|
|
}),
|
|
filterProfanity: (text) => text || '',
|
|
|
|
// Database management methods used by admin routes
|
|
addCustomWord: (word, severity, category, createdBy) => Promise.resolve({
|
|
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: (wordId) => Promise.resolve({
|
|
success: false,
|
|
error: 'Profanity filter not available - please check server configuration'
|
|
}),
|
|
updateCustomWord: (wordId, updates) => Promise.resolve({
|
|
success: false,
|
|
error: 'Profanity filter not available - please check server configuration'
|
|
}),
|
|
getCustomWords: () => Promise.resolve([]),
|
|
loadCustomWords: () => Promise.resolve(),
|
|
|
|
// Utility methods
|
|
getAllWords: () => [],
|
|
getSeverity: () => 'none',
|
|
getSeverityLevel: () => 0,
|
|
getSeverityName: () => 'none',
|
|
normalizeText: (text) => text || '',
|
|
buildPatterns: () => [],
|
|
|
|
// Cleanup method
|
|
close: () => {},
|
|
|
|
// Special property to identify this as a fallback filter
|
|
_isFallback: true
|
|
};
|
|
}
|
|
|
|
// Initialize profanity filter asynchronously
|
|
async function initializeProfanityFilter() {
|
|
try {
|
|
profanityFilter = await ProfanityFilter.create();
|
|
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.');
|
|
|
|
profanityFilter = createFallbackFilter();
|
|
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'));
|
|
});
|
|
}
|
|
|
|
// Async server startup function
|
|
async function startServer() {
|
|
try {
|
|
// Initialize profanity filter first
|
|
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, () => {
|
|
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 (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', () => {
|
|
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);
|
|
});
|
|
});
|