- Create dedicated ProfanityFilter class with isolated SQLite database - Separate profanity.db from main application database to prevent SQLITE_MISUSE errors - Add comprehensive custom word management (CRUD operations) - Implement advanced profanity detection with leetspeak and pattern matching - Add admin UI for managing custom profanity words - Add extensive test suites for both profanity filter and API routes - Update server.js to use isolated profanity filter - Add proper database initialization and cleanup methods - Support in-memory databases for testing Breaking changes: - Profanity filter now uses separate database file - Updated admin API endpoints for profanity management - Enhanced profanity detection capabilities
242 lines
9.3 KiB
JavaScript
242 lines
9.3 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
|
|
module.exports = (db, 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, (req, res) => {
|
|
db.all(
|
|
'SELECT id, address, description, latitude, longitude, persistent, created_at FROM locations ORDER BY created_at DESC',
|
|
[],
|
|
(err, rows) => {
|
|
if (err) {
|
|
console.error('Error fetching all locations:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
);
|
|
});
|
|
|
|
// Update a location (admin only)
|
|
router.put('/locations/:id', authenticateAdmin, (req, res) => {
|
|
const { id } = req.params;
|
|
const { address, latitude, longitude, description } = req.body;
|
|
|
|
if (!address) {
|
|
res.status(400).json({ error: 'Address is required' });
|
|
return;
|
|
}
|
|
|
|
db.run(
|
|
'UPDATE locations SET address = ?, latitude = ?, longitude = ?, description = ? WHERE id = ?',
|
|
[address, latitude, longitude, description, id],
|
|
function(err) {
|
|
if (err) {
|
|
console.error('Error updating location:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
return;
|
|
}
|
|
|
|
if (this.changes === 0) {
|
|
res.status(404).json({ error: 'Location not found' });
|
|
return;
|
|
}
|
|
|
|
res.json({ message: 'Location updated successfully' });
|
|
}
|
|
);
|
|
});
|
|
|
|
// Toggle persistent status of a location (admin only)
|
|
router.patch('/locations/:id/persistent', authenticateAdmin, (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;
|
|
}
|
|
|
|
db.run(
|
|
'UPDATE locations SET persistent = ? WHERE id = ?',
|
|
[persistent ? 1 : 0, id],
|
|
function(err) {
|
|
if (err) {
|
|
console.error('Error updating persistent status:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
return;
|
|
}
|
|
|
|
if (this.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 });
|
|
}
|
|
);
|
|
});
|
|
|
|
// Delete a location (admin authentication required)
|
|
router.delete('/locations/:id', authenticateAdmin, (req, res) => {
|
|
const { id } = req.params;
|
|
|
|
db.run('DELETE FROM locations WHERE id = ?', [id], function(err) {
|
|
if (err) {
|
|
console.error('Error deleting location:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
return;
|
|
}
|
|
|
|
if (this.changes === 0) {
|
|
res.status(404).json({ error: 'Location not found' });
|
|
return;
|
|
}
|
|
|
|
res.json({ message: 'Location deleted successfully' });
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
};
|