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');
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) {
// Initialize separate database for profanity filter
const defaultDbPath = path.join(__dirname, 'profanity.db');
this.dbPath = dbPath || defaultDbPath;
this.db = new sqlite3.Database(this.dbPath);
this.isInitialized = false;
// Base profanity words - comprehensive list
this.baseProfanityWords = [
@ -60,17 +72,66 @@ class ProfanityFilter {
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
};
// Initialize database and load custom words
// Initialize custom words array
this.customWords = [];
this.initializeDatabase();
this.loadCustomWords();
// Build patterns
// Build initial patterns with base words only
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() {
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)
*/
getAllWords() {
this._checkInitialization();
const baseWords = this.baseProfanityWords.map(word => ({
word: word.toLowerCase(),
severity: this.getSeverity(word),
@ -234,6 +307,8 @@ class ProfanityFilter {
containsProfanity(text) {
if (!text || typeof text !== 'string') return false;
this._checkInitialization();
const normalizedText = this.normalizeText(text);
const originalText = text.toLowerCase();
@ -258,6 +333,8 @@ class ProfanityFilter {
};
}
this._checkInitialization();
const normalizedText = this.normalizeText(text);
const originalText = text.toLowerCase();
const matches = [];

View file

@ -219,7 +219,13 @@ button[type="submit"]:hover {
color: #721c24;
border: 2px solid #dc3545;
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 {

View file

@ -25,18 +25,12 @@ const db = new sqlite3.Database('icewatch.db');
console.log('Database connection established');
// Initialize profanity filter with its own database
// Initialize profanity filter with its own database (async)
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 that matches the full ProfanityFilter interface
profanityFilter = {
// Create fallback filter function
function createFallbackFilter() {
return {
// Core profanity checking methods
checkText: () => ({ isProfane: false, reason: null }),
containsProfanity: () => false,
@ -84,8 +78,21 @@ try {
// Special property to identify this as a fallback filter
_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
@ -175,28 +182,48 @@ function setupRoutes() {
});
}
// Validate profanity filter is properly initialized before setting up routes
// 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');
// 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);
}
console.log('===========================================');
});
}
// Start the server
startServer();
// Graceful shutdown
process.on('SIGINT', () => {