ice/routes/admin.js
Deco Vander c7f39e4939 feat: isolate profanity filter with separate database
- 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
2025-07-04 00:03:24 -04:00

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;
};