diff --git a/backend/src/config/multerConfig.js b/backend/src/config/multerConfig.js index 98365cce..48f2aa69 100644 --- a/backend/src/config/multerConfig.js +++ b/backend/src/config/multerConfig.js @@ -9,6 +9,7 @@ const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { validateFileType } = require('../utils/fileSecurityUtils'); +const logger = require('../utils/logger'); /** * Get the storage path from environment or default @@ -220,14 +221,14 @@ const createCustomUploader = (config) => { const uploadTimeoutMiddleware = (timeout = 300000) => { return (req, res, next) => { req.setTimeout(timeout, () => { - console.error('Upload request timed out'); + logger.error('Upload request timed out'); if (!res.headersSent) { res.status(408).json({ error: 'Upload request timed out' }); } }); res.setTimeout(timeout, () => { - console.error('Upload response timed out'); + logger.error('Upload response timed out'); }); next(); diff --git a/backend/src/middleware/maintenance.js b/backend/src/middleware/maintenance.js index 37ffb202..bb1493f3 100644 --- a/backend/src/middleware/maintenance.js +++ b/backend/src/middleware/maintenance.js @@ -1,4 +1,5 @@ const { db } = require('../database/db'); +const logger = require('../utils/logger'); // Cache maintenance mode status to avoid DB queries on every request let maintenanceMode = false; @@ -26,7 +27,7 @@ async function queryWithRetry(queryFn, retries = MAX_RETRIES) { error.code === 'ECONNRESET'; if (isConnectionError) { - console.warn(`Database connection error, retrying in ${RETRY_DELAY}ms... (attempt ${i + 1}/${retries})`); + logger.warn(`Database connection error, retrying in ${RETRY_DELAY}ms... (attempt ${i + 1}/${retries})`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); } else { throw error; // Don't retry non-connection errors @@ -56,7 +57,7 @@ async function checkMaintenanceMode() { return maintenanceMode; } catch (error) { - console.error('Error checking maintenance mode after retries:', error.message); + logger.error('Error checking maintenance mode after retries:', error.message); // Return cached value or false if no cache return maintenanceMode; } @@ -102,7 +103,7 @@ async function maintenanceMiddleware(req, res, next) { } } catch (error) { // If we can't check maintenance mode, allow the request to proceed - console.error('Failed to check maintenance mode, allowing request:', error.message); + logger.error('Failed to check maintenance mode, allowing request:', error.message); } next(); diff --git a/backend/src/middleware/secureImageMiddleware.js b/backend/src/middleware/secureImageMiddleware.js index a2733122..d881c1be 100644 --- a/backend/src/middleware/secureImageMiddleware.js +++ b/backend/src/middleware/secureImageMiddleware.js @@ -333,7 +333,7 @@ class SecureImageMiddleware { await db('security_logs').insert(logData).catch(console.error); } } catch (error) { - console.error('Error logging security event:', error); + logger.error('Error logging security event:', error); } } @@ -363,7 +363,7 @@ class SecureImageMiddleware { perHour: config.perHour || 500 }; } catch (error) { - console.error('Error getting rate limit settings:', error); + logger.error('Error getting rate limit settings:', error); return { perMinute: 30, per5Minutes: 100, perHour: 500 }; } } diff --git a/backend/src/middleware/secureStatic.js b/backend/src/middleware/secureStatic.js index 2572d3d2..88361dd3 100644 --- a/backend/src/middleware/secureStatic.js +++ b/backend/src/middleware/secureStatic.js @@ -1,6 +1,7 @@ const path = require('path'); const express = require('express'); const { safePathJoin, isPathSafe } = require('../utils/fileSecurityUtils'); +const logger = require('../utils/logger'); /** * Create a secure static file serving middleware that prevents path traversal attacks @@ -17,7 +18,7 @@ function secureStatic(basePath, options = {}) { // Validate the path doesn't contain dangerous patterns if (!isPathSafe(requestedPath)) { - console.warn(`Potential path traversal attempt blocked: ${requestedPath}`); + logger.warn(`Potential path traversal attempt blocked: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } @@ -52,7 +53,7 @@ function secureStatic(basePath, options = {}) { return staticMiddleware(req, res, next); } catch (error) { // Path traversal detected - console.error(`Path traversal blocked: ${requestedPath}`, error.message); + logger.error(`Path traversal blocked: ${requestedPath}`, error.message); return res.status(403).json({ error: 'Access denied' }); } }; diff --git a/backend/src/middleware/sessionTimeout.js b/backend/src/middleware/sessionTimeout.js index 3c7968bb..5edce16f 100644 --- a/backend/src/middleware/sessionTimeout.js +++ b/backend/src/middleware/sessionTimeout.js @@ -1,6 +1,7 @@ const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const { getAdminTokenFromRequest } = require('../utils/tokenUtils'); +const logger = require('../utils/logger'); // In-memory session tracking (in production, use Redis) const sessions = new Map(); @@ -69,7 +70,7 @@ async function getSessionTimeout() { } catch (error) { // Only log if it's not a connection error (to avoid spam) if (error.code !== 'ECONNRESET' && !error.message?.includes('Connection terminated')) { - console.error('Error getting session timeout:', error.message); + logger.error('Error getting session timeout:', error.message); } } diff --git a/backend/src/routes/adminArchives.js b/backend/src/routes/adminArchives.js index b9735550..d81c8713 100644 --- a/backend/src/routes/adminArchives.js +++ b/backend/src/routes/adminArchives.js @@ -9,14 +9,14 @@ const { requirePermission } = require('../middleware/permissions'); const archiver = require('archiver'); const StreamZip = require('node-stream-zip'); const { requireEventOwnership } = require('../middleware/ownership'); +const logger = require('../utils/logger'); +const { getPagination } = require('../utils/routeHelpers'); const router = express.Router(); // Get all archived events router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) => { try { - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 20; - const offset = (page - 1) * limit; + const { page, limit, offset } = getPagination(req); // Get total count const totalCount = await db('events') @@ -48,7 +48,7 @@ router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) const stats = await fs.stat(fullArchivePath); archiveFileSize = stats.size; } catch (error) { - console.error(`Archive file not found: ${archive.archive_path}`); + logger.error(`Archive file not found: ${archive.archive_path}`); } } @@ -78,7 +78,7 @@ router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) } }); } catch (error) { - console.error('Archives list error:', error); + logger.error('Archives list error:', error); res.status(500).json({ error: 'Failed to fetch archives' }); } }); @@ -113,7 +113,7 @@ router.get('/:id', adminAuth, requirePermission('archives.view'), requireEventOw path: archive.archive_path }; } catch (error) { - console.error('Archive file not found:', error); + logger.error('Archive file not found:', error); } } @@ -134,7 +134,7 @@ router.get('/:id', adminAuth, requirePermission('archives.view'), requireEventOw archiveFile: archiveFileInfo }); } catch (error) { - console.error('Archive details error:', error); + logger.error('Archive details error:', error); res.status(500).json({ error: 'Failed to fetch archive details' }); } }); @@ -179,9 +179,9 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re await fs.mkdir(eventDir, { recursive: true }); // Log ZIP contents for debugging - console.log(`Extracting archive to: ${eventDir}`); + logger.info(`Extracting archive to: ${eventDir}`); const entries = Object.values(await zip.entries()); - console.log(`Archive contains ${entries.length} entries`); + logger.info(`Archive contains ${entries.length} entries`); // Stream-extract everything to disk await zip.extract(null, eventDir); @@ -203,12 +203,12 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re if (m && m.filename) manifestByFilename.set(m.filename, m); } } - console.log(`Loaded photos manifest: ${manifestByFilename.size} entries`); + logger.info(`Loaded photos manifest: ${manifestByFilename.size} entries`); } catch (e) { if (e.code !== 'ENOENT') { - console.warn('Photos manifest present but unreadable; falling back to filenames', e.message); + logger.warn('Photos manifest present but unreadable; falling back to filenames', e.message); } else { - console.log('No photos manifest in archive (older archive); original_filename falls back to filename'); + logger.info('No photos manifest in archive (older archive); original_filename falls back to filename'); } } @@ -286,9 +286,9 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re }); } } catch (statError) { - console.error(`Failed to stat file: ${actualFilePath}`); - console.error(`Entry name was: ${entry.name}`); - console.error('Error:', statError.message); + logger.error(`Failed to stat file: ${actualFilePath}`); + logger.error(`Entry name was: ${entry.name}`); + logger.error('Error:', statError.message); // Skip this file if we can't stat it continue; } @@ -301,7 +301,7 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re } } catch (extractError) { - console.error('Archive extraction error:', extractError); + logger.error('Archive extraction error:', extractError); return res.status(500).json({ error: 'Failed to extract archive: ' + extractError.message }); } @@ -331,7 +331,7 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re res.json({ message: 'Archive restored successfully' }); } catch (error) { - console.error('Archive restore error:', error); + logger.error('Archive restore error:', error); res.status(500).json({ error: 'Failed to restore archive' }); } }); @@ -380,7 +380,7 @@ router.get('/:id/download', adminAuth, requirePermission('archives.download'), r metadata: JSON.stringify({ event_name: archive.event_name }) }); } catch (error) { - console.error('Archive download error:', error); + logger.error('Archive download error:', error); res.status(500).json({ error: 'Failed to download archive' }); } }); @@ -404,7 +404,7 @@ router.delete('/:id', adminAuth, requirePermission('archives.delete'), requireEv const fullArchivePath = path.join(storagePath, archive.archive_path); await fs.unlink(fullArchivePath); } catch (error) { - console.error('Failed to delete archive file:', error); + logger.error('Failed to delete archive file:', error); } } @@ -440,7 +440,7 @@ router.delete('/:id', adminAuth, requirePermission('archives.delete'), requireEv res.json({ message: 'Archive deleted permanently' }); } catch (error) { - console.error('Archive delete error:', error); + logger.error('Archive delete error:', error); res.status(500).json({ error: 'Failed to delete archive' }); } }); diff --git a/backend/src/routes/adminBackup.js b/backend/src/routes/adminBackup.js index 81ee5db2..5cf39f18 100644 --- a/backend/src/routes/adminBackup.js +++ b/backend/src/routes/adminBackup.js @@ -4,6 +4,7 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { triggerManualBackup, getBackupStatus, cleanupOldBackupRuns, getBackupManifest, validateBackupManifest } = require('../services/backupService'); const logger = require('../utils/logger'); +const { errorResponse, getPagination } = require('../utils/routeHelpers'); const { formatBytes } = require('../utils/formatBytes'); const fs = require('fs').promises; const path = require('path'); @@ -31,8 +32,7 @@ router.get('/config', adminAuth, requirePermission('backup.view'), async (req, r res.json(config); } catch (error) { - logger.error('Failed to get backup configuration:', error); - res.status(500).json({ error: 'Failed to get backup configuration' }); + errorResponse(res, error, 500, 'Failed to get backup configuration'); } }); @@ -93,21 +93,19 @@ router.put('/config', adminAuth, requirePermission('backup.create'), async (req, res.json({ success: true, message: 'Backup configuration updated' }); } catch (error) { - logger.error('Failed to update backup configuration:', error); - res.status(500).json({ error: 'Failed to update backup configuration' }); + errorResponse(res, error, 500, 'Failed to update backup configuration'); } }); // Get backup status and history router.get('/status', adminAuth, requirePermission('backup.view'), async (req, res) => { try { - const limit = parseInt(req.query.limit) || 10; + const { limit } = getPagination(req, { limit: 10 }); const status = await getBackupStatus(limit); res.json(status); } catch (error) { - logger.error('Failed to get backup status:', error); - res.status(500).json({ error: 'Failed to get backup status' }); + errorResponse(res, error, 500, 'Failed to get backup status'); } }); @@ -127,8 +125,7 @@ router.post('/run', adminAuth, requirePermission('backup.create'), async (req, r res.json({ success: true, message: 'Backup started' }); } catch (error) { - logger.error('Failed to trigger manual backup:', error); - res.status(500).json({ error: 'Failed to trigger backup' }); + errorResponse(res, error, 500, 'Failed to trigger backup'); } }); @@ -156,8 +153,7 @@ router.get('/runs/:id', adminAuth, requirePermission('backup.view'), async (req, res.json(run); } catch (error) { - logger.error('Failed to get backup run details:', error); - res.status(500).json({ error: 'Failed to get backup run details' }); + errorResponse(res, error, 500, 'Failed to get backup run details'); } }); @@ -191,8 +187,7 @@ router.get('/files', adminAuth, requirePermission('backup.view'), async (req, re } }); } catch (error) { - logger.error('Failed to get backup file states:', error); - res.status(500).json({ error: 'Failed to get file states' }); + errorResponse(res, error, 500, 'Failed to get file states'); } }); @@ -205,8 +200,7 @@ router.delete('/cleanup', adminAuth, requirePermission('backup.delete'), async ( res.json({ success: true, message: `Cleaned up backup runs older than ${days} days` }); } catch (error) { - logger.error('Failed to cleanup old backup runs:', error); - res.status(500).json({ error: 'Failed to cleanup backup runs' }); + errorResponse(res, error, 500, 'Failed to cleanup backup runs'); } }); @@ -337,8 +331,7 @@ router.post('/test-connection', adminAuth, requirePermission('backup.create'), a res.status(400).json({ error: 'Invalid destination type' }); } } catch (error) { - logger.error('Failed to test backup connection:', error); - res.status(500).json({ error: 'Failed to test connection' }); + errorResponse(res, error, 500, 'Failed to test connection'); } }); @@ -381,8 +374,7 @@ router.post('/manifest/validate', adminAuth, requirePermission('backup.view'), a manifestPath }); } catch (error) { - logger.error('Failed to validate manifest:', error); - res.status(500).json({ error: 'Failed to validate manifest' }); + errorResponse(res, error, 500, 'Failed to validate manifest'); } }); @@ -494,8 +486,7 @@ router.post('/manifests/validate', adminAuth, requirePermission('backup.view'), manifestPath }); } catch (error) { - logger.error('Failed to validate manifest:', error); - res.status(500).json({ error: 'Failed to validate manifest' }); + errorResponse(res, error, 500, 'Failed to validate manifest'); } }); @@ -526,8 +517,7 @@ router.get('/s3/buckets', adminAuth, requirePermission('backup.view'), async (re owner: result.Owner || null }); } catch (error) { - logger.error('Failed to list S3 buckets:', error); - res.status(500).json({ error: 'Failed to list S3 buckets' }); + errorResponse(res, error, 500, 'Failed to list S3 buckets'); } }); @@ -563,8 +553,7 @@ router.get('/s3/files', adminAuth, requirePermission('backup.view'), async (req, prefix: prefix }); } catch (error) { - logger.error('Failed to list S3 files:', error); - res.status(500).json({ error: 'Failed to list S3 files' }); + errorResponse(res, error, 500, 'Failed to list S3 files'); } }); @@ -625,8 +614,7 @@ router.delete('/s3/cleanup', adminAuth, requirePermission('backup.delete'), asyn message: `Cleaned up ${deletedCount} S3 backup files older than ${retentionDays} days` }); } catch (error) { - logger.error('Failed to cleanup S3 backups:', error); - res.status(500).json({ error: 'Failed to cleanup S3 backups' }); + errorResponse(res, error, 500, 'Failed to cleanup S3 backups'); } }); @@ -677,8 +665,7 @@ router.post('/s3/test-upload', adminAuth, requirePermission('backup.create'), as message: 'S3 upload test completed successfully' }); } catch (error) { - logger.error('S3 upload test failed:', error); - res.status(500).json({ error: 'S3 upload test failed' }); + errorResponse(res, error, 500, 'S3 upload test failed'); } }); @@ -765,8 +752,7 @@ router.get('/download/:backupId', adminAuth, requirePermission('backup.view'), a return res.status(400).json({ error: 'Unknown backup type' }); } } catch (error) { - logger.error('Failed to download backup:', error); - res.status(500).json({ error: 'Failed to download backup' }); + errorResponse(res, error, 500, 'Failed to download backup'); } }); @@ -838,8 +824,7 @@ router.get('/checksums', adminAuth, requirePermission('backup.view'), async (req path: targetPath || '/' }); } catch (error) { - logger.error('Failed to get file checksums:', error); - res.status(500).json({ error: 'Failed to get file checksums' }); + errorResponse(res, error, 500, 'Failed to get file checksums'); } }); @@ -941,8 +926,7 @@ router.post('/estimate', adminAuth, requirePermission('backup.view'), async (req warnings: totalSize > 10 * 1024 * 1024 * 1024 ? ['Backup size exceeds 10GB, may take significant time'] : [] }); } catch (error) { - logger.error('Failed to estimate backup size:', error); - res.status(500).json({ error: 'Failed to estimate backup size' }); + errorResponse(res, error, 500, 'Failed to estimate backup size'); } }); diff --git a/backend/src/routes/adminCMS.js b/backend/src/routes/adminCMS.js index 7af0c2f6..db4bcc9f 100644 --- a/backend/src/routes/adminCMS.js +++ b/backend/src/routes/adminCMS.js @@ -7,6 +7,7 @@ const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { validateFileType } = require('../utils/fileSecurityUtils'); +const logger = require('../utils/logger'); const router = express.Router(); const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); @@ -43,7 +44,7 @@ router.get('/pages', adminAuth, requirePermission('cms.view'), async (req, res) const pages = await db('cms_pages').select('*').orderBy('slug', 'asc'); res.json(pages); } catch (error) { - console.error('Error fetching CMS pages:', error); + logger.error('Error fetching CMS pages:', error); res.status(500).json({ error: 'Failed to fetch pages' }); } }); @@ -60,7 +61,7 @@ router.get('/pages/:slug', adminAuth, requirePermission('cms.view'), async (req, res.json(page); } catch (error) { - console.error('Error fetching CMS page:', error); + logger.error('Error fetching CMS page:', error); res.status(500).json({ error: 'Failed to fetch page' }); } }); @@ -144,7 +145,7 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [ res.json(updated); } catch (error) { - console.error('Error updating CMS page:', error); + logger.error('Error updating CMS page:', error); res.status(500).json({ error: 'Failed to update page' }); } }); @@ -184,7 +185,7 @@ router.post( res.json({ logo_url: logoUrl }); } catch (error) { - console.error('Error uploading CMS page logo:', error); + logger.error('Error uploading CMS page logo:', error); res.status(500).json({ error: 'Failed to upload logo' }); } } @@ -208,7 +209,7 @@ router.delete( res.json({ logo_url: null }); } catch (error) { - console.error('Error clearing CMS page logo:', error); + logger.error('Error clearing CMS page logo:', error); res.status(500).json({ error: 'Failed to clear logo' }); } } diff --git a/backend/src/routes/adminCategories.js b/backend/src/routes/adminCategories.js index 55a18b25..99bd811f 100644 --- a/backend/src/routes/adminCategories.js +++ b/backend/src/routes/adminCategories.js @@ -4,6 +4,7 @@ const { db, logActivity } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); +const logger = require('../utils/logger'); const router = express.Router(); // Get all global categories @@ -15,7 +16,7 @@ router.get('/global', adminAuth, requirePermission('settings.view'), async (req, res.json(categories); } catch (error) { - console.error('Error fetching categories:', error); + logger.error('Error fetching categories:', error); res.status(500).json({ error: 'Failed to fetch categories' }); } }); @@ -35,7 +36,7 @@ router.get('/event/:eventId', adminAuth, requirePermission('settings.view'), asy res.json(categories); } catch (error) { - console.error('Error fetching event categories:', error); + logger.error('Error fetching event categories:', error); res.status(500).json({ error: 'Failed to fetch categories' }); } }); @@ -101,7 +102,7 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [ res.json(category); } catch (error) { - console.error('Error creating category:', error); + logger.error('Error creating category:', error); res.status(500).json({ error: 'Failed to create category' }); } }); @@ -165,7 +166,7 @@ router.put('/:id', adminAuth, requirePermission('settings.edit'), [ res.json(updated); } catch (error) { - console.error('Error updating category:', error); + logger.error('Error updating category:', error); res.status(500).json({ error: 'Failed to update category' }); } }); @@ -214,7 +215,7 @@ router.put('/:id/hero', adminAuth, requirePermission('settings.edit'), [ res.json(updated); } catch (error) { - console.error('Error updating category hero:', error); + logger.error('Error updating category hero:', error); res.status(500).json({ error: 'Failed to update category hero' }); } }); @@ -248,7 +249,7 @@ router.delete('/:id', adminAuth, requirePermission('settings.edit'), async (req, res.json({ message: 'Category deleted successfully' }); } catch (error) { - console.error('Error deleting category:', error); + logger.error('Error deleting category:', error); res.status(500).json({ error: 'Failed to delete category' }); } }); diff --git a/backend/src/routes/adminCssTemplates.js b/backend/src/routes/adminCssTemplates.js index b3e4ef54..4bb4b288 100644 --- a/backend/src/routes/adminCssTemplates.js +++ b/backend/src/routes/adminCssTemplates.js @@ -11,6 +11,7 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { sanitizeCSS, validateCSS, MAX_CSS_SIZE } = require('../utils/cssSanitizer'); const { DEFAULT_CSS_TEMPLATE } = require('../../migrations/core/052_add_css_templates'); +const logger = require('../utils/logger'); /** * GET /admin/css-templates @@ -23,7 +24,7 @@ router.get('/', adminAuth, requirePermission('branding.view'), async (req, res) ); res.json({ success: true, templates }); } catch (error) { - console.error('Get CSS templates error:', error); + logger.error('Get CSS templates error:', error); res.status(500).json({ error: 'Failed to fetch templates' }); } }); @@ -42,7 +43,7 @@ router.get('/enabled', adminAuth, requirePermission('branding.view'), async (req ); res.json({ success: true, templates }); } catch (error) { - console.error('Get enabled templates error:', error); + logger.error('Get enabled templates error:', error); res.status(500).json({ error: 'Failed to fetch templates' }); } }); @@ -73,7 +74,7 @@ router.get('/:slotNumber', adminAuth, requirePermission('branding.view'), [ res.json({ success: true, template }); } catch (error) { - console.error('Get template error:', error); + logger.error('Get template error:', error); res.status(500).json({ error: 'Failed to fetch template' }); } }); @@ -150,7 +151,7 @@ router.put('/:slotNumber', adminAuth, requirePermission('branding.edit'), [ sanitization_warnings: warnings }); } catch (error) { - console.error('Update template error:', error); + logger.error('Update template error:', error); res.status(500).json({ error: 'Failed to update template' }); } }); @@ -187,7 +188,7 @@ router.post('/:slotNumber/reset', adminAuth, requirePermission('branding.edit'), res.json({ success: true, template }); } catch (error) { - console.error('Reset template error:', error); + logger.error('Reset template error:', error); res.status(500).json({ error: 'Failed to reset template' }); } }); diff --git a/backend/src/routes/adminDashboard.js b/backend/src/routes/adminDashboard.js index 6a67b771..f0681604 100644 --- a/backend/src/routes/adminDashboard.js +++ b/backend/src/routes/adminDashboard.js @@ -6,6 +6,7 @@ const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity'); const { formatBoolean } = require('../utils/dbCompat'); const { resolveAdapter } = require('../services/trackers'); const logger = require('../utils/logger'); +const { errorResponse, getPagination } = require('../utils/routeHelpers'); const router = express.Router(); /** @@ -126,16 +127,15 @@ router.get('/stats', adminAuth, requirePermission('analytics.view'), async (req, totalEvents: totalEvents.count || 0 }); } catch (error) { - console.error('Dashboard stats error:', error); - res.status(500).json({ error: 'Failed to fetch dashboard statistics' }); + errorResponse(res, error, 500, 'Failed to fetch dashboard statistics'); } }); // Get recent activity router.get('/activity', adminAuth, requirePermission('analytics.view'), async (req, res) => { try { - const limit = parseInt(req.query.limit) || 10; - + const { limit } = getPagination(req, { limit: 10 }); + const activities = await db('activity_logs') .select('activity_logs.*', 'events.event_name') .leftJoin('events', 'activity_logs.event_id', 'events.id') @@ -155,7 +155,7 @@ router.get('/activity', adminAuth, requirePermission('analytics.view'), async (r if (typeof activity.metadata === 'object') return activity.metadata; return JSON.parse(activity.metadata); } catch (e) { - console.warn('Failed to parse metadata for activity:', activity.id, e.message); + logger.warn('Failed to parse metadata for activity:', activity.id, e.message); return {}; } })(), @@ -164,8 +164,7 @@ router.get('/activity', adminAuth, requirePermission('analytics.view'), async (r res.json(formattedActivities); } catch (error) { - console.error('Activity log error:', error); - res.status(500).json({ error: 'Failed to fetch activity log' }); + errorResponse(res, error, 500, 'Failed to fetch activity log'); } }); @@ -233,7 +232,7 @@ router.get('/health', adminAuth, requirePermission('settings.view'), async (req, } }); } catch (error) { - console.error('Health check error:', error); + logger.error('Health check error:', error); res.status(500).json({ overall: 'error', error: 'Failed to check system health' @@ -400,8 +399,7 @@ router.get('/analytics', adminAuth, requirePermission('analytics.view'), async ( } }); } catch (error) { - console.error('Analytics error:', error); - res.status(500).json({ error: 'Failed to fetch analytics data' }); + errorResponse(res, error, 500, 'Failed to fetch analytics data'); } }); @@ -586,8 +584,7 @@ router.get('/crm-stats', adminAuth, async (req, res) => { generatedAt: new Date().toISOString(), }); } catch (error) { - require('../utils/logger').error('CRM stats error:', error); - res.status(500).json({ error: 'Failed to load CRM stats' }); + errorResponse(res, error, 500, 'Failed to load CRM stats'); } }); diff --git a/backend/src/routes/adminDatabaseBackup.js b/backend/src/routes/adminDatabaseBackup.js index cd649886..35658240 100644 --- a/backend/src/routes/adminDatabaseBackup.js +++ b/backend/src/routes/adminDatabaseBackup.js @@ -5,6 +5,7 @@ const { requirePermission } = require('../middleware/permissions'); const { databaseBackupService } = require('../services/databaseBackup'); const { db } = require('../database/db'); const logger = require('../utils/logger'); +const { getPagination } = require('../utils/routeHelpers'); // All routes require admin authentication router.use(adminAuth); @@ -154,10 +155,8 @@ router.get('/progress', requirePermission('backup.view'), async (req, res) => { */ router.get('/history', requirePermission('backup.view'), async (req, res) => { try { - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 20; - const offset = (page - 1) * limit; - + const { page, limit, offset } = getPagination(req); + const [backups, totalCount] = await Promise.all([ db('database_backup_runs') .orderBy('started_at', 'desc') diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index 23d3bffa..5fa85ca1 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -5,6 +5,8 @@ const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { wrapEmailHtml, processEmailQueue } = require('../services/emailProcessor'); +const { errorResponse } = require('../utils/routeHelpers'); +const logger = require('../utils/logger'); const router = express.Router(); // Get email configuration @@ -31,8 +33,7 @@ router.get('/config', adminAuth, requirePermission('email.view'), async (req, re smtp_pass: config.smtp_pass ? '********' : '' }); } catch (error) { - console.error('Email config fetch error:', error); - res.status(500).json({ error: 'Failed to fetch email configuration' }); + errorResponse(res, error, 500, 'Failed to fetch email configuration'); } }); @@ -113,8 +114,7 @@ router.post('/config', [ res.json({ message: 'Email configuration updated successfully' }); } catch (error) { - console.error('Email config update error:', error); - res.status(500).json({ error: 'Failed to update email configuration' }); + errorResponse(res, error, 500, 'Failed to update email configuration'); } }); @@ -131,8 +131,7 @@ router.get('/incoming-config', adminAuth, requirePermission('email.view'), async imap_folder: c?.imap_folder || 'INBOX', }); } catch (error) { - console.error('Incoming mail config fetch error:', error); - res.status(500).json({ error: 'Failed to fetch incoming mail configuration' }); + errorResponse(res, error, 500, 'Failed to fetch incoming mail configuration'); } }); @@ -168,8 +167,7 @@ router.post('/incoming-config', [ await logActivity('incoming_mail_config_updated', { imap_host }, null, { type: 'admin', id: req.admin.id, name: req.admin.username }); res.json({ message: 'Incoming mail configuration updated successfully' }); } catch (error) { - console.error('Incoming mail config update error:', error); - res.status(500).json({ error: 'Failed to update incoming mail configuration' }); + errorResponse(res, error, 500, 'Failed to update incoming mail configuration'); } }); @@ -192,7 +190,7 @@ router.post('/incoming-config/folders', adminAuth, requirePermission('email.view ); res.json({ folders }); } catch (error) { - console.error('IMAP folder detection error:', error); + logger.error('IMAP folder detection error:', error); res.status(422).json({ error: `Could not connect to the mailbox (${error.message}). Check host, port (IMAP is usually 993) and credentials.` }); } }); @@ -217,7 +215,7 @@ router.post('/incoming-config/test', adminAuth, requirePermission('email.view'), } res.json(result); } catch (error) { - console.error('IMAP connection test error:', error); + logger.error('IMAP connection test error:', error); res.status(422).json({ error: `Could not connect to the mailbox (${error.message}). Check host, port (IMAP is usually 993), credentials and folder.` }); } }); @@ -239,7 +237,7 @@ router.post('/incoming-config/roundtrip', adminAuth, requirePermission('email.se return res.status(result.reason === 'not_received' ? 504 : 400) .json({ error: map[result.reason] || 'Round-trip test failed.', sent: !!result.sent, recipient: result.recipient }); } catch (error) { - console.error('Round-trip test error:', error); + logger.error('Round-trip test error:', error); res.status(422).json({ error: `Round-trip test failed (${error.message}) — check both SMTP and IMAP settings.` }); } }); @@ -253,7 +251,7 @@ router.post('/incoming-config/poll', adminAuth, requirePermission('email.view'), const result = await emailIntakeService.pollOnce(); res.json(result); // { processed } or { skipped: 'disabled'|'unconfigured'|'busy' } } catch (error) { - console.error('Manual poll error:', error); + logger.error('Manual poll error:', error); res.status(422).json({ error: `Mailbox poll failed (${error.message}).` }); } }); @@ -268,8 +266,7 @@ router.get('/received', adminAuth, requirePermission('email.view'), async (req, const items = await base.clone().orderBy('received_at', 'desc').limit(pageSize).offset((page - 1) * pageSize); res.json({ items, pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) } }); } catch (error) { - console.error('Received emails fetch error:', error); - res.status(500).json({ error: 'Failed to fetch received emails' }); + errorResponse(res, error, 500, 'Failed to fetch received emails'); } }); @@ -322,7 +319,7 @@ router.post('/test', adminAuth, requirePermission('email.send'), async (req, res debug: process.env.NODE_ENV === 'development' }; - console.log('Creating email transporter with config:', { + logger.info('Creating email transporter with config:', { host: transportConfig.host, port: transportConfig.port, secure: transportConfig.secure, @@ -356,8 +353,8 @@ router.post('/test', adminAuth, requirePermission('email.send'), async (req, res res.json({ message: 'Test email sent successfully' }); } catch (error) { - console.error('Test email error:', error); - console.error('Error stack:', error.stack); + logger.error('Test email error:', error); + logger.error('Error stack:', error.stack); // Provide more specific error messages with translation keys let errorMessage = 'Error sending email'; @@ -428,7 +425,7 @@ router.post('/flush-queue', adminAuth, requirePermission('email.send'), async (r } catch (_) { /* activity logging is best-effort */ } res.json({ message: 'Email queue flushed', ...summary }); } catch (error) { - console.error('Flush email queue error:', error); + logger.error('Flush email queue error:', error); res.status(500).json({ error: 'Failed to flush email queue', details: error.message }); } }); @@ -516,7 +513,7 @@ router.get('/queue', adminAuth, requirePermission('email.view'), [ pagination: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) || 1 }, }); } catch (error) { - console.error('List email queue error:', error); + logger.error('List email queue error:', error); res.status(500).json({ error: 'Failed to load email queue', details: error.message }); } }); @@ -528,7 +525,7 @@ function parseVariables(template) { if (typeof template.variables === 'object') return template.variables; return JSON.parse(template.variables); } catch (e) { - console.warn('Failed to parse variables for template:', template.template_key, e.message); + logger.warn('Failed to parse variables for template:', template.template_key, e.message); return []; } } @@ -611,8 +608,7 @@ router.get('/templates', adminAuth, requirePermission('email.view'), async (req, res.json(formattedTemplates); } catch (error) { - console.error('Email templates fetch error:', error); - res.status(500).json({ error: 'Failed to fetch email templates' }); + errorResponse(res, error, 500, 'Failed to fetch email templates'); } }); @@ -641,8 +637,7 @@ router.get('/templates/:key', adminAuth, requirePermission('email.view'), async updated_at: template.updated_at, }); } catch (error) { - console.error('Email template fetch error:', error); - res.status(500).json({ error: 'Failed to fetch email template' }); + errorResponse(res, error, 500, 'Failed to fetch email template'); } }); @@ -730,8 +725,7 @@ router.put('/templates/:key', [ res.json({ message: 'Email template updated successfully' }); } catch (error) { - console.error('Email template update error:', error); - res.status(500).json({ error: 'Failed to update email template' }); + errorResponse(res, error, 500, 'Failed to update email template'); } }); @@ -818,8 +812,7 @@ router.post('/templates', [ return res.status(201).json({ template_key: templateKey, id: templateId }); } catch (error) { - console.error('Email template create error:', error); - return res.status(500).json({ error: 'Failed to create email template' }); + return errorResponse(res, error, 500, 'Failed to create email template'); } }); @@ -897,8 +890,7 @@ router.post('/templates/:key/preview', adminAuth, requirePermission('email.view' language }); } catch (error) { - console.error('Email template preview error:', error); - res.status(500).json({ error: 'Failed to preview email template' }); + errorResponse(res, error, 500, 'Failed to preview email template'); } }); diff --git a/backend/src/routes/adminEventRename.js b/backend/src/routes/adminEventRename.js index 00bdbfc4..a1af4f78 100644 --- a/backend/src/routes/adminEventRename.js +++ b/backend/src/routes/adminEventRename.js @@ -8,6 +8,7 @@ const { body, validationResult } = require('express-validator'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const eventRenameService = require('../services/eventRenameService'); +const logger = require('../utils/logger'); const router = express.Router(); /** @@ -50,7 +51,7 @@ router.post('/:eventId/rename', adminAuth, requirePermission('events.edit'), [ data: result.data }); } catch (error) { - console.error('Error renaming event:', error); + logger.error('Error renaming event:', error); res.status(500).json({ success: false, error: 'Failed to rename event' }); } }); @@ -81,7 +82,7 @@ router.post('/:eventId/validate-rename', adminAuth, requirePermission('events.ed res.json(validation); } catch (error) { - console.error('Error validating rename:', error); + logger.error('Error validating rename:', error); res.status(500).json({ valid: false, error: 'Validation failed' }); } }); diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 4b30546d..591fca39 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -18,6 +18,7 @@ const { escapeLikePattern } = require('../utils/sqlSecurity'); // formatDate import removed - dates are formatted by email processor const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation'); const logger = require('../utils/logger'); +const { errorResponse } = require('../utils/routeHelpers'); const { buildShareLinkVariants } = require('../services/shareLinkService'); const { parseBooleanInput, parseStringInput } = require('../utils/parsers'); const eventTypeService = require('../services/eventTypeService'); @@ -441,7 +442,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [ logger.debug('Create event request body', { body: req.body }); const errors = validationResult(req); if (!errors.isEmpty()) { - console.error('Validation errors:', errors.array()); + logger.error('Validation errors:', errors.array()); return res.status(400).json({ errors: errors.array() }); } @@ -923,8 +924,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [ created_at: new Date().toISOString() }); } catch (error) { - console.error('Error creating event:', error); - res.status(500).json({ error: 'Failed to create event' }); + errorResponse(res, error, 500, 'Failed to create event'); } }); @@ -1022,8 +1022,7 @@ router.get('/', adminAuth, requirePermission('events.view'), async (req, res) => } }); } catch (error) { - console.error('Error fetching events:', error); - res.status(500).json({ error: 'Failed to fetch events' }); + errorResponse(res, error, 500, 'Failed to fetch events'); } }); @@ -1105,8 +1104,7 @@ router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res) })), })); } catch (error) { - console.error('Error fetching event:', error); - res.status(500).json({ error: 'Failed to fetch event details' }); + errorResponse(res, error, 500, 'Failed to fetch event details'); } }); @@ -1264,8 +1262,7 @@ router.post('/:id/publish', adminAuth, requirePermission('events.edit'), require res.json({ message: 'Event published successfully', is_draft: false }); } catch (error) { - logger.error('Error publishing event:', { error: error.message }); - res.status(500).json({ error: 'Failed to publish event' }); + errorResponse(res, error, 500, 'Failed to publish event'); } }); @@ -1451,8 +1448,7 @@ router.post('/:id/duplicate', adminAuth, requirePermission('events.create'), req is_draft: true, }); } catch (error) { - logger.error('Error duplicating event:', { error: error.message }); - res.status(500).json({ error: 'Failed to duplicate event' }); + errorResponse(res, error, 500, 'Failed to duplicate event'); } }); @@ -1831,8 +1827,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne res.json({ message: 'Event updated successfully' }); } catch (error) { - console.error('Error updating event:', error); - res.status(500).json({ error: 'Failed to update event' }); + errorResponse(res, error, 500, 'Failed to update event'); } }); @@ -1891,8 +1886,7 @@ router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), r is_active: newStatus }); } catch (error) { - console.error('Error toggling event status:', error); - res.status(500).json({ error: 'Failed to toggle event status' }); + errorResponse(res, error, 500, 'Failed to toggle event status'); } }); @@ -1958,8 +1952,7 @@ router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit slideshow_url: await buildSlideshowUrl(event.slug, token) }); } catch (error) { - logger.error('Error generating slideshow link', { error: error.message }); - res.status(500).json({ error: 'Failed to generate slideshow link' }); + errorResponse(res, error, 500, 'Failed to generate slideshow link'); } }); @@ -1984,8 +1977,7 @@ router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit' res.json({ show_share_token: null }); } catch (error) { - logger.error('Error disabling slideshow link', { error: error.message }); - res.status(500).json({ error: 'Failed to disable slideshow link' }); + errorResponse(res, error, 500, 'Failed to disable slideshow link'); } }); @@ -2036,8 +2028,7 @@ router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requ show_colorfilter: updates.show_colorfilter ?? event.show_colorfilter ?? 'none' }); } catch (error) { - logger.error('Error updating slideshow settings', { error: error.message }); - res.status(500).json({ error: 'Failed to update slideshow settings' }); + errorResponse(res, error, 500, 'Failed to update slideshow settings'); } }); @@ -2125,8 +2116,7 @@ router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'), emailSent: sendEmail }); } catch (error) { - console.error('Error resetting password:', error); - res.status(500).json({ error: 'Failed to reset password' }); + errorResponse(res, error, 500, 'Failed to reset password'); } }); @@ -2202,7 +2192,7 @@ router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), re name: req.admin.username }); } catch (logError) { - console.error('Warning: Failed to log activity:', logError); + logger.error('Warning: Failed to log activity:', logError); // Don't fail the request if activity logging fails } @@ -2211,9 +2201,8 @@ router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), re message: 'Creation email has been queued for sending' }); } catch (error) { - console.error('Error resending creation email:', error); - console.error('Stack trace:', error.stack); - res.status(500).json({ error: 'Failed to resend creation email' }); + logger.error('Error resending creation email:', error); + errorResponse(res, error, 500, 'Failed to resend creation email'); } }); @@ -2243,8 +2232,7 @@ router.post('/:id/archive', adminAuth, requirePermission('events.archive'), requ res.json({ message: 'Event archived successfully' }); } catch (error) { - console.error('Error archiving event:', error); - res.status(500).json({ error: 'Failed to archive event' }); + errorResponse(res, error, 500, 'Failed to archive event'); } }); @@ -2297,7 +2285,7 @@ router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [ name: event.event_name }); } catch (error) { - console.error(`Failed to archive event ${event.id}:`, error); + logger.error(`Failed to archive event ${event.id}:`, error); results.failed.push({ id: event.id, name: event.event_name, @@ -2322,8 +2310,7 @@ router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [ results }); } catch (error) { - console.error('Error in bulk archive:', error); - res.status(500).json({ error: 'Failed to perform bulk archive' }); + errorResponse(res, error, 500, 'Failed to perform bulk archive'); } }); @@ -2391,8 +2378,7 @@ router.post('/bulk-delete', adminAuth, requirePermission('events.delete'), [ results }); } catch (error) { - logger.error('Error in bulk delete', { error: error.message }); - res.status(500).json({ error: 'Failed to perform bulk delete' }); + errorResponse(res, error, 500, 'Failed to perform bulk delete'); } }); @@ -2446,8 +2432,7 @@ router.post('/:id/logo', adminAuth, requirePermission('events.edit'), requireEve hero_logo_url: logoUrl }); } catch (error) { - logger.error('Error uploading event logo:', { error: error.message, eventId: req.params.id }); - res.status(500).json({ error: 'Failed to upload event logo' }); + errorResponse(res, error, 500, 'Failed to upload event logo'); } }); @@ -2490,8 +2475,7 @@ router.delete('/:id/logo', adminAuth, requirePermission('events.edit'), requireE res.json({ message: 'Event logo removed successfully' }); } catch (error) { - logger.error('Error deleting event logo:', { error: error.message, eventId: req.params.id }); - res.status(500).json({ error: 'Failed to delete event logo' }); + errorResponse(res, error, 500, 'Failed to delete event logo'); } }); diff --git a/backend/src/routes/adminGuests.js b/backend/src/routes/adminGuests.js index 71643fa3..fb7d6ddc 100644 --- a/backend/src/routes/adminGuests.js +++ b/backend/src/routes/adminGuests.js @@ -8,6 +8,7 @@ const { requirePermission } = require('../middleware/permissions'); const { requireEventOwnership } = require('../middleware/ownership'); const feedbackService = require('../services/feedbackService'); const logger = require('../utils/logger'); +const { errorResponse } = require('../utils/routeHelpers'); const FRONTEND_URL = process.env.FRONTEND_URL || ''; @@ -94,8 +95,7 @@ router.get( res.json({ guests }); } catch (error) { - logger.error('Error listing guests:', error); - res.status(500).json({ error: 'Failed to list guests' }); + errorResponse(res, error, 500, 'Failed to list guests'); } } ); @@ -144,8 +144,7 @@ router.get( })), }); } catch (error) { - logger.error('Error fetching aggregate view:', error); - res.status(500).json({ error: 'Failed to fetch aggregate view' }); + errorResponse(res, error, 500, 'Failed to fetch aggregate view'); } } ); @@ -196,8 +195,7 @@ router.get( res.json({ invites }); } catch (error) { - logger.error('Error listing invites:', error); - res.status(500).json({ error: 'Failed to list invites' }); + errorResponse(res, error, 500, 'Failed to list invites'); } } ); @@ -266,8 +264,7 @@ router.post( }, }); } catch (error) { - logger.error('Error creating invite:', error); - res.status(500).json({ error: 'Failed to create invite' }); + errorResponse(res, error, 500, 'Failed to create invite'); } } ); @@ -302,8 +299,7 @@ router.delete( res.json({ success: true }); } catch (error) { - logger.error('Error revoking invite:', error); - res.status(500).json({ error: 'Failed to revoke invite' }); + errorResponse(res, error, 500, 'Failed to revoke invite'); } } ); @@ -457,8 +453,7 @@ router.get( selections, }); } catch (error) { - logger.error('Error fetching guest detail:', error); - res.status(500).json({ error: 'Failed to fetch guest detail' }); + errorResponse(res, error, 500, 'Failed to fetch guest detail'); } } ); @@ -509,8 +504,7 @@ router.get( res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); return res.send(selections.map((s) => s.original_filename || s.filename).join('\n')); } catch (error) { - logger.error('Error exporting guest:', error); - res.status(500).json({ error: 'Failed to export guest' }); + errorResponse(res, error, 500, 'Failed to export guest'); } } ); @@ -548,8 +542,7 @@ router.delete( res.json({ success: true, ...result }); } catch (error) { - logger.error('Error deleting guest:', error); - res.status(500).json({ error: 'Failed to delete guest' }); + errorResponse(res, error, 500, 'Failed to delete guest'); } } ); @@ -600,8 +593,7 @@ router.post( res.json({ success: true, ...result }); } catch (error) { - logger.error('Error merging guests:', error); - res.status(500).json({ error: 'Failed to merge guests' }); + errorResponse(res, error, 500, 'Failed to merge guests'); } } ); diff --git a/backend/src/routes/adminNotifications.js b/backend/src/routes/adminNotifications.js index b7d97f97..822e9427 100644 --- a/backend/src/routes/adminNotifications.js +++ b/backend/src/routes/adminNotifications.js @@ -2,6 +2,7 @@ const express = require('express'); const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); +const logger = require('../utils/logger'); const router = express.Router(); // Get notifications (unread activity logs) @@ -39,7 +40,7 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) if (typeof notification.metadata === 'object') return notification.metadata; return JSON.parse(notification.metadata); } catch (e) { - console.warn('Failed to parse metadata for notification:', notification.id, e.message); + logger.warn('Failed to parse metadata for notification:', notification.id, e.message); return {}; } })(), @@ -59,7 +60,7 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) unreadCount: unreadCount.count || 0 }); } catch (error) { - console.error('Notifications fetch error:', error); + logger.error('Notifications fetch error:', error); res.status(500).json({ error: 'Failed to fetch notifications' }); } }); @@ -77,7 +78,7 @@ router.put('/:id/read', adminAuth, requirePermission('settings.edit'), async (re res.json({ message: 'Notification marked as read' }); } catch (error) { - console.error('Mark notification read error:', error); + logger.error('Mark notification read error:', error); res.status(500).json({ error: 'Failed to mark notification as read' }); } }); @@ -93,7 +94,7 @@ router.put('/read-all', adminAuth, requirePermission('settings.edit'), async (re res.json({ message: 'All notifications marked as read' }); } catch (error) { - console.error('Mark all notifications read error:', error); + logger.error('Mark all notifications read error:', error); res.status(500).json({ error: 'Failed to mark all notifications as read' }); } }); @@ -112,7 +113,7 @@ router.delete('/clear-all', adminAuth, requirePermission('settings.edit'), async const deletedCount = await db('activity_logs').delete(); res.json({ message: 'All notifications cleared', deletedCount }); } catch (error) { - console.error('Clear notifications error:', error); + logger.error('Clear notifications error:', error); res.status(500).json({ error: 'Failed to clear notifications' }); } }); diff --git a/backend/src/routes/adminPhotoExport.js b/backend/src/routes/adminPhotoExport.js index 421ade3f..b6e1aa6c 100644 --- a/backend/src/routes/adminPhotoExport.js +++ b/backend/src/routes/adminPhotoExport.js @@ -11,7 +11,9 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { requireEventOwnership } = require('../middleware/ownership'); const { PhotoFilterBuilder } = require('../utils/photoFilterBuilder'); +const { getPagination } = require('../utils/routeHelpers'); const { PhotoExportService } = require('../services/photoExportService'); +const logger = require('../utils/logger'); const exportService = new PhotoExportService(); @@ -66,8 +68,7 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), re const sort = req.query.sort || 'date'; const order = req.query.order || 'desc'; - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 50; + const { page, limit } = getPagination(req, { limit: 50 }); // Build filtered query const filterBuilder = new PhotoFilterBuilder( @@ -124,7 +125,7 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), re } }); } catch (error) { - console.error('Filter photos error:', error); + logger.error('Filter photos error:', error); res.status(500).json({ error: 'Failed to filter photos' }); } }); @@ -146,7 +147,7 @@ router.get('/:eventId/filter-summary', adminAuth, requirePermission('photos.view data: summary }); } catch (error) { - console.error('Filter summary error:', error); + logger.error('Filter summary error:', error); res.status(500).json({ error: 'Failed to get filter summary' }); } }); @@ -206,7 +207,7 @@ router.post('/:eventId/export', adminAuth, requirePermission('photos.download'), res.send(result.content); } } catch (error) { - console.error('Export photos error:', error); + logger.error('Export photos error:', error); res.status(500).json({ error: error.message || 'Failed to export photos' }); } }); diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index 50496a52..9eeb022b 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -22,6 +22,8 @@ const downloadZipService = require('../services/downloadZipService'); const { findReplacementCandidate, replacePhoto } = require('../services/photoReplacementService'); const { requireEventOwnership } = require('../middleware/ownership'); const { getStorage } = require('../services/storage'); +const { errorResponse } = require('../utils/routeHelpers'); +const logger = require('../utils/logger'); const router = express.Router(); // Get storage path from environment or default @@ -31,7 +33,7 @@ const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '. // IMPORTANT: Using synchronous functions to prevent file corruption const storage = multer.diskStorage({ destination: (req, file, cb) => { - console.log('Multer destination called for file:', file.originalname); + logger.info('Multer destination called for file:', file.originalname); const { eventId } = req.params; // We'll validate the event exists in the route handler @@ -40,7 +42,7 @@ const storage = multer.diskStorage({ // Create directory synchronously require('fs').mkdirSync(tempPath, { recursive: true }); - console.log('Temp destination path:', tempPath); + logger.info('Temp destination path:', tempPath); // Store temp path for cleanup req.tempUploadPath = tempPath; @@ -48,10 +50,10 @@ const storage = multer.diskStorage({ cb(null, tempPath); }, filename: (req, file, cb) => { - console.log('Multer filename called for file:', file.originalname); + logger.info('Multer filename called for file:', file.originalname); // Use a simple temporary filename const tempName = `temp_${Date.now()}_${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`; - console.log('Temp filename:', tempName); + logger.info('Temp filename:', tempName); cb(null, tempName); } }); @@ -89,7 +91,7 @@ const resolveAllowedTypes = async (req, res, next) => { try { req.allowedMimeTypes = await getAllowedMimeTypes(); } catch (error) { - console.error('Failed to resolve allowed MIME types:', error); + logger.error('Failed to resolve allowed MIME types:', error); req.allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; } next(); @@ -111,7 +113,7 @@ const uploadTimeout = (timeout = 300000) => { // 5 minutes default return (req, res, next) => { // Set timeout for the request req.setTimeout(timeout, () => { - console.error('Upload request timed out'); + logger.error('Upload request timed out'); if (!res.headersSent) { res.status(408).json({ error: 'Upload request timed out' }); } @@ -119,7 +121,7 @@ const uploadTimeout = (timeout = 300000) => { // 5 minutes default // Set response timeout as well res.setTimeout(timeout, () => { - console.error('Upload response timed out'); + logger.error('Upload response timed out'); }); next(); @@ -133,13 +135,12 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r try { maxFilesPerUpload = await getMaxFilesPerUpload(); } catch (error) { - console.error('Failed to resolve max files per upload:', error); - return res.status(500).json({ error: 'Unable to determine upload limits' }); + return errorResponse(res, error, 500, 'Unable to determine upload limits'); } upload.array('photos', maxFilesPerUpload)(req, res, (err) => { if (err) { - console.error('Multer error:', err); + logger.error('Multer error:', err); if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'File too large. Maximum size is 10GB per file.' }); @@ -166,7 +167,7 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r try { await fs.rm(req.tempUploadPath, { recursive: true, force: true }); } catch (e) { - console.error('Failed to clean up temp upload directory:', e); + logger.error('Failed to clean up temp upload directory:', e); } }; res.on('finish', cleanupTempDir); @@ -177,16 +178,16 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r const { category_id, replace_by_name } = req.body; const replaceByName = replace_by_name === 'true' || replace_by_name === true; - console.log('Upload request received for event:', eventId); - console.log('Body:', req.body); - console.log('Files:', req.files ? req.files.length : 'none'); - console.log('File details:', req.files?.map(f => ({ name: f.originalname, size: f.size, mimetype: f.mimetype }))); - console.log('Category ID received:', category_id); + logger.info('Upload request received for event:', eventId); + logger.info('Body:', req.body); + logger.info('Files:', req.files ? req.files.length : 'none'); + logger.info('File details:', req.files?.map(f => ({ name: f.originalname, size: f.size, mimetype: f.mimetype }))); + logger.info('Category ID received:', category_id); // Verify event exists and admin has access const event = await db('events').where({ id: eventId }).first(); if (!event) { - console.error('Event not found:', eventId); + logger.error('Event not found:', eventId); return res.status(404).json({ error: 'Event not found' }); } @@ -213,8 +214,8 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r } if (!req.files || req.files.length === 0) { - console.error('No files in request. req.files:', req.files); - console.error('Request body keys:', Object.keys(req.body)); + logger.error('No files in request. req.files:', req.files); + logger.error('Request body keys:', Object.keys(req.body)); return res.status(400).json({ error: 'No files uploaded' }); } @@ -389,7 +390,7 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r category_id: parsedCategoryId, }); } catch (err) { - console.error(`Error queuing file ${file.originalname}:`, err); + logger.error(`Error queuing file ${file.originalname}:`, err); errors.push({ filename: file.originalname, error: err.message }); } } @@ -451,10 +452,9 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r // 202 Accepted — files stored, processing happens in background. res.status(202).json(response); } catch (error) { - console.error('Error uploading photos:', error); // Temp directory cleanup is handled by the response finish/close // listeners above, regardless of which exit path fires. - res.status(500).json({ error: 'Failed to upload photos' }); + errorResponse(res, error, 500, 'Failed to upload photos'); } }); @@ -523,8 +523,7 @@ router.get( ...summariseUpload(group.photos), }); } catch (error) { - console.error('Error reading upload status:', error); - res.status(500).json({ error: 'Failed to read upload status' }); + errorResponse(res, error, 500, 'Failed to read upload status'); } } ); @@ -575,7 +574,7 @@ router.get( return; } } catch (e) { - console.error('Upload stream poll error:', e); + logger.error('Upload stream poll error:', e); } }; @@ -622,8 +621,7 @@ router.post( }); res.json({ id: photo.id, status: 'pending' }); } catch (error) { - console.error('Error retrying photo processing:', error); - res.status(500).json({ error: 'Failed to retry photo processing' }); + errorResponse(res, error, 500, 'Failed to retry photo processing'); } } ); @@ -651,7 +649,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos. const originalKey = resolvePhotoStorageKey(event, photo); if (originalKey) await storage.delete(originalKey); } catch (error) { - console.error('Error deleting photo file:', error); + logger.error('Error deleting photo file:', error); } // photo.thumbnail_path is stored as the canonical storage key @@ -660,7 +658,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos. try { await storage.delete(photo.thumbnail_path); } catch (error) { - console.error('Error deleting thumbnail:', error); + logger.error('Error deleting thumbnail:', error); } } if (photo.hero_path) { @@ -700,8 +698,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos. downloadZipService.invalidate(parseInt(eventId)); res.json({ message: 'Photo deleted successfully' }); } catch (error) { - console.error('Error deleting photo:', error); - res.status(500).json({ error: 'Failed to delete photo' }); + errorResponse(res, error, 500, 'Failed to delete photo'); } }); @@ -763,8 +760,7 @@ router.patch('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.e photo: updatedPhoto }); } catch (error) { - console.error('Error updating photo:', error); - res.status(500).json({ error: 'Failed to update photo' }); + errorResponse(res, error, 500, 'Failed to update photo'); } }); @@ -797,7 +793,7 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, requirePermission('photos const originalKey = resolvePhotoStorageKey(event, photo); if (originalKey) await storage.delete(originalKey); } catch (error) { - console.error('Error deleting photo file:', error); + logger.error('Error deleting photo file:', error); } if (photo.thumbnail_path) { @@ -842,8 +838,7 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, requirePermission('photos downloadZipService.invalidate(parseInt(eventId)); res.json({ message: `${photos.length} photos deleted successfully` }); } catch (error) { - console.error('Error bulk deleting photos:', error); - res.status(500).json({ error: 'Failed to delete photos' }); + errorResponse(res, error, 500, 'Failed to delete photos'); } }); @@ -905,8 +900,7 @@ router.post('/:eventId/photos/bulk-update', adminAuth, requirePermission('photos res.json({ message: `${photoIds.length} photos updated successfully` }); } catch (error) { - console.error('Error bulk updating photos:', error); - res.status(500).json({ error: 'Failed to update photos' }); + errorResponse(res, error, 500, 'Failed to update photos'); } }); @@ -961,8 +955,7 @@ router.get('/:eventId/photos/:photoId/download', adminAuth, requirePermission('p }); res.sendFile(filePath); } catch (error) { - console.error('Error downloading photo:', error); - res.status(500).json({ error: 'Failed to download photo' }); + errorResponse(res, error, 500, 'Failed to download photo'); } }); @@ -1096,8 +1089,7 @@ router.get('/:eventId/photos', adminAuth, requirePermission('photos.view'), requ })) }); } catch (error) { - console.error('Error fetching photos:', error); - res.status(500).json({ error: 'Failed to fetch photos' }); + errorResponse(res, error, 500, 'Failed to fetch photos'); } }); @@ -1153,8 +1145,7 @@ router.get('/:eventId/photo/:photoId', adminAuth, requirePermission('photos.view } res.sendFile(path.resolve(filePath)); } catch (error) { - console.error('Error serving photo:', error); - res.status(500).json({ error: 'Failed to serve photo' }); + errorResponse(res, error, 500, 'Failed to serve photo'); } }); @@ -1168,7 +1159,7 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, requirePermission('photos. .first(); if (!photo) { - console.error(`Photo not found: ${photoId}, event ${eventId}`); + logger.error(`Photo not found: ${photoId}, event ${eventId}`); return res.status(404).json({ error: 'Photo not found' }); } @@ -1192,7 +1183,7 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, requirePermission('photos. const thumbnailPath = await ensureThumbnail(photo); if (!thumbnailPath) { - console.error(`Failed to generate thumbnail for photo ${photoId}`); + logger.error(`Failed to generate thumbnail for photo ${photoId}`); return res.status(404).json({ error: 'Thumbnail generation failed' }); } @@ -1209,10 +1200,9 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, requirePermission('photos. const stream = await storage.get(thumbnailPath); stream.pipe(res); } catch (error) { - console.error('Error serving thumbnail:', error); - console.error('Photo ID:', req.params.photoId); - console.error('Event ID:', req.params.eventId); - res.status(500).json({ error: 'Failed to serve thumbnail' }); + logger.error('Error serving thumbnail:', error); + logger.error('Photo ID:', req.params.photoId); + errorResponse(res, error, 500, 'Failed to serve thumbnail'); } }); @@ -1232,8 +1222,7 @@ router.get('/:eventId/debug', adminAuth, requirePermission('photos.view'), requi storagePath: getStoragePath() }); } catch (error) { - console.error('Error fetching admin photo debug data:', error); - res.status(500).json({ error: 'Failed to fetch photo debug data' }); + errorResponse(res, error, 500, 'Failed to fetch photo debug data'); } }); @@ -1275,8 +1264,7 @@ router.post('/:eventId/chunked-upload/init', adminAuth, requirePermission('photo res.json(result); } catch (error) { - console.error('Error initializing chunked upload:', error); - res.status(500).json({ error: 'Failed to initialize upload' }); + errorResponse(res, error, 500, 'Failed to initialize upload'); } }); @@ -1296,7 +1284,7 @@ router.post('/:eventId/chunked-upload/:uploadId/chunk/:chunkIndex', adminAuth, r res.json(result); } catch (error) { - console.error('Error uploading chunk:', error); + logger.error('Error uploading chunk:', error); res.status(500).json({ error: error.message || 'Failed to upload chunk' }); } }); @@ -1329,7 +1317,7 @@ router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, requirePer try { await fs.rm(mergedFile.tempDir, { recursive: true, force: true }); } catch (cleanupErr) { - console.warn('Failed to clean up temp directory:', cleanupErr.message); + logger.warn('Failed to clean up temp directory:', cleanupErr.message); } res.json({ @@ -1338,7 +1326,7 @@ router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, requirePer photos: uploadedPhotos }); } catch (error) { - console.error('Error completing chunked upload:', error); + logger.error('Error completing chunked upload:', error); res.status(500).json({ error: error.message || 'Failed to complete upload' }); } }); @@ -1356,8 +1344,7 @@ router.get('/:eventId/chunked-upload/:uploadId/status', adminAuth, requirePermis res.json(status); } catch (error) { - console.error('Error getting upload status:', error); - res.status(500).json({ error: 'Failed to get upload status' }); + errorResponse(res, error, 500, 'Failed to get upload status'); } }); @@ -1370,8 +1357,7 @@ router.delete('/:eventId/chunked-upload/:uploadId', adminAuth, requirePermission res.json({ success: true, message: 'Upload aborted' }); } catch (error) { - console.error('Error aborting upload:', error); - res.status(500).json({ error: 'Failed to abort upload' }); + errorResponse(res, error, 500, 'Failed to abort upload'); } }); diff --git a/backend/src/routes/adminRestore.js b/backend/src/routes/adminRestore.js index 0042608c..775b7c18 100644 --- a/backend/src/routes/adminRestore.js +++ b/backend/src/routes/adminRestore.js @@ -5,6 +5,7 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { body, query, validationResult } = require('express-validator'); const logger = require('../utils/logger'); +const { getPagination } = require('../utils/routeHelpers'); const { db } = require('../database/db'); const path = require('path'); const fs = require('fs').promises; @@ -48,7 +49,7 @@ function transformS3Config(body) { */ router.get('/status', requirePermission('backup.view'), async (req, res) => { try { - const limit = parseInt(req.query.limit) || 10; + const { limit } = getPagination(req, { limit: 10 }); const history = await restoreService.getRestoreHistory(limit); const status = { diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index d45445c4..159c8c8a 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -22,6 +22,8 @@ const { sanitizeCss } = require('../utils/cssSanitizer'); const { upsertAppSetting } = require('../utils/appSettings'); const { clearShareLinkSettingsCache } = require('../services/shareLinkService'); const { resetSecurityConfigCache } = require('../utils/authSecurity'); +const { errorResponse } = require('../utils/routeHelpers'); +const logger = require('../utils/logger'); const router = express.Router(); const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD } = require('../services/uploadSettings'); const watermarkService = require('../services/watermarkService'); @@ -158,8 +160,7 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) res.json(settingsObject); } catch (error) { - console.error('Settings fetch error:', error); - res.status(500).json({ error: 'Failed to fetch settings' }); + errorResponse(res, error, 500, 'Failed to fetch settings'); } }); @@ -200,8 +201,7 @@ router.get('/customer-surface', adminAuth, requirePermission('settings.view'), a res.json(settings); } catch (error) { - console.error('Customer surface settings fetch error:', error); - res.status(500).json({ error: 'Failed to fetch customer surface settings' }); + errorResponse(res, error, 500, 'Failed to fetch customer surface settings'); } }); @@ -231,8 +231,7 @@ router.put('/customer-surface', adminAuth, requirePermission('settings.edit'), a res.json({ message: 'Customer surface settings updated', updated: updates.map((u) => u.setting_key) }); } catch (error) { - console.error('Customer surface settings save error:', error); - res.status(500).json({ error: 'Failed to save customer surface settings' }); + errorResponse(res, error, 500, 'Failed to save customer surface settings'); } }); @@ -295,8 +294,7 @@ router.put('/accounting', adminAuth, requirePermission('settings.edit'), async ( } res.json({ message: 'Accounting settings updated', updated: updates.map((u) => u.setting_key) }); } catch (error) { - console.error('Accounting settings save error:', error); - res.status(500).json({ error: 'Failed to save accounting settings' }); + errorResponse(res, error, 500, 'Failed to save accounting settings'); } }); @@ -360,8 +358,7 @@ router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (r require('../utils/slideshowGlobals').invalidateSlideshowGlobals(); res.json({ message: 'Slideshow settings updated', updated: updates.map((u) => u.setting_key) }); } catch (error) { - console.error('Slideshow settings save error:', error); - res.status(500).json({ error: 'Failed to save slideshow settings' }); + errorResponse(res, error, 500, 'Failed to save slideshow settings'); } }); @@ -413,8 +410,7 @@ router.get('/:type', adminAuth, requirePermission('settings.view'), async (req, res.json(settingsObject); } catch (error) { - console.error('Settings fetch error:', error); - res.status(500).json({ error: 'Failed to fetch settings' }); + errorResponse(res, error, 500, 'Failed to fetch settings'); } }); @@ -434,8 +430,7 @@ router.get('/password/complexity', adminAuth, requirePermission('settings.view') config }); } catch (error) { - console.error('Password complexity settings fetch error:', error); - res.status(500).json({ error: 'Failed to fetch password complexity settings' }); + errorResponse(res, error, 500, 'Failed to fetch password complexity settings'); } }); @@ -577,9 +572,9 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re const faviconPath = path.join(getStoragePath(), relativePath); try { await fs.unlink(faviconPath); - console.log('Deleted favicon file:', faviconPath); + logger.info('Deleted favicon file:', faviconPath); } catch (err) { - console.error('Error deleting favicon file:', err); + logger.error('Error deleting favicon file:', err); } } } @@ -608,9 +603,9 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re const logoPath = path.join(getStoragePath(), relativePath); try { await fs.unlink(logoPath); - console.log('Deleted logo file:', logoPath); + logger.info('Deleted logo file:', logoPath); } catch (err) { - console.error('Error deleting logo file:', err); + logger.error('Error deleting logo file:', err); } } } @@ -656,20 +651,20 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re if (currentSettings && currentSettings.enabled) { // Start background regeneration of all watermarks - console.log('Watermark settings changed, starting background regeneration'); + logger.info('Watermark settings changed, starting background regeneration'); watermarkGeneratorService.regenerateAll() .then(result => { - console.log(`Watermark regeneration completed: ${result.success}/${result.total} successful`); + logger.info(`Watermark regeneration completed: ${result.success}/${result.total} successful`); }) .catch(err => { - console.error('Watermark regeneration failed:', err); + logger.error('Watermark regeneration failed:', err); }); watermarkRegenerationStarted = true; } else { // Watermarking was disabled, clear all pre-generated watermarks - console.log('Watermarking disabled, clearing pre-generated watermarks'); + logger.info('Watermarking disabled, clearing pre-generated watermarks'); watermarkGeneratorService.clearAllWatermarks() - .catch(err => console.error('Failed to clear watermarks:', err)); + .catch(err => logger.error('Failed to clear watermarks:', err)); } } @@ -678,8 +673,7 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re watermarkRegenerationStarted }); } catch (error) { - console.error('Branding update error:', error); - res.status(500).json({ error: 'Failed to update branding settings' }); + errorResponse(res, error, 500, 'Failed to update branding settings'); } }); @@ -711,7 +705,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl } await fs.unlink(oldPath); } catch (error) { - console.error('Failed to delete old logo:', error); + logger.error('Failed to delete old logo:', error); } } @@ -751,8 +745,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl logoUrl: publicPath }); } catch (error) { - console.error('Logo upload error:', error); - res.status(500).json({ error: 'Failed to upload logo' }); + errorResponse(res, error, 500, 'Failed to upload logo'); } }); @@ -772,7 +765,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req if (p.startsWith('"')) p = JSON.parse(p); await fs.unlink(p); } catch (error) { - console.error('Failed to delete logo file:', error); + logger.error('Failed to delete logo file:', error); } } await db('app_settings') @@ -781,8 +774,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req res.json({ message: 'Logo removed' }); } catch (error) { - console.error('Logo delete error:', error); - res.status(500).json({ error: 'Failed to remove logo' }); + errorResponse(res, error, 500, 'Failed to remove logo'); } }); @@ -812,7 +804,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e try { await fs.unlink(oldPath); } catch (error) { - console.error('Failed to delete old watermark logo:', error); + logger.error('Failed to delete old watermark logo:', error); } } } @@ -854,13 +846,13 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e let watermarkRegenerationStarted = false; if (currentSettings && currentSettings.enabled) { - console.log('Watermark logo changed, starting background regeneration'); + logger.info('Watermark logo changed, starting background regeneration'); watermarkGeneratorService.regenerateAll() .then(result => { - console.log(`Watermark regeneration completed: ${result.success}/${result.total} successful`); + logger.info(`Watermark regeneration completed: ${result.success}/${result.total} successful`); }) .catch(err => { - console.error('Watermark regeneration failed:', err); + logger.error('Watermark regeneration failed:', err); }); watermarkRegenerationStarted = true; } @@ -871,8 +863,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e watermarkRegenerationStarted }); } catch (error) { - console.error('Watermark logo upload error:', error); - res.status(500).json({ error: 'Failed to upload watermark logo' }); + errorResponse(res, error, 500, 'Failed to upload watermark logo'); } }); @@ -908,8 +899,7 @@ router.put('/theme', adminAuth, requirePermission('settings.edit'), async (req, res.json({ message: 'Theme settings updated successfully' }); } catch (error) { - console.error('Theme update error:', error); - res.status(500).json({ error: 'Failed to update theme settings' }); + errorResponse(res, error, 500, 'Failed to update theme settings'); } }); @@ -1005,7 +995,7 @@ router.put('/general', adminAuth, requirePermission('settings.edit'), async (req require('../services/downloadFilenameService').clearCache(); require('../services/downloadZipService').invalidateAll(); } catch (e) { - console.warn('Failed to invalidate download caches after filename setting change:', e.message); + logger.warn('Failed to invalidate download caches after filename setting change:', e.message); } } @@ -1020,8 +1010,7 @@ router.put('/general', adminAuth, requirePermission('settings.edit'), async (req res.json({ message: 'General settings updated successfully' }); } catch (error) { - console.error('General settings update error:', error); - res.status(500).json({ error: 'Failed to update general settings' }); + errorResponse(res, error, 500, 'Failed to update general settings'); } }); @@ -1059,8 +1048,7 @@ router.put('/security', adminAuth, requirePermission('settings.edit'), async (re res.json({ message: 'Security settings updated successfully' }); } catch (error) { - console.error('Security settings update error:', error); - res.status(500).json({ error: 'Failed to update security settings' }); + errorResponse(res, error, 500, 'Failed to update security settings'); } }); @@ -1115,8 +1103,7 @@ router.put('/analytics', adminAuth, requirePermission('settings.edit'), async (r res.json({ message: 'Analytics settings updated successfully' }); } catch (error) { - console.error('Analytics settings update error:', error); - res.status(500).json({ error: 'Failed to update analytics settings' }); + errorResponse(res, error, 500, 'Failed to update analytics settings'); } }); @@ -1179,8 +1166,7 @@ router.put('/seo', adminAuth, requirePermission('settings.edit'), async (req, re res.json({ message: 'SEO settings updated successfully' }); } catch (error) { - console.error('SEO settings update error:', error); - res.status(500).json({ error: 'Failed to update SEO settings' }); + errorResponse(res, error, 500, 'Failed to update SEO settings'); } }); @@ -1216,7 +1202,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async const stats = await fs.stat(fullArchivePath); archiveStorage += stats.size; } catch (error) { - console.error('Archive file not found:', archive.archive_path, error.message); + logger.error('Archive file not found:', archive.archive_path, error.message); } } } @@ -1234,7 +1220,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async rawDiskFree = Number(diskStats.bsize) * Number(diskStats.bfree); rawDiskAvailable = Number(diskStats.bsize) * Number(diskStats.bavail); } catch (diskError) { - console.error('Disk stats error:', diskError.message); + logger.error('Disk stats error:', diskError.message); } const clampDiskValue = (value) => { @@ -1333,7 +1319,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async } }); } catch (error) { - console.error('Storage settings read error:', error.message); + logger.error('Storage settings read error:', error.message); } const capacityOverrideEnv = parseEnvOverride('STORAGE_CAPACITY_OVERRIDE_BYTES', 'STORAGE_CAPACITY_OVERRIDE_GB'); @@ -1407,8 +1393,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async disk_override_source: overrideSource }); } catch (error) { - console.error('Storage info error:', error); - res.status(500).json({ error: 'Failed to fetch storage information' }); + errorResponse(res, error, 500, 'Failed to fetch storage information'); } }); @@ -1445,8 +1430,7 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp res.json({ faviconUrl }); } catch (error) { - console.error('Error uploading favicon:', error); - res.status(500).json({ error: 'Failed to upload favicon' }); + errorResponse(res, error, 500, 'Failed to upload favicon'); } }); @@ -1509,8 +1493,7 @@ router.put('/security/rate-limit', adminAuth, requirePermission('settings.edit') res.json({ message: 'Rate limit settings updated successfully' }); } catch (error) { - console.error('Rate limit settings update error:', error); - res.status(500).json({ error: 'Failed to update rate limit settings' }); + errorResponse(res, error, 500, 'Failed to update rate limit settings'); } }); @@ -1530,8 +1513,7 @@ router.get('/public-site/default', adminAuth, requirePermission('settings.view') } }); } catch (error) { - console.error('Failed to load public site defaults:', error); - res.status(500).json({ error: 'Failed to load defaults' }); + errorResponse(res, error, 500, 'Failed to load defaults'); } }); @@ -1584,8 +1566,7 @@ router.post('/public-site/reset', adminAuth, requirePermission('settings.edit'), branding: defaults.branding }); } catch (error) { - console.error('Failed to reset public site template:', error); - res.status(500).json({ error: 'Failed to reset template' }); + errorResponse(res, error, 500, 'Failed to reset template'); } }); diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index 95aa9f26..029df297 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -29,7 +29,7 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req const packageJson = JSON.parse(packageContent); backendVersion = packageJson.version || '1.0.0'; } catch (err) { - console.error('Could not read package.json:', err); + logger.error('Could not read package.json:', err); } const channel = getCurrentChannel(backendVersion); @@ -42,7 +42,7 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req channel: channel }); } catch (error) { - console.error('Error fetching version:', error); + logger.error('Error fetching version:', error); res.status(500).json({ error: 'Failed to fetch version information' }); } }); @@ -231,7 +231,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req, `, [dbName]); dbSize = result.rows[0]?.size || 0; } catch (error) { - console.error('Error getting PostgreSQL database size:', error); + logger.error('Error getting PostgreSQL database size:', error); } } else { // SQLite - check file size @@ -240,7 +240,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req, const stats = await fs.stat(dbPath); dbSize = stats.size; } catch (error) { - console.error('Error getting SQLite database size:', error); + logger.error('Error getting SQLite database size:', error); } } @@ -285,7 +285,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req, const stats = await fs.stat(fullArchivePath); archiveStorage += stats.size; } catch (error) { - console.error('Archive file not found:', archive.archive_path); + logger.error('Archive file not found:', archive.archive_path); } } } @@ -345,7 +345,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req, res.json(status); } catch (error) { - console.error('Error fetching system status:', error); + logger.error('Error fetching system status:', error); res.status(500).json({ error: 'Failed to fetch system status' }); } }); @@ -407,7 +407,7 @@ router.get('/database', adminAuth, requirePermission('settings.view'), async (re timestamp: new Date() }); } catch (error) { - console.error('Error fetching database info:', error); + logger.error('Error fetching database info:', error); res.status(500).json({ error: 'Failed to fetch database information' }); } }); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index e99b5861..185b02e2 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -15,6 +15,7 @@ const { const { endSession } = require('../middleware/sessionTimeout'); const { revokeToken } = require('../utils/tokenRevocation'); const logger = require('../utils/logger'); +const { errorResponse } = require('../utils/routeHelpers'); const { setAdminAuthCookie, clearAdminAuthCookie, @@ -132,8 +133,7 @@ router.post('/admin/login', [ } }); } catch (error) { - logger.error('Login error:', error); - res.status(500).json({ error: 'Login failed' }); + errorResponse(res, error, 500, 'Login failed'); } }); @@ -175,8 +175,7 @@ router.post('/logout', async (req, res) => { res.json({ message: 'Logged out successfully' }); } catch (error) { - logger.error('Logout error:', error); - res.status(500).json({ error: 'Logout failed' }); + errorResponse(res, error, 500, 'Logout failed'); } }); @@ -288,8 +287,7 @@ router.post('/gallery/verify', [ } }); } catch (error) { - logger.error('Gallery verification error:', error); - res.status(500).json({ error: 'Verification failed' }); + errorResponse(res, error, 500, 'Verification failed'); } }); @@ -364,8 +362,7 @@ router.post('/gallery/:slug/client-login', [ accessLevel: 'client' }); } catch (error) { - logger.error('Client login error:', error); - res.status(500).json({ error: 'Authentication failed' }); + errorResponse(res, error, 500, 'Authentication failed'); } }); @@ -451,8 +448,7 @@ router.post('/gallery/share-login', [ } }); } catch (error) { - logger.error('Share link authentication error:', error); - res.status(500).json({ error: 'Share link login failed' }); + errorResponse(res, error, 500, 'Share link login failed'); } }); @@ -467,8 +463,7 @@ router.post('/gallery/logout', async (req, res) => { clearGalleryAuthCookies(res, slug); res.json({ message: 'Logged out successfully' }); } catch (error) { - logger.error('Gallery logout error:', error); - res.status(500).json({ error: 'Logout failed' }); + errorResponse(res, error, 500, 'Logout failed'); } }); @@ -682,8 +677,7 @@ router.post('/admin/change-password', [ score: passwordValidation.score }); } catch (error) { - logger.error('Password change error:', error); - res.status(500).json({ error: 'Failed to change password' }); + errorResponse(res, error, 500, 'Failed to change password'); } }); diff --git a/backend/src/routes/customer.js b/backend/src/routes/customer.js index d2b827d9..68f210de 100644 --- a/backend/src/routes/customer.js +++ b/backend/src/routes/customer.js @@ -19,6 +19,7 @@ const { db, logActivity } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const { getBcryptRounds } = require('../utils/passwordValidation'); const logger = require('../utils/logger'); +const { errorResponse } = require('../utils/routeHelpers'); const { getClientIp } = require('../utils/requestIp'); const { customerAuth } = require('../middleware/customerAuth'); const { setGalleryAuthCookies } = require('../utils/tokenUtils'); @@ -117,8 +118,7 @@ router.get('/events', customerAuth, async (req, res) => { })), }); } catch (error) { - logger.error('Customer event list error:', error); - res.status(500).json({ error: 'Failed to load events' }); + errorResponse(res, error, 500, 'Failed to load events'); } }); @@ -225,8 +225,7 @@ router.get('/events/:slug/access-token', [ }, }); } catch (error) { - logger.error('Customer access-token exchange error:', error); - res.status(500).json({ error: 'Failed to issue access token' }); + errorResponse(res, error, 500, 'Failed to issue access token'); } }); @@ -248,8 +247,7 @@ router.get('/profile', customerAuth, async (req, res) => { } res.json({ profile: shapeProfile(row) }); } catch (error) { - logger.error('Customer profile read error:', error); - res.status(500).json({ error: 'Failed to load profile' }); + errorResponse(res, error, 500, 'Failed to load profile'); } }); @@ -316,8 +314,7 @@ router.put('/profile', [ res.json({ profile: shapeProfile(row) }); } catch (error) { - logger.error('Customer profile update error:', error); - res.status(500).json({ error: 'Failed to update profile' }); + errorResponse(res, error, 500, 'Failed to update profile'); } }); @@ -376,8 +373,7 @@ router.post('/profile/password', [ res.json({ message: 'Password updated' }); } catch (error) { - logger.error('Customer password change error:', error); - res.status(500).json({ error: 'Failed to change password' }); + errorResponse(res, error, 500, 'Failed to change password'); } }); @@ -457,8 +453,7 @@ router.get('/quotes', customerAuth, async (req, res) => { })), }); } catch (error) { - logger.error('Customer quotes list error:', error); - res.status(500).json({ error: 'Failed to load quotes' }); + errorResponse(res, error, 500, 'Failed to load quotes'); } }); @@ -545,8 +540,7 @@ router.get('/invoices', customerAuth, async (req, res) => { })), }); } catch (error) { - logger.error('Customer invoice list error:', error); - res.status(500).json({ error: 'Failed to load invoices' }); + errorResponse(res, error, 500, 'Failed to load invoices'); } }); @@ -582,8 +576,7 @@ router.get('/quotes/:id/pdf', customerAuth, async (req, res) => { res.set('Content-Disposition', `inline; filename="${filename}"`); res.send(buf); } catch (error) { - logger.error('Customer quote PDF error:', error); - res.status(500).json({ error: 'Failed to render quote PDF' }); + errorResponse(res, error, 500, 'Failed to render quote PDF'); } }); @@ -614,8 +607,7 @@ router.get('/invoices/:id/pdf', customerAuth, async (req, res) => { res.set('Content-Disposition', `inline; filename="${filename}"`); res.send(buf); } catch (error) { - logger.error('Customer invoice PDF error:', error); - res.status(500).json({ error: 'Failed to render invoice PDF' }); + errorResponse(res, error, 500, 'Failed to render invoice PDF'); } }); @@ -679,8 +671,7 @@ router.get('/contracts', customerAuth, async (req, res) => { })), }); } catch (error) { - logger.error('Customer contracts list error:', error); - res.status(500).json({ error: 'Failed to load contracts' }); + errorResponse(res, error, 500, 'Failed to load contracts'); } }); @@ -717,8 +708,7 @@ router.get('/contracts/:id/pdf', customerAuth, async (req, res) => { res.set('Content-Disposition', `inline; filename="${path.basename(filePath)}"`); fs.createReadStream(filePath).pipe(res); } catch (error) { - logger.error('Customer contract PDF error:', error); - res.status(500).json({ error: 'Failed to render contract PDF' }); + errorResponse(res, error, 500, 'Failed to render contract PDF'); } }); diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index 39b6e800..18ac1e47 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -14,6 +14,7 @@ const { buildShareLinkVariants } = require('../services/shareLinkService'); const { parseBooleanInput, parseStringInput } = require('../utils/parsers'); const eventTypeService = require('../services/eventTypeService'); const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('../utils/emailNormalization'); +const logger = require('../utils/logger'); // Use parseStringInput from shared parsers for customer data extraction const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name); @@ -235,7 +236,7 @@ router.post('/', adminAuth, [ }); } } catch (waError) { - console.warn('Failed to queue WhatsApp notification on create', waError.message); + logger.warn('Failed to queue WhatsApp notification on create', waError.message); } } @@ -271,7 +272,7 @@ router.post('/', adminAuth, [ customer_email: customerEmail }); } catch (error) { - console.error(error); + logger.error(error); res.status(500).json({ error: 'Failed to create event' }); } }); diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 1187185d..e24603a8 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -14,7 +14,7 @@ const secureImageService = require('../services/secureImageService'); const logger = require('../utils/logger'); const { resolvePhotoFilePath } = require('../services/photoResolver'); const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService'); -const { handleAsync } = require('../utils/routeHelpers'); +const { handleAsync, errorResponse } = require('../utils/routeHelpers'); const { NotFoundError } = require('../utils/errors'); const { ensureThumbnail, ensureHeroImage, ensurePreviewImage, withLocalCopy } = require('../services/imageProcessor'); const downloadZipService = require('../services/downloadZipService'); @@ -213,8 +213,7 @@ router.get('/:slug/info', async (req, res) => { promo_markdown: event.promo_markdown || null }); } catch (error) { - console.error('Error fetching gallery info:', error); - res.status(500).json({ error: 'Failed to fetch gallery info' }); + errorResponse(res, error, 500, 'Failed to fetch gallery info'); } }); @@ -739,8 +738,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) }) }); } catch (error) { - console.error('Error fetching photos:', error); - res.status(500).json({ error: 'Failed to fetch photos' }); + errorResponse(res, error, 500, 'Failed to fetch photos'); } }); @@ -772,8 +770,7 @@ router.patch('/:slug/photos/:photoId/visibility', verifyGalleryAccess, async (re res.json({ message: 'Photo visibility updated', visibility }); } catch (error) { - logger.error('Error updating photo visibility:', error); - res.status(500).json({ error: 'Failed to update photo visibility' }); + errorResponse(res, error, 500, 'Failed to update photo visibility'); } }); @@ -801,8 +798,7 @@ router.patch('/:slug/photos/visibility/bulk', verifyGalleryAccess, async (req, r res.json({ message: `${count} photos updated`, visibility }); } catch (error) { - logger.error('Error bulk updating photo visibility:', error); - res.status(500).json({ error: 'Failed to update photo visibility' }); + errorResponse(res, error, 500, 'Failed to update photo visibility'); } }); @@ -916,13 +912,7 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, denySlideshowToken, }); } } catch (error) { - logger.error('Unexpected error processing gallery download', { - slug: req.params.slug, - photoId: req.params.photoId, - eventId: req.event?.id, - error: error.message, - }); - res.status(500).json({ error: 'Failed to download photo' }); + errorResponse(res, error, 500, 'Failed to download photo'); } }); @@ -1093,12 +1083,7 @@ router.get('/:slug/download-all', verifyGalleryAccess, denySlideshowToken, async action: 'download_all' }); } catch (error) { - logger.error('Error creating bulk gallery download', { - slug: req.params.slug, - eventId: req.event?.id, - error: error.message, - }); - res.status(500).json({ error: 'Failed to create download archive' }); + errorResponse(res, error, 500, 'Failed to create download archive'); } }); @@ -1215,12 +1200,7 @@ router.post('/:slug/download-selected', verifyGalleryAccess, denySlideshowToken, action: 'download_selected' }); } catch (error) { - logger.error('Error in download-selected:', { - slug: req.params.slug, - eventId: req.event?.id, - error: error.message, - }); - res.status(500).json({ error: 'Failed to download selected photos' }); + errorResponse(res, error, 500, 'Failed to download selected photos'); } }); @@ -1452,13 +1432,7 @@ router.get('/:slug/photo/:photoId', } } } catch (error) { - logger.error('Error serving photo:', { - error: error.message, - stack: error.stack, - photoId: req.params.photoId, - eventId: req.event?.id - }); - res.status(500).json({ error: 'Failed to serve photo' }); + errorResponse(res, error, 500, 'Failed to serve photo'); } } ); @@ -1549,12 +1523,7 @@ router.get('/:slug/thumbnail/:photoId', stream.pipe(res); } } catch (error) { - logger.error('Error serving thumbnail:', { - error: error.message, - photoId: req.params.photoId, - eventId: req.event?.id - }); - res.status(500).json({ error: 'Failed to serve thumbnail' }); + errorResponse(res, error, 500, 'Failed to serve thumbnail'); } } ); @@ -1763,8 +1732,7 @@ router.get('/:slug/feedback-settings', verifyGalleryAccess, async (req, res) => identity_mode: settings.identity_mode || 'simple' }); } catch (error) { - console.error('Error fetching feedback settings:', error); - res.status(500).json({ error: 'Failed to fetch feedback settings' }); + errorResponse(res, error, 500, 'Failed to fetch feedback settings'); } }); @@ -1826,8 +1794,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async ( fs.mkdirSync(tempUploadDir, { recursive: true, mode: 0o755 }); logger.info('Created temp upload directory:', tempUploadDir); } catch (mkdirErr) { - logger.error('Failed to create temp upload directory:', mkdirErr); - return res.status(500).json({ error: 'Server configuration error: unable to create upload directory' }); + return errorResponse(res, mkdirErr, 500, 'Server configuration error: unable to create upload directory'); } } @@ -1877,7 +1844,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async ( // Handle upload upload(req, res, async (err) => { if (err) { - console.error('Upload error:', err); + logger.error('Upload error:', err); return res.status(400).json({ error: err.message }); } @@ -1911,13 +1878,11 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async ( errors: result.errors.length > 0 ? result.errors : undefined, }); } catch (processError) { - console.error('Photo processing error:', processError); - res.status(500).json({ error: 'Failed to process photos' }); + errorResponse(res, processError, 500, 'Failed to process photos'); } }); } catch (error) { - console.error('Upload route error:', error); - res.status(500).json({ error: 'Failed to upload photos' }); + errorResponse(res, error, 500, 'Failed to upload photos'); } }); @@ -1955,7 +1920,7 @@ router.get('/:slug/css-template', async (req, res) => { res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour cache res.send(template.css_content); } catch (error) { - console.error('Get CSS template error:', error); + logger.error('Get CSS template error:', error); res.status(500).send('/* Error loading template */'); } }); diff --git a/backend/src/routes/protectedImages.js b/backend/src/routes/protectedImages.js index 15ddbb1b..a71af7f9 100644 --- a/backend/src/routes/protectedImages.js +++ b/backend/src/routes/protectedImages.js @@ -8,6 +8,7 @@ const { getStorage } = require('../services/storage'); const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('../services/photoResolver'); const { withLocalCopy } = require('../services/imageProcessor'); const crypto = require('crypto'); +const logger = require('../utils/logger'); const router = express.Router(); @@ -164,7 +165,7 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, async (req, res) = res.send(finalImage); } catch (error) { - console.error('Error serving protected image:', error); + logger.error('Error serving protected image:', error); res.status(500).json({ error: 'Failed to serve image' }); } }); @@ -208,7 +209,7 @@ router.post('/:slug/photo/:photoId/generate-secure-token', verifyGalleryAccess, }); } catch (error) { - console.error('Error generating secure token:', error); + logger.error('Error generating secure token:', error); res.status(500).json({ error: 'Failed to generate token' }); } }); @@ -242,7 +243,7 @@ router.post('/:slug/photo/:photoId/generate-url', verifyGalleryAccess, async (re }); } catch (error) { - console.error('Error generating signed URL:', error); + logger.error('Error generating signed URL:', error); res.status(500).json({ error: 'Failed to generate URL' }); } }); @@ -304,7 +305,7 @@ router.get('/:slug/photo/:photoId/signed/:token', async (req, res) => { res.send(imageBuffer); } catch (error) { - console.error('Error serving signed image:', error); + logger.error('Error serving signed image:', error); res.status(500).json({ error: 'Failed to serve image' }); } }); diff --git a/backend/src/routes/publicCMS.js b/backend/src/routes/publicCMS.js index 2215c096..b0943a48 100644 --- a/backend/src/routes/publicCMS.js +++ b/backend/src/routes/publicCMS.js @@ -1,5 +1,6 @@ const express = require('express'); const { db } = require('../database/db'); +const logger = require('../utils/logger'); const router = express.Router(); // Get public CMS page @@ -41,7 +42,7 @@ router.get('/pages/:slug', async (req, res) => { updated_at: page.updated_at }); } catch (error) { - console.error('Error fetching public CMS page:', error); + logger.error('Error fetching public CMS page:', error); res.status(500).json({ error: 'Failed to fetch page' }); } }); diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index 5ca6f484..a66c92b3 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -1,5 +1,6 @@ const express = require('express'); const { db, withRetry } = require('../database/db'); +const logger = require('../utils/logger'); const router = express.Router(); // Get public settings (branding and theme) @@ -205,7 +206,7 @@ router.get('/', async (req, res) => { res.json(publicSettings); } catch (error) { - console.error('Public settings fetch error:', error); + logger.error('Public settings fetch error:', error); res.status(500).json({ error: 'Failed to fetch settings' }); } }); diff --git a/backend/src/routes/secureImages.js b/backend/src/routes/secureImages.js index 79093eee..d47ab7c1 100644 --- a/backend/src/routes/secureImages.js +++ b/backend/src/routes/secureImages.js @@ -412,7 +412,7 @@ async function getRecentAccessStats() { return acc; }, {}); } catch (error) { - console.error('Error getting recent access stats:', error); + logger.error('Error getting recent access stats:', error); return {}; } } @@ -440,7 +440,7 @@ async function getSuspiciousActivityStats() { uniqueIPs: parseInt(uniqueIPs.count) }; } catch (error) { - console.error('Error getting suspicious activity stats:', error); + logger.error('Error getting suspicious activity stats:', error); return { suspiciousEvents: 0, uniqueIPs: 0 }; } } diff --git a/backend/src/services/photoProcessor.js b/backend/src/services/photoProcessor.js index 9b64d2e9..360592c6 100644 --- a/backend/src/services/photoProcessor.js +++ b/backend/src/services/photoProcessor.js @@ -11,14 +11,14 @@ const logger = require('../utils/logger'); function normalizeFiles(files) { // Handle null, undefined, or falsy values if (!files) { - console.log('[normalizeFiles] No files provided'); + logger.info('[normalizeFiles] No files provided'); return []; } // Handle arrays if (Array.isArray(files)) { const validFiles = files.filter(Boolean); - console.log(`[normalizeFiles] Normalized ${validFiles.length} files from array`); + logger.info(`[normalizeFiles] Normalized ${validFiles.length} files from array`); return validFiles; } @@ -26,11 +26,11 @@ function normalizeFiles(files) { try { if (typeof files === 'object' && typeof files[Symbol.iterator] === 'function') { const validFiles = Array.from(files).filter(Boolean); - console.log(`[normalizeFiles] Normalized ${validFiles.length} files from iterable`); + logger.info(`[normalizeFiles] Normalized ${validFiles.length} files from iterable`); return validFiles; } } catch (err) { - console.warn('[normalizeFiles] Failed to iterate files object:', err.message); + logger.warn('[normalizeFiles] Failed to iterate files object:', err.message); } // Handle plain objects (multer fieldname mapping) @@ -39,16 +39,16 @@ function normalizeFiles(files) { const validFiles = Object.values(files) .flatMap((value) => (Array.isArray(value) ? value : [value])) .filter(Boolean); - console.log(`[normalizeFiles] Normalized ${validFiles.length} files from object`); + logger.info(`[normalizeFiles] Normalized ${validFiles.length} files from object`); return validFiles; } catch (err) { - console.warn('[normalizeFiles] Failed to process files object:', err.message); + logger.warn('[normalizeFiles] Failed to process files object:', err.message); return []; } } // Unexpected type - console.warn('[normalizeFiles] Unexpected files type:', typeof files); + logger.warn('[normalizeFiles] Unexpected files type:', typeof files); return []; } @@ -115,7 +115,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ try { await fs.access(tempPath); } catch (accessErr) { - console.error(`Temp file not accessible: ${tempPath}`, { + logger.error(`Temp file not accessible: ${tempPath}`, { originalname: file?.originalname, error: accessErr.message }); @@ -156,7 +156,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ }; } } catch (metadataError) { - console.warn(`Could not extract image dimensions for ${file.originalname}:`, metadataError.message); + logger.warn(`Could not extract image dimensions for ${file.originalname}:`, metadataError.message); } } @@ -167,14 +167,14 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ contentType: file.mimetype, }); } catch (uploadErr) { - console.error(`Failed to upload ${file.originalname} → ${finalKey}:`, uploadErr); + logger.error(`Failed to upload ${file.originalname} → ${finalKey}:`, uploadErr); throw new Error(`Failed to upload to storage: ${uploadErr.message}`); } finally { try { await fs.unlink(tempPath); } catch (unlinkErr) { if (unlinkErr?.code !== 'ENOENT') { - console.warn(`Failed to clean up temp upload ${tempPath}:`, { + logger.warn(`Failed to clean up temp upload ${tempPath}:`, { error: unlinkErr.message, code: unlinkErr.code }); @@ -262,9 +262,9 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ type: photoType }); - console.log(`Successfully processed file ${file.originalname} (ID: ${photoId})`); + logger.info(`Successfully processed file ${file.originalname} (ID: ${photoId})`); } catch (error) { - console.error(`Error processing file ${file.originalname}:`, { + logger.error(`Error processing file ${file.originalname}:`, { error: error.message, stack: error.stack, originalname: file.originalname, @@ -277,7 +277,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ try { await trx.rollback(); } catch (rollbackErr) { - console.error('Failed to rollback transaction:', rollbackErr); + logger.error('Failed to rollback transaction:', rollbackErr); } } diff --git a/backend/src/services/recaptcha.js b/backend/src/services/recaptcha.js index b7891fec..ab91382e 100644 --- a/backend/src/services/recaptcha.js +++ b/backend/src/services/recaptcha.js @@ -1,5 +1,6 @@ const axios = require('axios'); const { db } = require('../database/db'); +const logger = require('../utils/logger'); async function verifyRecaptcha(token) { // Check if reCAPTCHA is enabled @@ -32,7 +33,7 @@ async function verifyRecaptcha(token) { // If no secret key configured, fail closed if (!secretKey) { - console.warn('reCAPTCHA enabled but no secret key configured — blocking request'); + logger.warn('reCAPTCHA enabled but no secret key configured — blocking request'); return false; } @@ -50,7 +51,7 @@ async function verifyRecaptcha(token) { return response.data.success === true; } catch (error) { - console.error('reCAPTCHA verification error:', error); + logger.error('reCAPTCHA verification error:', error); return false; } } diff --git a/backend/src/services/secureImageService.js b/backend/src/services/secureImageService.js index 67338761..fc2e9551 100644 --- a/backend/src/services/secureImageService.js +++ b/backend/src/services/secureImageService.js @@ -4,6 +4,7 @@ const { db } = require('../database/db'); const watermarkService = require('./watermarkService'); const path = require('path'); const fs = require('fs').promises; +const logger = require('../utils/logger'); class SecureImageService { constructor() { @@ -268,7 +269,7 @@ class SecureImageService { return buffer; } catch (error) { - console.error('Error processing protected image:', error); + logger.error('Error processing protected image:', error); // Return original on error return await fs.readFile(imagePath); } @@ -346,7 +347,7 @@ class SecureImageService { } } catch (error) { - console.error('Error logging image access:', error); + logger.error('Error logging image access:', error); } } @@ -386,7 +387,7 @@ class SecureImageService { } } catch (error) { - console.error('Error checking for rapid access:', error); + logger.error('Error checking for rapid access:', error); } } @@ -417,7 +418,7 @@ class SecureImageService { }) }); - console.warn(`Suspicious activity flagged: ${reason}`, { + logger.warn(`Suspicious activity flagged: ${reason}`, { clientFingerprint, photoId, details @@ -432,12 +433,12 @@ class SecureImageService { .first(); if (parseInt(recentSuspicious.count) >= 3) { - console.warn(`Client fingerprint flagged for blocking: ${clientFingerprint}`); + logger.warn(`Client fingerprint flagged for blocking: ${clientFingerprint}`); // This would be handled by the middleware's blocking system } } catch (error) { - console.error('Error flagging suspicious activity:', error); + logger.error('Error flagging suspicious activity:', error); } } @@ -457,13 +458,13 @@ class SecureImageService { // Flag if more than 10 accesses to same photo in 5 minutes if (accessCount > 10) { - console.warn(`Suspicious activity detected: ${accessCount} accesses to photo ${photoId} from ${clientFingerprint}`); + logger.warn(`Suspicious activity detected: ${accessCount} accesses to photo ${photoId} from ${clientFingerprint}`); return true; } return false; } catch (error) { - console.error('Error detecting suspicious activity:', error); + logger.error('Error detecting suspicious activity:', error); return false; } } diff --git a/backend/src/services/shareLinkService.js b/backend/src/services/shareLinkService.js index 8bcfe1f2..c3efaf75 100644 --- a/backend/src/services/shareLinkService.js +++ b/backend/src/services/shareLinkService.js @@ -2,6 +2,7 @@ const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const { extractShareToken, isPotentialShareToken, buildSharePath } = require('../utils/shareLinkUtils'); const { getFrontendBaseUrl } = require('../utils/frontendUrl'); +const logger = require('../utils/logger'); const SETTING_KEY = 'general_short_gallery_urls'; const CACHE_TTL_MS = 60_000; @@ -59,7 +60,7 @@ const getRawSettingValue = async () => { const setting = await db('app_settings').where({ setting_key: SETTING_KEY }).first(); return setting?.setting_value ?? null; } catch (error) { - console.error('Failed to read gallery URL setting:', error.message); + logger.error('Failed to read gallery URL setting:', error.message); return null; } }; diff --git a/backend/src/services/uploadSettings.js b/backend/src/services/uploadSettings.js index 62f71b8f..e438748e 100644 --- a/backend/src/services/uploadSettings.js +++ b/backend/src/services/uploadSettings.js @@ -1,4 +1,5 @@ const { db } = require('../database/db'); +const logger = require('../utils/logger'); const DEFAULT_MAX_FILES_PER_UPLOAD = 500; const MAX_ALLOWED_FILES_PER_UPLOAD = 2000; @@ -87,7 +88,7 @@ const getMaxFilesPerUpload = async () => { cacheExpiresAt = Date.now() + CACHE_TTL_MS; return parsedValue; } catch (error) { - console.error('Failed to read max files per upload setting:', error.message); + logger.error('Failed to read max files per upload setting:', error.message); cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD; cacheExpiresAt = Date.now() + CACHE_TTL_MS; return DEFAULT_MAX_FILES_PER_UPLOAD; @@ -147,7 +148,7 @@ const getAllowedMimeTypes = async () => { allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS; return mimeTypes; } catch (error) { - console.error('Failed to read allowed file types setting:', error.message); + logger.error('Failed to read allowed file types setting:', error.message); const fallback = extensionsToMimeTypes(DEFAULT_ALLOWED_FILE_TYPES); cachedAllowedTypes = fallback; allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS; diff --git a/backend/src/services/watermarkGeneratorService.js b/backend/src/services/watermarkGeneratorService.js index eaf4bad6..b8177e04 100644 --- a/backend/src/services/watermarkGeneratorService.js +++ b/backend/src/services/watermarkGeneratorService.js @@ -12,6 +12,7 @@ const { db } = require('../database/db'); const watermarkService = require('./watermarkService'); const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('./photoResolver'); const { withLocalCopy } = require('./imageProcessor'); +const logger = require('../utils/logger'); class WatermarkGeneratorService { constructor() { @@ -79,7 +80,7 @@ class WatermarkGeneratorService { return result; } catch (error) { - console.error(`Error generating watermark for photo ${photoId}:`, error); + logger.error(`Error generating watermark for photo ${photoId}:`, error); return { success: false, error: error.message }; } } @@ -155,7 +156,7 @@ class WatermarkGeneratorService { return results; } catch (error) { - console.error(`Error generating watermarks for event ${eventId}:`, error); + logger.error(`Error generating watermarks for event ${eventId}:`, error); return { ...results, errors: [...results.errors, error.message] }; } } @@ -232,7 +233,7 @@ class WatermarkGeneratorService { return results; } - console.log(`Starting watermark regeneration for ${photos.length} photos`); + logger.info(`Starting watermark regeneration for ${photos.length} photos`); // Process in batches for (let i = 0; i < photos.length; i += this.batchSize) { @@ -281,11 +282,11 @@ class WatermarkGeneratorService { } results.status = 'completed'; - console.log(`Watermark regeneration completed: ${results.success}/${results.total} successful`); + logger.info(`Watermark regeneration completed: ${results.success}/${results.total} successful`); return results; } catch (error) { - console.error('Error during watermark regeneration:', error); + logger.error('Error during watermark regeneration:', error); results.status = 'failed'; results.errors.push(error.message); return results; @@ -320,10 +321,10 @@ class WatermarkGeneratorService { watermark_generated_at: null }); - console.log(`Cleared ${photos.length} watermarks`); + logger.info(`Cleared ${photos.length} watermarks`); return { success: true, cleared: photos.length }; } catch (error) { - console.error('Error clearing watermarks:', error); + logger.error('Error clearing watermarks:', error); return { success: false, error: error.message }; } } @@ -350,7 +351,7 @@ class WatermarkGeneratorService { return { success: true }; } catch (error) { - console.error(`Error deleting watermark for photo ${photoId}:`, error); + logger.error(`Error deleting watermark for photo ${photoId}:`, error); return { success: false, error: error.message }; } } diff --git a/backend/src/services/watermarkService.js b/backend/src/services/watermarkService.js index c46f252f..ccb9c6ee 100644 --- a/backend/src/services/watermarkService.js +++ b/backend/src/services/watermarkService.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs').promises; const { db } = require('../database/db'); const { getStorage } = require('./storage'); +const logger = require('../utils/logger'); class WatermarkService { constructor() { @@ -44,7 +45,7 @@ class WatermarkService { companyName: settingsObj.branding_company_name || 'Photo Gallery' }; } catch (error) { - console.error('Error fetching watermark settings:', error); + logger.error('Error fetching watermark settings:', error); return null; } } @@ -138,7 +139,7 @@ class WatermarkService { watermarkMetadata = { width: targetWidth, height: targetHeight }; } catch (error) { - console.error('Error processing watermark logo:', error); + logger.error('Error processing watermark logo:', error); watermarkBuffer = null; } } @@ -209,7 +210,7 @@ class WatermarkService { return watermarkedBuffer; } catch (error) { - console.error('Error applying watermark:', error); + logger.error('Error applying watermark:', error); // Return original image on error return await fs.readFile(imagePath); } @@ -287,7 +288,7 @@ class WatermarkService { error: null }; } catch (error) { - console.error(`Error generating watermark for photo ${photo.id}:`, error); + logger.error(`Error generating watermark for photo ${photo.id}:`, error); return { success: false, watermarkPath: null, @@ -308,7 +309,7 @@ class WatermarkService { await getStorage().delete(watermarkPath); return true; } catch (error) { - console.error('Error deleting watermark file:', error); + logger.error('Error deleting watermark file:', error); return false; } } diff --git a/backend/src/utils/dateFormatter.js b/backend/src/utils/dateFormatter.js index 55cc8eaa..cd7606e9 100644 --- a/backend/src/utils/dateFormatter.js +++ b/backend/src/utils/dateFormatter.js @@ -1,4 +1,5 @@ const { db } = require('../database/db'); +const logger = require('./logger'); // Default date format settings const DEFAULT_FORMAT = { @@ -19,7 +20,7 @@ async function formatDate(date, language = 'en') { try { dateConfig = JSON.parse(setting.setting_value); } catch (e) { - console.warn('Failed to parse date format setting:', e.message); + logger.warn('Failed to parse date format setting:', e.message); dateConfig = DEFAULT_FORMAT; } } else { @@ -46,7 +47,7 @@ async function formatDate(date, language = 'en') { // Check if date is valid if (isNaN(dateObj.getTime())) { - console.error('Invalid date provided to formatDate:', date); + logger.error('Invalid date provided to formatDate:', date); throw new Error('Invalid date'); } @@ -91,7 +92,7 @@ async function formatDate(date, language = 'en') { }); } } catch (error) { - console.error('Error formatting date:', error); + logger.error('Error formatting date:', error); // Fallback to basic formatting return date instanceof Date ? date.toLocaleDateString() : new Date(date).toLocaleDateString(); } diff --git a/backend/src/utils/dbCompat.js b/backend/src/utils/dbCompat.js index 2e28033b..a9d2cd77 100644 --- a/backend/src/utils/dbCompat.js +++ b/backend/src/utils/dbCompat.js @@ -5,6 +5,7 @@ // Note: Requiring db here creates circular dependency // db should be passed as parameter or required where needed +const logger = require('./logger'); /** * Get database client type @@ -91,7 +92,7 @@ async function getDatabaseSize(db, dbName) { const stats = await fs.stat(dbPath); return stats.size; } catch (error) { - console.error('Error getting SQLite database size:', error); + logger.error('Error getting SQLite database size:', error); return 0; } } diff --git a/backend/src/utils/fileSecurityUtils.js b/backend/src/utils/fileSecurityUtils.js index 0a5c3628..39e59b70 100644 --- a/backend/src/utils/fileSecurityUtils.js +++ b/backend/src/utils/fileSecurityUtils.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs').promises; +const logger = require('./logger'); /** * Secure file security utilities to prevent path traversal and validate file types @@ -174,7 +175,7 @@ async function validateFileContent(filePath, expectedMimeType) { return true; }); } catch (error) { - console.error('Error validating file content:', error); + logger.error('Error validating file content:', error); return false; } } @@ -239,7 +240,7 @@ function createFileUploadValidator(options = {}) { try { await fs.unlink(file.path); } catch (err) { - console.error('Error removing invalid file:', err); + logger.error('Error removing invalid file:', err); } return res.status(400).json({ error: `File content does not match declared type: ${file.originalname}` @@ -250,7 +251,7 @@ function createFileUploadValidator(options = {}) { next(); } catch (error) { - console.error('File validation error:', error); + logger.error('File validation error:', error); res.status(500).json({ error: 'File validation failed' }); } }; diff --git a/backend/src/utils/routeHelpers.js b/backend/src/utils/routeHelpers.js index d29e9913..28d7ce31 100644 --- a/backend/src/utils/routeHelpers.js +++ b/backend/src/utils/routeHelpers.js @@ -5,6 +5,7 @@ const { validationResult } = require('express-validator'); const { ValidationError } = require('./errors'); +const logger = require('./logger'); /** * Wraps an async route handler to catch errors and pass them to the error handler. @@ -69,25 +70,26 @@ const successResponse = (res, data, statusCode = 200, message = null) => { }; /** - * Sends a standardized error response. - * Note: Prefer throwing custom errors and letting the error handler format the response. + * Logs an error and sends a standardized error response of shape `{ error: }`. * * @param {Response} res - Express response object - * @param {string} message - Error message + * @param {Error|*} error - The caught error (logged, never sent to the client) * @param {number} [statusCode=500] - HTTP status code - * @param {string} [code] - Optional error code - * @param {*} [details] - Optional additional error details + * @param {string} [publicMessage] - Message sent to the client; falls back to the error's message * * @example - * errorResponse(res, 'Invalid input', 400, 'VALIDATION_ERROR', { field: 'email' }); + * } catch (error) { + * errorResponse(res, error, 500, 'Failed to fetch events'); + * } */ -const errorResponse = (res, message, statusCode = 500, code = null, details = null) => { - const response = { - error: message, - ...(code && { code }), - ...(details && { details }) - }; - res.status(statusCode).json(response); +const errorResponse = (res, error, statusCode = 500, publicMessage) => { + const message = publicMessage || (error instanceof Error ? error.message : String(error)); + const route = res.req ? `${res.req.method} ${res.req.originalUrl}` : null; + logger.error(route ? `${route} - ${message}` : message, { + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : undefined + }); + res.status(statusCode).json({ error: message }); }; /**