1b4b497fdf
Mirror to GitHub / mirror (push) Successful in 44s
Test and Lint / backend-test (push) Successful in 1m42s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
- Remove all console.log/debug statements from production code - Add NODE_ENV checks for development-only logging - Remove test scripts (test-feedback, test-image-security, test-backup-*, test-restore) - Remove one-time fix scripts (fix-temp-photos, fix-migration-state, mark-migration-applied) - Remove sensitive files (.env.backup, ADMIN_CREDENTIALS.txt) - Update package.json to remove references to deleted scripts - Replace console statements with logger utility in backend - Secure error boundaries to not expose stack traces in production This makes the codebase production-ready with no debug output or test scripts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
515 lines
14 KiB
JavaScript
515 lines
14 KiB
JavaScript
const express = require('express');
|
|
const { db } = require('../database/db');
|
|
const { adminAuth } = require('../middleware/auth');
|
|
const secureImageMiddleware = require('../middleware/secureImageMiddleware');
|
|
const logger = require('../utils/logger');
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* Get image security settings
|
|
*/
|
|
router.get('/settings', adminAuth, async (req, res) => {
|
|
try {
|
|
const settings = await db('app_settings')
|
|
.whereIn('setting_key', [
|
|
'default_protection_level',
|
|
'default_image_quality',
|
|
'enable_devtools_protection',
|
|
'max_image_requests_per_minute',
|
|
'max_image_requests_per_5_minutes',
|
|
'max_image_requests_per_hour',
|
|
'suspicious_activity_threshold',
|
|
'enable_canvas_rendering',
|
|
'default_fragmentation_level',
|
|
'security_monitoring_enabled',
|
|
'block_suspicious_ips',
|
|
'log_security_events_to_db',
|
|
'auto_block_threshold'
|
|
])
|
|
.select('setting_key', 'setting_value');
|
|
|
|
const config = {};
|
|
settings.forEach(setting => {
|
|
config[setting.setting_key] = JSON.parse(setting.setting_value);
|
|
});
|
|
|
|
res.json(config);
|
|
} catch (error) {
|
|
logger.error('Error getting image security settings', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to get security settings' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Update image security settings
|
|
*/
|
|
router.put('/settings', adminAuth, async (req, res) => {
|
|
try {
|
|
const updates = req.body;
|
|
|
|
// Validate settings
|
|
const validSettings = [
|
|
'default_protection_level',
|
|
'default_image_quality',
|
|
'enable_devtools_protection',
|
|
'max_image_requests_per_minute',
|
|
'max_image_requests_per_5_minutes',
|
|
'max_image_requests_per_hour',
|
|
'suspicious_activity_threshold',
|
|
'enable_canvas_rendering',
|
|
'default_fragmentation_level',
|
|
'security_monitoring_enabled',
|
|
'block_suspicious_ips',
|
|
'log_security_events_to_db',
|
|
'auto_block_threshold'
|
|
];
|
|
|
|
// Update each setting
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (validSettings.includes(key)) {
|
|
await db('app_settings')
|
|
.where('setting_key', key)
|
|
.update({
|
|
setting_value: JSON.stringify(value),
|
|
updated_at: new Date()
|
|
});
|
|
}
|
|
}
|
|
|
|
logger.info('Image security settings updated', {
|
|
adminId: req.admin.id,
|
|
updates: Object.keys(updates)
|
|
});
|
|
|
|
res.json({ message: 'Settings updated successfully' });
|
|
} catch (error) {
|
|
logger.error('Error updating image security settings', {
|
|
error: error.message,
|
|
adminId: req.admin.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to update security settings' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get security monitoring dashboard data
|
|
*/
|
|
router.get('/dashboard', adminAuth, async (req, res) => {
|
|
try {
|
|
const { timeframe = '24h' } = req.query;
|
|
|
|
let timeFilter;
|
|
switch (timeframe) {
|
|
case '1h':
|
|
timeFilter = new Date(Date.now() - 3600000);
|
|
break;
|
|
case '24h':
|
|
timeFilter = new Date(Date.now() - 86400000);
|
|
break;
|
|
case '7d':
|
|
timeFilter = new Date(Date.now() - 604800000);
|
|
break;
|
|
default:
|
|
timeFilter = new Date(Date.now() - 86400000);
|
|
}
|
|
|
|
// Get image access statistics
|
|
const accessStats = await db('image_access_logs')
|
|
.where('accessed_at', '>', timeFilter.toISOString())
|
|
.select('access_type')
|
|
.count('* as count')
|
|
.groupBy('access_type');
|
|
|
|
// Get security events
|
|
const securityEvents = await db('security_logs')
|
|
.where('timestamp', '>', timeFilter.toISOString())
|
|
.select('event_type')
|
|
.count('* as count')
|
|
.groupBy('event_type');
|
|
|
|
// Get top suspicious IPs
|
|
const suspiciousIPs = await db('security_logs')
|
|
.where('timestamp', '>', timeFilter.toISOString())
|
|
.where('event_type', 'like', '%suspicious%')
|
|
.select('client_ip')
|
|
.count('* as count')
|
|
.groupBy('client_ip')
|
|
.orderBy('count', 'desc')
|
|
.limit(10);
|
|
|
|
// Get most accessed photos
|
|
const topPhotos = await db('image_access_logs')
|
|
.join('photos', 'image_access_logs.photo_id', 'photos.id')
|
|
.join('events', 'photos.event_id', 'events.id')
|
|
.where('image_access_logs.accessed_at', '>', timeFilter.toISOString())
|
|
.select('photos.filename', 'events.event_name', 'photos.id')
|
|
.count('* as access_count')
|
|
.groupBy('photos.id', 'photos.filename', 'events.event_name')
|
|
.orderBy('access_count', 'desc')
|
|
.limit(10);
|
|
|
|
// Get middleware status
|
|
const middlewareStatus = secureImageMiddleware.getSecurityStatus();
|
|
|
|
// Calculate totals
|
|
const totalAccess = accessStats.reduce((sum, stat) => sum + parseInt(stat.count), 0);
|
|
const totalSecurityEvents = securityEvents.reduce((sum, stat) => sum + parseInt(stat.count), 0);
|
|
|
|
// Get unique visitors
|
|
const uniqueVisitors = await db('image_access_logs')
|
|
.where('accessed_at', '>', timeFilter.toISOString())
|
|
.countDistinct('client_fingerprint as count')
|
|
.first();
|
|
|
|
res.json({
|
|
timeframe,
|
|
summary: {
|
|
totalAccess,
|
|
totalSecurityEvents,
|
|
uniqueVisitors: parseInt(uniqueVisitors.count),
|
|
suspiciousIPsCount: suspiciousIPs.length
|
|
},
|
|
accessStats: accessStats.reduce((acc, stat) => {
|
|
acc[stat.access_type] = parseInt(stat.count);
|
|
return acc;
|
|
}, {}),
|
|
securityEvents: securityEvents.reduce((acc, stat) => {
|
|
acc[stat.event_type] = parseInt(stat.count);
|
|
return acc;
|
|
}, {}),
|
|
suspiciousIPs: suspiciousIPs.map(ip => ({
|
|
ip: ip.client_ip,
|
|
incidents: parseInt(ip.count)
|
|
})),
|
|
topPhotos: topPhotos.map(photo => ({
|
|
id: photo.id,
|
|
filename: photo.filename,
|
|
eventName: photo.event_name,
|
|
accessCount: parseInt(photo.access_count)
|
|
})),
|
|
middlewareStatus
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error getting security dashboard data', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to get dashboard data' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get detailed security logs
|
|
*/
|
|
router.get('/logs', adminAuth, async (req, res) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
limit = 50,
|
|
eventType = null,
|
|
timeframe = '24h'
|
|
} = req.query;
|
|
|
|
let timeFilter;
|
|
switch (timeframe) {
|
|
case '1h':
|
|
timeFilter = new Date(Date.now() - 3600000);
|
|
break;
|
|
case '24h':
|
|
timeFilter = new Date(Date.now() - 86400000);
|
|
break;
|
|
case '7d':
|
|
timeFilter = new Date(Date.now() - 604800000);
|
|
break;
|
|
default:
|
|
timeFilter = new Date(Date.now() - 86400000);
|
|
}
|
|
|
|
let query = db('security_logs')
|
|
.where('timestamp', '>', timeFilter.toISOString())
|
|
.orderBy('timestamp', 'desc');
|
|
|
|
if (eventType) {
|
|
query = query.where('event_type', eventType);
|
|
}
|
|
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
const logs = await query.limit(parseInt(limit)).offset(offset);
|
|
|
|
// Get total count for pagination
|
|
let countQuery = db('security_logs')
|
|
.where('timestamp', '>', timeFilter.toISOString())
|
|
.count('* as total');
|
|
|
|
if (eventType) {
|
|
countQuery = countQuery.where('event_type', eventType);
|
|
}
|
|
|
|
const totalResult = await countQuery.first();
|
|
const total = parseInt(totalResult.total);
|
|
|
|
res.json({
|
|
logs: logs.map(log => ({
|
|
...log,
|
|
details: log.details ? JSON.parse(log.details) : null
|
|
})),
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
pages: Math.ceil(total / parseInt(limit))
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error getting security logs', { error: error.message });
|
|
res.status(500).json({ error: 'Failed to get security logs' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get image access logs for a specific event
|
|
*/
|
|
router.get('/events/:eventId/access-logs', adminAuth, async (req, res) => {
|
|
try {
|
|
const { eventId } = req.params;
|
|
const { page = 1, limit = 50 } = req.query;
|
|
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
const logs = await db('image_access_logs')
|
|
.join('photos', 'image_access_logs.photo_id', 'photos.id')
|
|
.where('image_access_logs.event_id', eventId)
|
|
.select(
|
|
'image_access_logs.*',
|
|
'photos.filename'
|
|
)
|
|
.orderBy('image_access_logs.accessed_at', 'desc')
|
|
.limit(parseInt(limit))
|
|
.offset(offset);
|
|
|
|
const totalResult = await db('image_access_logs')
|
|
.where('event_id', eventId)
|
|
.count('* as total')
|
|
.first();
|
|
|
|
const total = parseInt(totalResult.total);
|
|
|
|
res.json({
|
|
logs: logs.map(log => ({
|
|
...log,
|
|
metadata: log.metadata ? JSON.parse(log.metadata) : null
|
|
})),
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
pages: Math.ceil(total / parseInt(limit))
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error getting event access logs', {
|
|
error: error.message,
|
|
eventId: req.params.eventId
|
|
});
|
|
res.status(500).json({ error: 'Failed to get access logs' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Block/unblock suspicious IPs
|
|
*/
|
|
router.post('/block-ip', adminAuth, async (req, res) => {
|
|
try {
|
|
const { ip, action = 'block' } = req.body;
|
|
|
|
if (!ip) {
|
|
return res.status(400).json({ error: 'IP address required' });
|
|
}
|
|
|
|
if (action === 'block') {
|
|
// Add to blocked IPs in middleware
|
|
secureImageMiddleware.suspiciousIPs.add(ip);
|
|
|
|
logger.warn('IP manually blocked by admin', {
|
|
ip,
|
|
adminId: req.admin.id,
|
|
adminUsername: req.admin.username
|
|
});
|
|
} else if (action === 'unblock') {
|
|
// Remove from blocked IPs
|
|
secureImageMiddleware.suspiciousIPs.delete(ip);
|
|
|
|
logger.info('IP manually unblocked by admin', {
|
|
ip,
|
|
adminId: req.admin.id,
|
|
adminUsername: req.admin.username
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
message: `IP ${ip} ${action}ed successfully`,
|
|
action,
|
|
ip
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error blocking/unblocking IP', {
|
|
error: error.message,
|
|
adminId: req.admin.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to update IP status' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Clear security logs older than specified time
|
|
*/
|
|
router.delete('/logs/cleanup', adminAuth, async (req, res) => {
|
|
try {
|
|
const { olderThan = '30d' } = req.body;
|
|
|
|
let cutoffDate;
|
|
switch (olderThan) {
|
|
case '7d':
|
|
cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
break;
|
|
case '30d':
|
|
cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
break;
|
|
case '90d':
|
|
cutoffDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
break;
|
|
default:
|
|
cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
}
|
|
|
|
// Delete old security logs
|
|
const securityDeleted = await db('security_logs')
|
|
.where('timestamp', '<', cutoffDate.toISOString())
|
|
.del();
|
|
|
|
// Delete old image access logs
|
|
const accessDeleted = await db('image_access_logs')
|
|
.where('accessed_at', '<', cutoffDate.toISOString())
|
|
.del();
|
|
|
|
logger.info('Security logs cleanup completed', {
|
|
adminId: req.admin.id,
|
|
securityLogsDeleted: securityDeleted,
|
|
accessLogsDeleted: accessDeleted,
|
|
cutoffDate: cutoffDate.toISOString()
|
|
});
|
|
|
|
res.json({
|
|
message: 'Cleanup completed successfully',
|
|
deleted: {
|
|
securityLogs: securityDeleted,
|
|
accessLogs: accessDeleted
|
|
},
|
|
cutoffDate: cutoffDate.toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error cleaning up security logs', {
|
|
error: error.message,
|
|
adminId: req.admin.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to cleanup logs' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Export security data for analysis
|
|
*/
|
|
router.get('/export', adminAuth, async (req, res) => {
|
|
try {
|
|
const { format = 'json', timeframe = '7d' } = req.query;
|
|
|
|
let timeFilter;
|
|
switch (timeframe) {
|
|
case '24h':
|
|
timeFilter = new Date(Date.now() - 86400000);
|
|
break;
|
|
case '7d':
|
|
timeFilter = new Date(Date.now() - 604800000);
|
|
break;
|
|
case '30d':
|
|
timeFilter = new Date(Date.now() - 2592000000);
|
|
break;
|
|
default:
|
|
timeFilter = new Date(Date.now() - 604800000);
|
|
}
|
|
|
|
// Get security logs
|
|
const securityLogs = await db('security_logs')
|
|
.where('timestamp', '>', timeFilter.toISOString())
|
|
.orderBy('timestamp', 'desc');
|
|
|
|
// Get image access logs
|
|
const accessLogs = await db('image_access_logs')
|
|
.where('accessed_at', '>', timeFilter.toISOString())
|
|
.orderBy('accessed_at', 'desc');
|
|
|
|
const exportData = {
|
|
exportDate: new Date().toISOString(),
|
|
timeframe,
|
|
securityLogs: securityLogs.map(log => ({
|
|
...log,
|
|
details: log.details ? JSON.parse(log.details) : null
|
|
})),
|
|
accessLogs: accessLogs.map(log => ({
|
|
...log,
|
|
metadata: log.metadata ? JSON.parse(log.metadata) : null
|
|
}))
|
|
};
|
|
|
|
if (format === 'csv') {
|
|
// Convert to CSV format (simplified)
|
|
const csv = convertToCSV(exportData);
|
|
res.set({
|
|
'Content-Type': 'text/csv',
|
|
'Content-Disposition': `attachment; filename="security-export-${timeframe}.csv"`
|
|
});
|
|
res.send(csv);
|
|
} else {
|
|
res.set({
|
|
'Content-Type': 'application/json',
|
|
'Content-Disposition': `attachment; filename="security-export-${timeframe}.json"`
|
|
});
|
|
res.json(exportData);
|
|
}
|
|
|
|
logger.info('Security data exported', {
|
|
adminId: req.admin.id,
|
|
format,
|
|
timeframe,
|
|
recordCount: exportData.securityLogs.length + exportData.accessLogs.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error exporting security data', {
|
|
error: error.message,
|
|
adminId: req.admin.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to export security data' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Helper function to convert data to CSV
|
|
*/
|
|
function convertToCSV(data) {
|
|
// Simplified CSV conversion for security logs
|
|
const headers = ['timestamp', 'event_type', 'client_ip', 'details'];
|
|
const rows = data.securityLogs.map(log => [
|
|
log.timestamp,
|
|
log.event_type,
|
|
log.client_ip,
|
|
JSON.stringify(log.details || {})
|
|
]);
|
|
|
|
return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
|
|
}
|
|
|
|
module.exports = router; |