Files
picpeak/backend/src/routes/adminImageSecurity.js
T
paul 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
chore: clean up codebase for production readiness
- 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>
2025-08-24 23:19:30 +02:00

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;