mplement proper async initialization for ProfanityFilter

- 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.
This commit is contained in:
Deco Vander 2025-07-04 13:58:28 -04:00
parent 542415cccd
commit 4dd48627d0
3 changed files with 150 additions and 40 deletions

View file

@ -7,11 +7,23 @@ const sqlite3 = require('sqlite3').verbose();
const path = require('path'); const path = require('path');
class ProfanityFilter { class ProfanityFilter {
/**
* Static factory method for creating and initializing a ProfanityFilter
* @param {string|null} dbPath - Optional path to database file
* @returns {Promise<ProfanityFilter>} Fully initialized ProfanityFilter instance
*/
static async create(dbPath = null) {
const filter = new ProfanityFilter(dbPath);
await filter.initialize();
return filter;
}
constructor(dbPath = null) { constructor(dbPath = null) {
// Initialize separate database for profanity filter // Initialize separate database for profanity filter
const defaultDbPath = path.join(__dirname, 'profanity.db'); const defaultDbPath = path.join(__dirname, 'profanity.db');
this.dbPath = dbPath || defaultDbPath; this.dbPath = dbPath || defaultDbPath;
this.db = new sqlite3.Database(this.dbPath); this.db = new sqlite3.Database(this.dbPath);
this.isInitialized = false;
// Base profanity words - comprehensive list // Base profanity words - comprehensive list
this.baseProfanityWords = [ this.baseProfanityWords = [
@ -60,17 +72,66 @@ class ProfanityFilter {
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/' '%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
}; };
// Initialize database and load custom words // Initialize custom words array
this.customWords = []; this.customWords = [];
this.initializeDatabase();
this.loadCustomWords();
// Build patterns // Build initial patterns with base words only
this.patterns = this.buildPatterns(); this.patterns = this.buildPatterns();
} }
/**
* Async initialization method that must be called after construction
* @returns {Promise<void>}
*/
async initialize() {
if (this.isInitialized) {
return;
}
try {
// Initialize database synchronously first
await this.initializeDatabaseAsync();
// Load custom words from database
await this.loadCustomWords();
this.isInitialized = true;
console.log('ProfanityFilter initialization completed successfully');
} catch (error) {
console.error('Error during ProfanityFilter initialization:', error);
throw error;
}
}
/** /**
* Initialize the database table for custom profanity words * Initialize the database table for custom profanity words (async version)
*/
async initializeDatabaseAsync() {
return new Promise((resolve, reject) => {
this.db.serialize(() => {
this.db.run(`CREATE TABLE IF NOT EXISTS profanity_words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL UNIQUE,
severity TEXT DEFAULT 'medium',
category TEXT DEFAULT 'custom',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'admin'
)`, (err) => {
if (err) {
console.error('Error creating profanity_words table:', err);
reject(err);
} else {
console.log('Profanity words table initialized successfully');
resolve();
}
});
});
});
}
/**
* Initialize the database table for custom profanity words (legacy sync version)
* @deprecated Use initializeDatabaseAsync() instead
*/ */
initializeDatabase() { initializeDatabase() {
this.db.serialize(() => { this.db.serialize(() => {
@ -120,10 +181,22 @@ class ProfanityFilter {
}); });
} }
/**
* Check if the filter is fully initialized and warn if not
* @private
*/
_checkInitialization() {
if (!this.isInitialized) {
console.warn('⚠️ ProfanityFilter: Using base words only - custom words not loaded yet. Call initialize() or use ProfanityFilter.create() for full functionality.');
}
}
/** /**
* Get all profanity words (base + custom) * Get all profanity words (base + custom)
*/ */
getAllWords() { getAllWords() {
this._checkInitialization();
const baseWords = this.baseProfanityWords.map(word => ({ const baseWords = this.baseProfanityWords.map(word => ({
word: word.toLowerCase(), word: word.toLowerCase(),
severity: this.getSeverity(word), severity: this.getSeverity(word),
@ -234,6 +307,8 @@ class ProfanityFilter {
containsProfanity(text) { containsProfanity(text) {
if (!text || typeof text !== 'string') return false; if (!text || typeof text !== 'string') return false;
this._checkInitialization();
const normalizedText = this.normalizeText(text); const normalizedText = this.normalizeText(text);
const originalText = text.toLowerCase(); const originalText = text.toLowerCase();
@ -258,6 +333,8 @@ class ProfanityFilter {
}; };
} }
this._checkInitialization();
const normalizedText = this.normalizeText(text); const normalizedText = this.normalizeText(text);
const originalText = text.toLowerCase(); const originalText = text.toLowerCase();
const matches = []; const matches = [];

View file

@ -219,7 +219,13 @@ button[type="submit"]:hover {
color: #721c24; color: #721c24;
border: 2px solid #dc3545; border: 2px solid #dc3545;
font-weight: bold; font-weight: bold;
animation: pulse 0.5s ease-in-out; }
/* Only animate for users who haven't requested reduced motion */
@media (prefers-reduced-motion: no-preference) {
.message.error.profanity-rejection {
animation: pulse 0.5s ease-in-out;
}
} }
[data-theme="dark"] .message.error.profanity-rejection { [data-theme="dark"] .message.error.profanity-rejection {

View file

@ -25,18 +25,12 @@ const db = new sqlite3.Database('icewatch.db');
console.log('Database connection established'); console.log('Database connection established');
// Initialize profanity filter with its own database // Initialize profanity filter with its own database (async)
let profanityFilter; let profanityFilter;
try {
profanityFilter = new ProfanityFilter(); // Create fallback filter function
console.log('Profanity filter initialized successfully with separate database'); function createFallbackFilter() {
} catch (error) { return {
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 that matches the full ProfanityFilter interface
profanityFilter = {
// Core profanity checking methods // Core profanity checking methods
checkText: () => ({ isProfane: false, reason: null }), checkText: () => ({ isProfane: false, reason: null }),
containsProfanity: () => false, containsProfanity: () => false,
@ -84,8 +78,21 @@ try {
// Special property to identify this as a fallback filter // Special property to identify this as a fallback filter
_isFallback: true _isFallback: true
}; };
}
console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!');
// 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 // Initialize database
@ -175,28 +182,48 @@ function setupRoutes() {
}); });
} }
// Validate profanity filter is properly initialized before setting up routes // Async server startup function
async function startServer() {
// Initialize routes after everything is set up try {
setupRoutes(); // Initialize profanity filter first
await initializeProfanityFilter();
// Start server
app.listen(PORT, () => { // Validate profanity filter is properly initialized
console.log('==========================================='); if (!profanityFilter) {
console.log('Great Lakes Ice Report server started'); console.error('CRITICAL ERROR: profanityFilter is undefined after initialization attempt.');
console.log(`Listening on port ${PORT}`); console.error('Cannot start server without a functional profanity filter.');
console.log(`Visit http://localhost:${PORT} to view the website`); process.exit(1);
}
// Display profanity filter status
if (profanityFilter._isFallback) { // Initialize routes after everything is set up
console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)'); setupRoutes();
console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!');
} else { // Start server
console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL'); 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);
} }
}
console.log('===========================================');
}); // Start the server
startServer();
// Graceful shutdown // Graceful shutdown
process.on('SIGINT', () => { process.on('SIGINT', () => {