diff --git a/models/Location.js b/models/Location.js deleted file mode 100644 index 8d5c5d7..0000000 --- a/models/Location.js +++ /dev/null @@ -1,131 +0,0 @@ -class Location { - constructor(db) { - this.db = db; - } - - async getActive(hoursThreshold = 48) { - const cutoffTime = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString(); - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT * FROM locations WHERE created_at > ? OR persistent = 1 ORDER BY created_at DESC', - [cutoffTime], - (err, rows) => { - if (err) return reject(err); - resolve(rows); - } - ); - }); - } - - async getAll() { - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT id, address, description, latitude, longitude, persistent, created_at FROM locations ORDER BY created_at DESC', - [], - (err, rows) => { - if (err) return reject(err); - resolve(rows); - } - ); - }); - } - - async create(location) { - const { address, latitude, longitude, description } = location; - return new Promise((resolve, reject) => { - this.db.run( - 'INSERT INTO locations (address, latitude, longitude, description) VALUES (?, ?, ?, ?)', - [address, latitude, longitude, description], - function(err) { - if (err) return reject(err); - resolve({ id: this.lastID, ...location }); - } - ); - }); - } - - async update(id, location) { - const { address, latitude, longitude, description } = location; - return new Promise((resolve, reject) => { - this.db.run( - 'UPDATE locations SET address = ?, latitude = ?, longitude = ?, description = ? WHERE id = ?', - [address, latitude, longitude, description, id], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async togglePersistent(id, persistent) { - return new Promise((resolve, reject) => { - this.db.run( - 'UPDATE locations SET persistent = ? WHERE id = ?', - [persistent ? 1 : 0, id], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async delete(id) { - return new Promise((resolve, reject) => { - this.db.run( - 'DELETE FROM locations WHERE id = ?', - [id], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async cleanupExpired(hoursThreshold = 48) { - const cutoffTime = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString(); - return new Promise((resolve, reject) => { - this.db.run( - 'DELETE FROM locations WHERE created_at < ? AND persistent = 0', - [cutoffTime], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async initializeTable() { - return new Promise((resolve, reject) => { - this.db.serialize(() => { - this.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, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `, (err) => { - if (err) return reject(err); - - this.db.run(` - ALTER TABLE locations ADD COLUMN persistent INTEGER DEFAULT 0 - `, (err) => { - if (err && !err.message.includes('duplicate column name')) { - return reject(err); - } - resolve(); - }); - }); - }); - }); - } -} - -module.exports = Location; \ No newline at end of file diff --git a/models/ProfanityWord.js b/models/ProfanityWord.js deleted file mode 100644 index 76ed356..0000000 --- a/models/ProfanityWord.js +++ /dev/null @@ -1,96 +0,0 @@ -class ProfanityWord { - constructor(db) { - this.db = db; - } - - async getAll() { - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT id, word, severity, category, created_at, created_by FROM profanity_words ORDER BY created_at DESC', - [], - (err, rows) => { - if (err) return reject(err); - resolve(rows); - } - ); - }); - } - - async loadWords() { - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT word, severity, category FROM profanity_words', - [], - (err, rows) => { - if (err) return reject(err); - resolve(rows); - } - ); - }); - } - - async create(word, severity, category, createdBy = 'admin') { - return new Promise((resolve, reject) => { - this.db.run( - 'INSERT INTO profanity_words (word, severity, category, created_by) VALUES (?, ?, ?, ?)', - [word.toLowerCase(), severity, category, createdBy], - function(err) { - if (err) return reject(err); - resolve({ - id: this.lastID, - word: word.toLowerCase(), - severity, - category, - created_by: createdBy - }); - } - ); - }); - } - - async update(id, word, severity, category) { - return new Promise((resolve, reject) => { - this.db.run( - 'UPDATE profanity_words SET word = ?, severity = ?, category = ? WHERE id = ?', - [word.toLowerCase(), severity, category, id], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async delete(id) { - return new Promise((resolve, reject) => { - this.db.run( - 'DELETE FROM profanity_words WHERE id = ?', - [id], - function(err) { - if (err) return reject(err); - resolve({ changes: this.changes }); - } - ); - }); - } - - async initializeTable() { - return new Promise((resolve, reject) => { - this.db.run(` - CREATE TABLE IF NOT EXISTS profanity_words ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - word TEXT NOT NULL UNIQUE, - severity TEXT NOT NULL, - category TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - created_by TEXT DEFAULT 'system' - ) - `, (err) => { - if (err) return reject(err); - resolve(); - }); - }); - } -} - -module.exports = ProfanityWord; \ No newline at end of file diff --git a/profanity-filter.js b/profanity-filter.js deleted file mode 100644 index eaaa604..0000000 --- a/profanity-filter.js +++ /dev/null @@ -1,573 +0,0 @@ -/** - * Comprehensive Server-Side Profanity Filter for Ice Watch - * Filters inappropriate language with database-backed custom word management - */ - -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} 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 = [ - // Common profanity - 'damn', 'hell', 'crap', 'shit', 'fuck', 'ass', 'bitch', 'bastard', - 'piss', 'whore', 'slut', 'retard', 'fag', 'gay', 'homo', 'tranny', - 'dickhead', 'asshole', 'motherfucker', 'cocksucker', 'twat', 'cunt', - - // Racial slurs and hate speech - 'nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike', - 'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey', - 'kyke', 'jigaboo', 'coon', 'darkie', 'mammy', 'pickaninny', - - // Sexual content - 'penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum', - 'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate', 'orgasm', - 'blowjob', 'handjob', 'anal', 'penetration', 'erection', 'climax', - - // Violence and threats - 'kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape', - 'violence', 'assault', 'attack', 'threat', 'harm', 'hurt', 'pain', - 'stab', 'strangle', 'torture', 'execute', 'assassinate', 'slaughter', - - // Drugs and substances - 'weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high', - 'stoned', 'drunk', 'alcohol', 'beer', 'liquor', 'vodka', 'whiskey', - 'ecstasy', 'lsd', 'crack', 'dope', 'pot', 'joint', 'bong', - - // Religious/cultural insults - 'jesus christ', 'goddamn', 'christ almighty', 'holy shit', 'god damn', - 'for christ sake', 'jesus fucking christ', 'holy fuck', - - // Body parts (inappropriate context) - 'testicles', 'balls', 'scrotum', 'clitoris', 'labia', 'anus', - 'rectum', 'butthole', 'nipples', 'breasts', - - // Misc inappropriate - 'wtf', 'omfg', 'stfu', 'gtfo', 'milf', 'dilf', 'thot', 'simp', - 'incel', 'chad', 'beta', 'alpha male', 'mansplain', 'karen' - ]; - - // Leetspeak and common substitutions - this.leetMap = { - '0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't', - '8': 'b', '9': 'g', '@': 'a', '$': 's', '!': 'i', '+': 't', '*': 'a', - '%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/' - }; - - // Initialize custom words array - this.customWords = []; - - // Initialize patterns to null; will be built during async initialization - this.patterns = null; - } - - /** - * Async initialization method that must be called after construction - * @returns {Promise} - */ - 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 (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(); - } - }); - }); - }); - } - -// Removed the deprecated initializeDatabase method as it is unused and no longer necessary. - - /** - * Load custom words from database - */ - async loadCustomWords() { - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT word, severity, category FROM profanity_words', - [], - (err, rows) => { - if (err) { - console.error('Error loading custom profanity words:', err); - reject(err); - return; - } - - this.customWords = rows.map(row => ({ - word: row.word.toLowerCase(), - severity: row.severity, - category: row.category - })); - - console.log(`Loaded ${this.customWords.length} custom profanity words`); - this.patterns = this.buildPatterns(); // Rebuild patterns with custom words - resolve(); - } - ); - }); - } - - /** - * 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), - category: 'base' - })); - - return [...baseWords, ...this.customWords]; - } - - /** - * Build regex patterns for profanity detection - */ - buildPatterns() { - const allWords = this.getAllWords(); - - return allWords.map(wordObj => { - const word = wordObj.word; - // Create simple word boundary pattern first - let pattern = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape special chars - - // For simple detection, use word boundaries - const simpleRegex = new RegExp(`\\b${pattern}\\b`, 'gi'); - - // Also create pattern with character substitutions for advanced detection - const advancedPattern = word.split('').map(char => { - const substitutes = this.getCharSubstitutes(char); - if (substitutes.length > 1) { - // Escape special regex characters in substitutes - const escapedSubs = substitutes.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); - return `[${escapedSubs.join('')}]`; - } - return char; - }).join('[\\s\\-_\\.,;:!?]*?'); - - const advancedRegex = new RegExp(`\\b${advancedPattern}\\b`, 'gi'); - - return { - regex: simpleRegex, - advancedRegex: advancedRegex, - word: word, - severity: wordObj.severity, - category: wordObj.category - }; - }); - } - - /** - * Get character substitutes including leetspeak - */ - getCharSubstitutes(char) { - const substitutes = [char]; - - // Add leetspeak equivalents - Object.keys(this.leetMap).forEach(leet => { - if (this.leetMap[leet] === char) { - substitutes.push(leet); - } - }); - - // Add common character substitutions - const charMap = { - 'a': ['@', '4', '*'], - 'e': ['3'], - 'i': ['1', '!', '|'], - 'o': ['0'], - 's': ['$', '5'], - 't': ['7', '+'], - 'g': ['6', '9'], - 'b': ['8'], - 'l': ['1', '|'] - }; - - if (charMap[char]) { - substitutes.push(...charMap[char]); - } - - return [...new Set(substitutes)]; // Remove duplicates - } - - /** - * Normalize text to handle various obfuscation attempts - */ - normalizeText(text) { - if (!text || typeof text !== 'string') return ''; - - let normalized = text.toLowerCase(); - - // Replace leetspeak characters - escape special regex characters properly - Object.keys(this.leetMap).forEach(leet => { - const escapedLeet = leet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - normalized = normalized.replace(new RegExp(escapedLeet, 'g'), this.leetMap[leet]); - }); - - // Handle spaced out words (f u c k -> fuck) - more comprehensive - normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3$4$5'); - normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3$4'); - normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3'); - - // Remove excessive punctuation but keep word boundaries - normalized = normalized.replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim(); - - return normalized; - } - - /** - * Check if text contains profanity - */ - containsProfanity(text) { - if (!text || typeof text !== 'string') return false; - - this._checkInitialization(); - - const normalizedText = this.normalizeText(text); - const originalText = text.toLowerCase(); - - return this.patterns.some(pattern => { - return pattern.regex.test(normalizedText) || - pattern.regex.test(originalText) || - (pattern.advancedRegex && pattern.advancedRegex.test(normalizedText)); - }); - } - - /** - * Get detailed analysis of profanity in text - */ - analyzeProfanity(text) { - if (!text || typeof text !== 'string') { - return { - hasProfanity: false, - matches: [], - severity: 'none', - count: 0, - filtered: '' - }; - } - - this._checkInitialization(); - - const normalizedText = this.normalizeText(text); - const originalText = text.toLowerCase(); - const matches = []; - let filteredText = text; - - this.patterns.forEach(pattern => { - let found = null; - let matchedText = ''; - - // Try simple regex first on both normalized and original text - found = normalizedText.match(pattern.regex); - if (found) { - matchedText = normalizedText; - } else { - found = originalText.match(pattern.regex); - if (found) { - matchedText = originalText; - } - } - - // If not found, try advanced regex - if (!found && pattern.advancedRegex) { - found = normalizedText.match(pattern.advancedRegex); - if (found) { - matchedText = normalizedText; - } else { - found = originalText.match(pattern.advancedRegex); - if (found) { - matchedText = originalText; - } - } - } - - if (found) { - // Only add if not already detected - if (!matches.some(m => m.word === pattern.word)) { - matches.push({ - word: pattern.word, - matches: found, - severity: pattern.severity, - category: pattern.category - }); - } - - // Replace in original text - try multiple patterns - const wordPattern = pattern.word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const simplePattern = new RegExp(`\\b${wordPattern}\\b`, 'gi'); - filteredText = filteredText.replace(simplePattern, '*'.repeat(Math.max(3, pattern.word.length))); - - // Also try to replace with the exact matches found - found.forEach(match => { - const exactPattern = new RegExp(match.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); - filteredText = filteredText.replace(exactPattern, '*'.repeat(Math.max(3, match.length))); - }); - - // Also try to replace with advanced pattern - if (pattern.advancedRegex) { - filteredText = filteredText.replace(pattern.advancedRegex, '*'.repeat(Math.max(3, pattern.word.length))); - } - } - }); - - const hasProfanity = matches.length > 0; - const maxSeverity = hasProfanity ? - Math.max(...matches.map(m => this.getSeverityLevel(m.severity))) : 0; - - return { - hasProfanity, - matches, - severity: this.getSeverityName(maxSeverity), - count: matches.length, - filtered: filteredText - }; - } - - /** - * Filter profanity from text - */ - filterProfanity(text, replacement = '*') { - const analysis = this.analyzeProfanity(text); - let filteredText = text; - for (const match of analysis.matches) { - const regex = new RegExp(`\\b${match.word}\\b`, 'gi'); - filteredText = filteredText.replace(regex, replacement.repeat(match.word.length)); - } - return filteredText; - } - - /** - * Get severity level for a word - */ - getSeverity(word) { - // High severity: hate speech, extreme profanity, threats - const highSeverity = [ - 'nigger', 'nigga', 'kill', 'murder', 'shoot', 'bomb', 'terrorist', - 'rape', 'kike', 'fag', 'raghead', 'towelhead', 'motherfucker', - 'cunt', 'cocksucker', 'jigaboo', 'coon', 'execute', 'assassinate' - ]; - - // Medium severity: sexual content, moderate profanity - const mediumSeverity = [ - 'fuck', 'shit', 'bitch', 'whore', 'slut', 'penis', 'vagina', - 'cock', 'dick', 'pussy', 'cum', 'sex', 'porn', 'asshole', - 'dickhead', 'twat', 'blowjob', 'handjob' - ]; - - if (highSeverity.includes(word.toLowerCase())) return 'high'; - if (mediumSeverity.includes(word.toLowerCase())) return 'medium'; - return 'low'; - } - - /** - * Get numeric severity level - */ - getSeverityLevel(severity) { - switch (severity) { - case 'high': return 3; - case 'medium': return 2; - case 'low': return 1; - default: return 0; - } - } - - /** - * Get severity name from level - */ - getSeverityName(level) { - switch (level) { - case 3: return 'high'; - case 2: return 'medium'; - case 1: return 'low'; - default: return 'none'; - } - } - - /** - * Add a custom word to the database - */ - async addCustomWord(word, severity = 'medium', category = 'custom', createdBy = 'admin') { - return new Promise((resolve, reject) => { - const normalizedWord = word.toLowerCase().trim(); - - this.db.run( - 'INSERT INTO profanity_words (word, severity, category, created_by) VALUES (?, ?, ?, ?)', - [normalizedWord, severity, category, createdBy], - function(err) { - if (err) { - if (err.message.includes('UNIQUE constraint failed')) { - reject(new Error('Word already exists in the filter')); - } else { - reject(err); - } - return; - } - - console.log(`Added custom profanity word: ${normalizedWord}`); - resolve({ - id: this.lastID, - word: normalizedWord, - severity, - category, - created_by: createdBy - }); - } - ); - }); - } - - /** - * Remove a custom word from the database - */ - async removeCustomWord(wordId) { - return new Promise((resolve, reject) => { - this.db.run( - 'DELETE FROM profanity_words WHERE id = ?', - [wordId], - function(err) { - if (err) { - reject(err); - return; - } - - if (this.changes === 0) { - reject(new Error('Word not found')); - return; - } - - console.log(`Removed custom profanity word with ID: ${wordId}`); - resolve({ deleted: true, changes: this.changes }); - } - ); - }); - } - - /** - * Get all custom words from database - */ - async getCustomWords() { - return new Promise((resolve, reject) => { - this.db.all( - 'SELECT id, word, severity, category, created_at, created_by FROM profanity_words ORDER BY created_at DESC', - [], - (err, rows) => { - if (err) { - reject(err); - return; - } - resolve(rows); - } - ); - }); - } - - /** - * Update a custom word - */ - async updateCustomWord(wordId, updates) { - return new Promise((resolve, reject) => { - const { word, severity, category } = updates; - - this.db.run( - 'UPDATE profanity_words SET word = ?, severity = ?, category = ? WHERE id = ?', - [word.toLowerCase().trim(), severity, category, wordId], - function(err) { - if (err) { - reject(err); - return; - } - - if (this.changes === 0) { - reject(new Error('Word not found')); - return; - } - - console.log(`Updated custom profanity word with ID: ${wordId}`); - resolve({ updated: true, changes: this.changes }); - } - ); - }); - } - - /** - * Close the database connection - */ - close() { - if (this.db) { - this.db.close((err) => { - if (err) { - console.error('Error closing profanity filter database:', err); - } - }); - } - } - -} - -module.exports = ProfanityFilter; diff --git a/routes/admin.js b/routes/admin.js deleted file mode 100644 index 5c2ca53..0000000 --- a/routes/admin.js +++ /dev/null @@ -1,231 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -module.exports = (locationModel, profanityWordModel, profanityFilter, authenticateAdmin) => { - // Admin login - router.post('/login', (req, res) => { - console.log('Admin login attempt'); - const { password } = req.body; - const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123'; - - if (password === ADMIN_PASSWORD) { - console.log('Admin login successful'); - res.json({ token: ADMIN_PASSWORD, message: 'Login successful' }); - } else { - console.warn('Admin login failed: invalid password'); - res.status(401).json({ error: 'Invalid password' }); - } - }); - - // Get all locations for admin (including expired ones) - router.get('/locations', authenticateAdmin, async (req, res) => { - try { - const rows = await locationModel.getAll(); - - // Process and clean data before sending - const locations = rows.map(row => ({ - id: row.id, - address: row.address, - description: row.description || '', - latitude: row.latitude, - longitude: row.longitude, - persistent: !!row.persistent, - created_at: row.created_at, - isActive: new Date(row.created_at) > new Date(Date.now() - 48 * 60 * 60 * 1000) - })); - - res.json(locations); - } catch (err) { - console.error('Error fetching all locations:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Update a location (admin only) - router.put('/locations/:id', authenticateAdmin, async (req, res) => { - const { id } = req.params; - const { address, latitude, longitude, description } = req.body; - - if (!address) { - res.status(400).json({ error: 'Address is required' }); - return; - } - - try { - const result = await locationModel.update(id, { - address, - latitude, - longitude, - description - }); - - if (result.changes === 0) { - res.status(404).json({ error: 'Location not found' }); - return; - } - - res.json({ message: 'Location updated successfully' }); - } catch (err) { - console.error('Error updating location:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Toggle persistent status of a location (admin only) - router.patch('/locations/:id/persistent', authenticateAdmin, async (req, res) => { - const { id } = req.params; - const { persistent } = req.body; - - if (typeof persistent !== 'boolean') { - res.status(400).json({ error: 'Persistent value must be a boolean' }); - return; - } - - try { - const result = await locationModel.togglePersistent(id, persistent); - - if (result.changes === 0) { - res.status(404).json({ error: 'Location not found' }); - return; - } - - console.log(`Location ${id} persistent status set to ${persistent}`); - res.json({ message: 'Persistent status updated successfully', persistent }); - } catch (err) { - console.error('Error updating persistent status:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Delete a location (admin authentication required) - router.delete('/locations/:id', authenticateAdmin, async (req, res) => { - const { id } = req.params; - - try { - const result = await locationModel.delete(id); - - if (result.changes === 0) { - res.status(404).json({ error: 'Location not found' }); - return; - } - - res.json({ message: 'Location deleted successfully' }); - } catch (err) { - console.error('Error deleting location:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Profanity Management Routes - - // Get all custom profanity words (admin only) - router.get('/profanity-words', authenticateAdmin, async (req, res) => { - try { - const words = await profanityFilter.getCustomWords(); - res.json(words); - } catch (error) { - console.error('Error fetching custom profanity words:', error); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Add a custom profanity word (admin only) - router.post('/profanity-words', authenticateAdmin, async (req, res) => { - try { - const { word, severity = 'medium', category = 'custom' } = req.body; - - if (!word || typeof word !== 'string' || word.trim().length === 0) { - return res.status(400).json({ error: 'Word is required and must be a non-empty string' }); - } - - if (!['low', 'medium', 'high'].includes(severity)) { - return res.status(400).json({ error: 'Severity must be low, medium, or high' }); - } - - const result = await profanityFilter.addCustomWord(word, severity, category, 'admin'); - await profanityFilter.loadCustomWords(); // Reload to update patterns - - console.log(`Admin added custom profanity word: ${word}`); - res.json(result); - } catch (error) { - console.error('Error adding custom profanity word:', error); - if (error.message.includes('already exists')) { - res.status(409).json({ error: error.message }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } - }); - - // Update a custom profanity word (admin only) - router.put('/profanity-words/:id', authenticateAdmin, async (req, res) => { - try { - const { id } = req.params; - const { word, severity, category } = req.body; - - if (!word || typeof word !== 'string' || word.trim().length === 0) { - return res.status(400).json({ error: 'Word is required and must be a non-empty string' }); - } - - if (!['low', 'medium', 'high'].includes(severity)) { - return res.status(400).json({ error: 'Severity must be low, medium, or high' }); - } - - const result = await profanityFilter.updateCustomWord(id, { word, severity, category }); - await profanityFilter.loadCustomWords(); // Reload to update patterns - - console.log(`Admin updated custom profanity word ID ${id}`); - res.json(result); - } catch (error) { - console.error('Error updating custom profanity word:', error); - if (error.message.includes('not found')) { - res.status(404).json({ error: error.message }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } - }); - - // Delete a custom profanity word (admin only) - router.delete('/profanity-words/:id', authenticateAdmin, async (req, res) => { - try { - const { id } = req.params; - - const result = await profanityFilter.removeCustomWord(id); - await profanityFilter.loadCustomWords(); // Reload to update patterns - - console.log(`Admin deleted custom profanity word ID ${id}`); - res.json(result); - } catch (error) { - console.error('Error deleting custom profanity word:', error); - if (error.message.includes('not found')) { - res.status(404).json({ error: error.message }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } - }); - - // Test profanity filter (admin only) - for testing purposes - router.post('/test-profanity', authenticateAdmin, (req, res) => { - try { - const { text } = req.body; - - if (!text || typeof text !== 'string') { - return res.status(400).json({ error: 'Text is required for testing' }); - } - - const analysis = profanityFilter.analyzeProfanity(text); - res.json({ - original: text, - analysis: analysis, - filtered: analysis.filtered - }); - } catch (error) { - console.error('Error testing profanity filter:', error); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - return router; -}; diff --git a/routes/config.js b/routes/config.js deleted file mode 100644 index fe844cb..0000000 --- a/routes/config.js +++ /dev/null @@ -1,22 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -module.exports = () => { - // Get API configuration - router.get('/', (req, res) => { - console.log('📡 API Config requested'); - const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN || null; - - console.log('MapBox token present:', !!MAPBOX_ACCESS_TOKEN); - console.log('MapBox token starts with pk:', MAPBOX_ACCESS_TOKEN?.startsWith('pk.')); - - res.json({ - // MapBox tokens are designed to be public (they have domain restrictions) - mapboxAccessToken: MAPBOX_ACCESS_TOKEN, - hasMapbox: !!MAPBOX_ACCESS_TOKEN - // SECURITY: Google Maps API key is kept server-side only - }); - }); - - return router; -}; diff --git a/routes/locations.js b/routes/locations.js deleted file mode 100644 index ca3f2eb..0000000 --- a/routes/locations.js +++ /dev/null @@ -1,97 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -module.exports = (locationModel, profanityFilter) => { - // Get all active locations (within 48 hours OR persistent) - router.get('/', async (req, res) => { - console.log('Fetching active locations'); - - try { - const locations = await locationModel.getActive(); - console.log(`Fetched ${locations.length} active locations (including persistent)`); - res.json(locations); - } catch (err) { - console.error('Error fetching locations:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Add a new location - router.post('/', async (req, res) => { - const { address, latitude, longitude } = req.body; - let { description } = req.body; - console.log(`Attempt to add new location: ${address}`); - - if (!address) { - console.warn('Failed to add location: Address is required'); - res.status(400).json({ error: 'Address is required' }); - return; - } - - // Check for profanity in description and reject if any is found - if (description && profanityFilter) { - try { - const analysis = profanityFilter.analyzeProfanity(description); - if (analysis.hasProfanity) { - console.warn(`Submission rejected due to inappropriate language (${analysis.count} word${analysis.count > 1 ? 's' : ''}, severity: ${analysis.severity}) - Original: "${req.body.description}"`); - - // Reject any submission with profanity - const wordText = analysis.count === 1 ? 'word' : 'words'; - const detectedWords = analysis.matches.map(m => m.word).join(', '); - - return res.status(400).json({ - error: 'Submission rejected', - message: `Your description contains inappropriate language and cannot be posted. Please revise your description to focus on road conditions and keep it professional.\n\nExample: "Multiple vehicles stuck, black ice present" or "Road very slippery, saw 3 accidents"`, - details: { - severity: analysis.severity, - wordCount: analysis.count, - detectedCategories: [...new Set(analysis.matches.map(m => m.category))] - } - }); - } - } catch (filterError) { - console.error('Error checking profanity:', filterError); - // Continue with original description if filter fails - } - } - - try { - const newLocation = await locationModel.create({ - address, - latitude, - longitude, - description - }); - - console.log(`Location added successfully: ${address}`); - res.json({ - ...newLocation, - created_at: new Date().toISOString() - }); - } catch (err) { - console.error('Error inserting location:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - // Legacy delete route (keeping for backwards compatibility) - router.delete('/:id', async (req, res) => { - const { id } = req.params; - - try { - const result = await locationModel.delete(id); - - if (result.changes === 0) { - res.status(404).json({ error: 'Location not found' }); - return; - } - - res.json({ message: 'Location deleted successfully' }); - } catch (err) { - console.error('Error deleting location:', err); - res.status(500).json({ error: 'Internal server error' }); - } - }); - - return router; -}; \ No newline at end of file diff --git a/server.js b/server.js deleted file mode 100644 index c98db53..0000000 --- a/server.js +++ /dev/null @@ -1,223 +0,0 @@ -require('dotenv').config({ path: '.env.local' }); -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const path = require('path'); -const cron = require('node-cron'); -const DatabaseService = require('./services/DatabaseService'); -const ProfanityFilterService = require('./services/ProfanityFilterService'); - -// 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 and services setup -const databaseService = new DatabaseService(); -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 { - const profanityWordModel = databaseService.getProfanityWordModel(); - profanityFilter = new ProfanityFilterService(profanityWordModel); - await profanityFilter.initialize(); - console.log('Profanity filter initialized successfully'); - } 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!'); - } -} - -// Database initialization is now handled by DatabaseService - -// Clean up expired locations (older than 48 hours, but not persistent ones) -const cleanupExpiredLocations = async () => { - console.log('Running cleanup of expired locations'); - try { - const locationModel = databaseService.getLocationModel(); - const result = await locationModel.cleanupExpired(); - console.log(`Cleaned up ${result.changes} expired locations (persistent reports preserved)`); - } catch (err) { - console.error('Error cleaning up expired locations:', err); - } -}; - -// 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() { - const locationModel = databaseService.getLocationModel(); - const profanityWordModel = databaseService.getProfanityWordModel(); - - // API Routes - app.use('/api/config', configRoutes()); - app.use('/api/locations', locationRoutes(locationModel, profanityFilter)); - app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, 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 database service first - await databaseService.initialize(); - console.log('Database service initialized successfully'); - - // Initialize profanity filter - 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 database service - databaseService.close(); - console.log('Database connections closed.'); - process.exit(0); -}); \ No newline at end of file diff --git a/services/DatabaseService.js b/services/DatabaseService.js deleted file mode 100644 index 3b9c100..0000000 --- a/services/DatabaseService.js +++ /dev/null @@ -1,93 +0,0 @@ -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); -const Location = require('../models/Location'); -const ProfanityWord = require('../models/ProfanityWord'); - -class DatabaseService { - constructor() { - this.mainDb = null; - this.profanityDb = null; - this.locationModel = null; - this.profanityWordModel = null; - } - - async initialize() { - await this.initializeMainDatabase(); - await this.initializeProfanityDatabase(); - } - - async initializeMainDatabase() { - return new Promise((resolve, reject) => { - const dbPath = path.join(__dirname, '..', 'icewatch.db'); - this.mainDb = new sqlite3.Database(dbPath, async (err) => { - if (err) { - console.error('Could not connect to main database', err); - return reject(err); - } - console.log('Connected to main SQLite database.'); - - this.locationModel = new Location(this.mainDb); - try { - await this.locationModel.initializeTable(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - } - - async initializeProfanityDatabase() { - return new Promise((resolve, reject) => { - const dbPath = path.join(__dirname, '..', 'profanity.db'); - this.profanityDb = new sqlite3.Database(dbPath, async (err) => { - if (err) { - console.error('Could not connect to profanity database', err); - return reject(err); - } - console.log('Connected to profanity SQLite database.'); - - this.profanityWordModel = new ProfanityWord(this.profanityDb); - try { - await this.profanityWordModel.initializeTable(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - } - - getLocationModel() { - if (!this.locationModel) { - throw new Error('Database not initialized. Call initialize() first.'); - } - return this.locationModel; - } - - getProfanityWordModel() { - if (!this.profanityWordModel) { - throw new Error('Database not initialized. Call initialize() first.'); - } - return this.profanityWordModel; - } - - getMainDb() { - return this.mainDb; - } - - getProfanityDb() { - return this.profanityDb; - } - - close() { - if (this.mainDb) { - this.mainDb.close(); - } - if (this.profanityDb) { - this.profanityDb.close(); - } - } -} - -module.exports = DatabaseService; \ No newline at end of file diff --git a/services/ProfanityFilterService.js b/services/ProfanityFilterService.js deleted file mode 100644 index a7ca723..0000000 --- a/services/ProfanityFilterService.js +++ /dev/null @@ -1,335 +0,0 @@ -/** - * Refactored Profanity Filter Service that uses the ProfanityWord model - */ - -class ProfanityFilterService { - constructor(profanityWordModel) { - this.profanityWordModel = profanityWordModel; - this.isInitialized = false; - - // Base profanity words - comprehensive list - this.baseProfanityWords = [ - // Common profanity - 'damn', 'hell', 'crap', 'shit', 'fuck', 'ass', 'bitch', 'bastard', - 'piss', 'whore', 'slut', 'retard', 'fag', 'gay', 'homo', 'tranny', - 'dickhead', 'asshole', 'motherfucker', 'cocksucker', 'twat', 'cunt', - - // Racial slurs and hate speech - 'nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike', - 'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey', - 'kyke', 'jigaboo', 'coon', 'darkie', 'mammy', 'pickaninny', - - // Sexual content - 'penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum', - 'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate', 'orgasm', - 'blowjob', 'handjob', 'anal', 'penetration', 'erection', 'climax', - - // Violence and threats - 'kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape', - 'violence', 'assault', 'attack', 'threat', 'harm', 'hurt', 'pain', - 'stab', 'strangle', 'torture', 'execute', 'assassinate', 'slaughter', - - // Drugs and substances - 'weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high', - 'stoned', 'drunk', 'alcohol', 'beer', 'liquor', 'vodka', 'whiskey', - 'ecstasy', 'lsd', 'crack', 'dope', 'pot', 'joint', 'bong', - - // Religious/cultural insults - 'jesus christ', 'goddamn', 'christ almighty', 'holy shit', 'god damn', - 'for christ sake', 'jesus fucking christ', 'holy fuck', - - // Body parts (inappropriate context) - 'testicles', 'balls', 'scrotum', 'clitoris', 'labia', 'anus', - 'rectum', 'butthole', 'nipples', 'breasts', - - // Misc inappropriate - 'wtf', 'omfg', 'stfu', 'gtfo', 'milf', 'dilf', 'thot', 'simp', - 'incel', 'chad', 'beta', 'alpha male', 'mansplain', 'karen' - ]; - - // Leetspeak and common substitutions - this.leetMap = { - '0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't', - '8': 'b', '9': 'g', '@': 'a', '$': 's', '!': 'i', '+': 't', '*': 'a', - '%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/' - }; - - // Initialize custom words array - this.customWords = []; - - // Initialize patterns to null; will be built during async initialization - this.patterns = null; - } - - /** - * Initialize the filter by loading custom words - */ - async initialize() { - if (this.isInitialized) { - return; - } - - try { - await this.loadCustomWords(); - this.isInitialized = true; - console.log('ProfanityFilterService initialization completed successfully'); - } catch (error) { - console.error('Error during ProfanityFilterService initialization:', error); - throw error; - } - } - - /** - * Load custom words from database using the model - */ - async loadCustomWords() { - try { - const rows = await this.profanityWordModel.loadWords(); - - this.customWords = rows.map(row => ({ - word: row.word.toLowerCase(), - severity: row.severity, - category: row.category - })); - - console.log(`Loaded ${this.customWords.length} custom profanity words`); - this.patterns = this.buildPatterns(); // Rebuild patterns with custom words - } catch (err) { - console.error('Error loading custom profanity words:', err); - throw err; - } - } - - /** - * Build regex patterns for all profanity words - */ - buildPatterns() { - const allWords = [...this.baseProfanityWords, ...this.customWords.map(w => w.word)]; - - // Sort by length (longest first) to catch longer variations before shorter ones - allWords.sort((a, b) => b.length - a.length); - - // Create patterns with word boundaries and common variations - return allWords.map(word => { - const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const pattern = escaped - .split('') - .map(char => { - const leetChars = Object.entries(this.leetMap) - .filter(([_, v]) => v === char.toLowerCase()) - .map(([k, _]) => k); - - if (leetChars.length > 0) { - const allChars = [char, ...leetChars].map(c => - c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - ); - return `[${allChars.join('')}]`; - } - return char; - }) - .join('[\\s\\-\\_\\*\\.]*'); - - return { - word: word, - pattern: new RegExp(`\\b${pattern}\\b`, 'gi'), - severity: this.getSeverity(word), - category: this.getCategory(word) - }; - }); - } - - /** - * Get severity level for a word - */ - getSeverity(word) { - // Check custom words first - const customWord = this.customWords.find(w => w.word === word.toLowerCase()); - if (customWord) { - return customWord.severity; - } - - // Categorize severity based on type - const highSeverity = ['nigger', 'nigga', 'cunt', 'fag', 'retard', 'kike', 'spic', 'gook', 'chink']; - const lowSeverity = ['damn', 'hell', 'crap', 'wtf', 'omfg']; - - if (highSeverity.includes(word.toLowerCase())) return 'high'; - if (lowSeverity.includes(word.toLowerCase())) return 'low'; - return 'medium'; - } - - /** - * Get category for a word - */ - getCategory(word) { - // Check custom words first - const customWord = this.customWords.find(w => w.word === word.toLowerCase()); - if (customWord) { - return customWord.category; - } - - // Categorize based on type - const categories = { - racial: ['nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike', 'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey'], - sexual: ['penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum', 'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate'], - violence: ['kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape', 'violence', 'assault', 'attack'], - substance: ['weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high', 'stoned', 'drunk', 'alcohol'], - general: ['shit', 'fuck', 'ass', 'bitch', 'bastard', 'damn', 'hell', 'crap'] - }; - - for (const [category, words] of Object.entries(categories)) { - if (words.includes(word.toLowerCase())) { - return category; - } - } - - return 'general'; - } - - /** - * Normalize text for checking - */ - normalizeText(text) { - if (!text) return ''; - - // Convert to lowercase and handle basic substitutions - let normalized = text.toLowerCase(); - - // Replace multiple spaces/special chars with single space - normalized = normalized.replace(/[\s\-\_\*\.]+/g, ' '); - - // Apply leet speak conversions - normalized = normalized.split('').map(char => - this.leetMap[char] || char - ).join(''); - - return normalized; - } - - /** - * Check if text contains profanity - */ - containsProfanity(text) { - if (!text || !this.patterns) return false; - - const normalized = this.normalizeText(text); - return this.patterns.some(({ pattern }) => pattern.test(normalized)); - } - - /** - * Analyze text for profanity with detailed results - */ - analyzeProfanity(text) { - if (!text || !this.patterns) { - return { - hasProfanity: false, - matches: [], - severity: 'none', - count: 0, - filtered: text || '' - }; - } - - const normalized = this.normalizeText(text); - const matches = []; - let filteredText = text; - - this.patterns.forEach(({ word, pattern, severity, category }) => { - const regex = new RegExp(pattern.source, 'gi'); - let match; - - while ((match = regex.exec(normalized)) !== null) { - matches.push({ - word: word, - found: match[0], - index: match.index, - severity: severity, - category: category - }); - - // Replace in filtered text - const replacement = '*'.repeat(match[0].length); - filteredText = filteredText.substring(0, match.index) + - replacement + - filteredText.substring(match.index + match[0].length); - } - }); - - // Determine overall severity - let overallSeverity = 'none'; - if (matches.length > 0) { - if (matches.some(m => m.severity === 'high')) { - overallSeverity = 'high'; - } else if (matches.some(m => m.severity === 'medium')) { - overallSeverity = 'medium'; - } else { - overallSeverity = 'low'; - } - } - - return { - hasProfanity: matches.length > 0, - matches: matches, - severity: overallSeverity, - count: matches.length, - filtered: filteredText - }; - } - - /** - * Filter profanity from text - */ - filterProfanity(text, replacementChar = '*') { - const analysis = this.analyzeProfanity(text); - return analysis.filtered; - } - - /** - * Add a custom word using the model - */ - async addCustomWord(word, severity = 'medium', category = 'custom', createdBy = 'admin') { - try { - const result = await this.profanityWordModel.create(word, severity, category, createdBy); - await this.loadCustomWords(); // Reload to update patterns - return result; - } catch (err) { - if (err.message.includes('UNIQUE constraint failed')) { - throw new Error('Word already exists in the filter'); - } - throw err; - } - } - - /** - * Remove a custom word using the model - */ - async removeCustomWord(wordId) { - const result = await this.profanityWordModel.delete(wordId); - if (result.changes === 0) { - throw new Error('Word not found'); - } - await this.loadCustomWords(); // Reload to update patterns - return { deleted: true, changes: result.changes }; - } - - /** - * Get all custom words using the model - */ - async getCustomWords() { - return await this.profanityWordModel.getAll(); - } - - /** - * Update a custom word using the model - */ - async updateCustomWord(wordId, updates) { - const { word, severity, category } = updates; - const result = await this.profanityWordModel.update(wordId, word, severity, category); - if (result.changes === 0) { - throw new Error('Word not found'); - } - await this.loadCustomWords(); // Reload to update patterns - return { updated: true, changes: result.changes }; - } -} - -module.exports = ProfanityFilterService; \ No newline at end of file