250 lines
8.6 KiB
JavaScript
250 lines
8.6 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
|
|
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);
|
|
});
|
|
});
|