refactor(backend): standardize error responses, logging, pagination

- errorResponse(res, error, status, publicMessage) in routeHelpers,
  wired into 125 catch blocks across 10 route files; wire format
  ({ error: <string> }) unchanged byte-for-byte
- Replace remaining console.* with logger across src (178 sites);
  3 intentional console sites kept (install boot, unbound .catch ref)
- Adopt getPagination in 6 routes where semantics match exactly
This commit is contained in:
Paul Nothaft
2026-07-03 07:49:45 +02:00
parent d44ead41c5
commit 6eb46d8c31
41 changed files with 372 additions and 482 deletions
+3 -2
View File
@@ -9,6 +9,7 @@ const multer = require('multer');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { validateFileType } = require('../utils/fileSecurityUtils'); const { validateFileType } = require('../utils/fileSecurityUtils');
const logger = require('../utils/logger');
/** /**
* Get the storage path from environment or default * Get the storage path from environment or default
@@ -220,14 +221,14 @@ const createCustomUploader = (config) => {
const uploadTimeoutMiddleware = (timeout = 300000) => { const uploadTimeoutMiddleware = (timeout = 300000) => {
return (req, res, next) => { return (req, res, next) => {
req.setTimeout(timeout, () => { req.setTimeout(timeout, () => {
console.error('Upload request timed out'); logger.error('Upload request timed out');
if (!res.headersSent) { if (!res.headersSent) {
res.status(408).json({ error: 'Upload request timed out' }); res.status(408).json({ error: 'Upload request timed out' });
} }
}); });
res.setTimeout(timeout, () => { res.setTimeout(timeout, () => {
console.error('Upload response timed out'); logger.error('Upload response timed out');
}); });
next(); next();
+4 -3
View File
@@ -1,4 +1,5 @@
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger');
// Cache maintenance mode status to avoid DB queries on every request // Cache maintenance mode status to avoid DB queries on every request
let maintenanceMode = false; let maintenanceMode = false;
@@ -26,7 +27,7 @@ async function queryWithRetry(queryFn, retries = MAX_RETRIES) {
error.code === 'ECONNRESET'; error.code === 'ECONNRESET';
if (isConnectionError) { 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)); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
} else { } else {
throw error; // Don't retry non-connection errors throw error; // Don't retry non-connection errors
@@ -56,7 +57,7 @@ async function checkMaintenanceMode() {
return maintenanceMode; return maintenanceMode;
} catch (error) { } 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 cached value or false if no cache
return maintenanceMode; return maintenanceMode;
} }
@@ -102,7 +103,7 @@ async function maintenanceMiddleware(req, res, next) {
} }
} catch (error) { } catch (error) {
// If we can't check maintenance mode, allow the request to proceed // 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(); next();
@@ -333,7 +333,7 @@ class SecureImageMiddleware {
await db('security_logs').insert(logData).catch(console.error); await db('security_logs').insert(logData).catch(console.error);
} }
} catch (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 perHour: config.perHour || 500
}; };
} catch (error) { } 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 }; return { perMinute: 30, per5Minutes: 100, perHour: 500 };
} }
} }
+3 -2
View File
@@ -1,6 +1,7 @@
const path = require('path'); const path = require('path');
const express = require('express'); const express = require('express');
const { safePathJoin, isPathSafe } = require('../utils/fileSecurityUtils'); const { safePathJoin, isPathSafe } = require('../utils/fileSecurityUtils');
const logger = require('../utils/logger');
/** /**
* Create a secure static file serving middleware that prevents path traversal attacks * 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 // Validate the path doesn't contain dangerous patterns
if (!isPathSafe(requestedPath)) { 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' }); return res.status(403).json({ error: 'Access denied' });
} }
@@ -52,7 +53,7 @@ function secureStatic(basePath, options = {}) {
return staticMiddleware(req, res, next); return staticMiddleware(req, res, next);
} catch (error) { } catch (error) {
// Path traversal detected // 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' }); return res.status(403).json({ error: 'Access denied' });
} }
}; };
+2 -1
View File
@@ -1,6 +1,7 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { db } = require('../database/db'); const { db } = require('../database/db');
const { getAdminTokenFromRequest } = require('../utils/tokenUtils'); const { getAdminTokenFromRequest } = require('../utils/tokenUtils');
const logger = require('../utils/logger');
// In-memory session tracking (in production, use Redis) // In-memory session tracking (in production, use Redis)
const sessions = new Map(); const sessions = new Map();
@@ -69,7 +70,7 @@ async function getSessionTimeout() {
} catch (error) { } catch (error) {
// Only log if it's not a connection error (to avoid spam) // Only log if it's not a connection error (to avoid spam)
if (error.code !== 'ECONNRESET' && !error.message?.includes('Connection terminated')) { 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);
} }
} }
+20 -20
View File
@@ -9,14 +9,14 @@ const { requirePermission } = require('../middleware/permissions');
const archiver = require('archiver'); const archiver = require('archiver');
const StreamZip = require('node-stream-zip'); const StreamZip = require('node-stream-zip');
const { requireEventOwnership } = require('../middleware/ownership'); const { requireEventOwnership } = require('../middleware/ownership');
const logger = require('../utils/logger');
const { getPagination } = require('../utils/routeHelpers');
const router = express.Router(); const router = express.Router();
// Get all archived events // Get all archived events
router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) => { router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) => {
try { try {
const page = parseInt(req.query.page) || 1; const { page, limit, offset } = getPagination(req);
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
// Get total count // Get total count
const totalCount = await db('events') const totalCount = await db('events')
@@ -48,7 +48,7 @@ router.get('/', adminAuth, requirePermission('archives.view'), async (req, res)
const stats = await fs.stat(fullArchivePath); const stats = await fs.stat(fullArchivePath);
archiveFileSize = stats.size; archiveFileSize = stats.size;
} catch (error) { } 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) { } catch (error) {
console.error('Archives list error:', error); logger.error('Archives list error:', error);
res.status(500).json({ error: 'Failed to fetch archives' }); 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 path: archive.archive_path
}; };
} catch (error) { } 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 archiveFile: archiveFileInfo
}); });
} catch (error) { } catch (error) {
console.error('Archive details error:', error); logger.error('Archive details error:', error);
res.status(500).json({ error: 'Failed to fetch archive details' }); 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 }); await fs.mkdir(eventDir, { recursive: true });
// Log ZIP contents for debugging // Log ZIP contents for debugging
console.log(`Extracting archive to: ${eventDir}`); logger.info(`Extracting archive to: ${eventDir}`);
const entries = Object.values(await zip.entries()); 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 // Stream-extract everything to disk
await zip.extract(null, eventDir); 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); 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) { } catch (e) {
if (e.code !== 'ENOENT') { 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 { } 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) { } catch (statError) {
console.error(`Failed to stat file: ${actualFilePath}`); logger.error(`Failed to stat file: ${actualFilePath}`);
console.error(`Entry name was: ${entry.name}`); logger.error(`Entry name was: ${entry.name}`);
console.error('Error:', statError.message); logger.error('Error:', statError.message);
// Skip this file if we can't stat it // Skip this file if we can't stat it
continue; continue;
} }
@@ -301,7 +301,7 @@ router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), re
} }
} catch (extractError) { } 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 }); 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' }); res.json({ message: 'Archive restored successfully' });
} catch (error) { } catch (error) {
console.error('Archive restore error:', error); logger.error('Archive restore error:', error);
res.status(500).json({ error: 'Failed to restore archive' }); 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 }) metadata: JSON.stringify({ event_name: archive.event_name })
}); });
} catch (error) { } catch (error) {
console.error('Archive download error:', error); logger.error('Archive download error:', error);
res.status(500).json({ error: 'Failed to download archive' }); 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); const fullArchivePath = path.join(storagePath, archive.archive_path);
await fs.unlink(fullArchivePath); await fs.unlink(fullArchivePath);
} catch (error) { } 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' }); res.json({ message: 'Archive deleted permanently' });
} catch (error) { } catch (error) {
console.error('Archive delete error:', error); logger.error('Archive delete error:', error);
res.status(500).json({ error: 'Failed to delete archive' }); res.status(500).json({ error: 'Failed to delete archive' });
} }
}); });
+19 -35
View File
@@ -4,6 +4,7 @@ const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { triggerManualBackup, getBackupStatus, cleanupOldBackupRuns, getBackupManifest, validateBackupManifest } = require('../services/backupService'); const { triggerManualBackup, getBackupStatus, cleanupOldBackupRuns, getBackupManifest, validateBackupManifest } = require('../services/backupService');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse, getPagination } = require('../utils/routeHelpers');
const { formatBytes } = require('../utils/formatBytes'); const { formatBytes } = require('../utils/formatBytes');
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
@@ -31,8 +32,7 @@ router.get('/config', adminAuth, requirePermission('backup.view'), async (req, r
res.json(config); res.json(config);
} catch (error) { } catch (error) {
logger.error('Failed to get backup configuration:', error); errorResponse(res, error, 500, 'Failed to get backup configuration');
res.status(500).json({ error: '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' }); res.json({ success: true, message: 'Backup configuration updated' });
} catch (error) { } catch (error) {
logger.error('Failed to update backup configuration:', error); errorResponse(res, error, 500, 'Failed to update backup configuration');
res.status(500).json({ error: 'Failed to update backup configuration' });
} }
}); });
// Get backup status and history // Get backup status and history
router.get('/status', adminAuth, requirePermission('backup.view'), async (req, res) => { router.get('/status', adminAuth, requirePermission('backup.view'), async (req, res) => {
try { try {
const limit = parseInt(req.query.limit) || 10; const { limit } = getPagination(req, { limit: 10 });
const status = await getBackupStatus(limit); const status = await getBackupStatus(limit);
res.json(status); res.json(status);
} catch (error) { } catch (error) {
logger.error('Failed to get backup status:', error); errorResponse(res, error, 500, 'Failed to get backup status');
res.status(500).json({ error: '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' }); res.json({ success: true, message: 'Backup started' });
} catch (error) { } catch (error) {
logger.error('Failed to trigger manual backup:', error); errorResponse(res, error, 500, 'Failed to trigger backup');
res.status(500).json({ error: 'Failed to trigger backup' });
} }
}); });
@@ -156,8 +153,7 @@ router.get('/runs/:id', adminAuth, requirePermission('backup.view'), async (req,
res.json(run); res.json(run);
} catch (error) { } catch (error) {
logger.error('Failed to get backup run details:', error); errorResponse(res, error, 500, 'Failed to get backup run details');
res.status(500).json({ error: 'Failed to get backup run details' });
} }
}); });
@@ -191,8 +187,7 @@ router.get('/files', adminAuth, requirePermission('backup.view'), async (req, re
} }
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get backup file states:', error); errorResponse(res, error, 500, 'Failed to get file states');
res.status(500).json({ error: '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` }); res.json({ success: true, message: `Cleaned up backup runs older than ${days} days` });
} catch (error) { } catch (error) {
logger.error('Failed to cleanup old backup runs:', error); errorResponse(res, error, 500, 'Failed to cleanup backup runs');
res.status(500).json({ error: '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' }); res.status(400).json({ error: 'Invalid destination type' });
} }
} catch (error) { } catch (error) {
logger.error('Failed to test backup connection:', error); errorResponse(res, error, 500, 'Failed to test connection');
res.status(500).json({ error: 'Failed to test connection' });
} }
}); });
@@ -381,8 +374,7 @@ router.post('/manifest/validate', adminAuth, requirePermission('backup.view'), a
manifestPath manifestPath
}); });
} catch (error) { } catch (error) {
logger.error('Failed to validate manifest:', error); errorResponse(res, error, 500, 'Failed to validate manifest');
res.status(500).json({ error: 'Failed to validate manifest' });
} }
}); });
@@ -494,8 +486,7 @@ router.post('/manifests/validate', adminAuth, requirePermission('backup.view'),
manifestPath manifestPath
}); });
} catch (error) { } catch (error) {
logger.error('Failed to validate manifest:', error); errorResponse(res, error, 500, 'Failed to validate manifest');
res.status(500).json({ error: 'Failed to validate manifest' });
} }
}); });
@@ -526,8 +517,7 @@ router.get('/s3/buckets', adminAuth, requirePermission('backup.view'), async (re
owner: result.Owner || null owner: result.Owner || null
}); });
} catch (error) { } catch (error) {
logger.error('Failed to list S3 buckets:', error); errorResponse(res, error, 500, 'Failed to list S3 buckets');
res.status(500).json({ error: 'Failed to list S3 buckets' });
} }
}); });
@@ -563,8 +553,7 @@ router.get('/s3/files', adminAuth, requirePermission('backup.view'), async (req,
prefix: prefix prefix: prefix
}); });
} catch (error) { } catch (error) {
logger.error('Failed to list S3 files:', error); errorResponse(res, error, 500, 'Failed to list S3 files');
res.status(500).json({ error: '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` message: `Cleaned up ${deletedCount} S3 backup files older than ${retentionDays} days`
}); });
} catch (error) { } catch (error) {
logger.error('Failed to cleanup S3 backups:', error); errorResponse(res, error, 500, 'Failed to cleanup S3 backups');
res.status(500).json({ error: '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' message: 'S3 upload test completed successfully'
}); });
} catch (error) { } catch (error) {
logger.error('S3 upload test failed:', error); errorResponse(res, error, 500, 'S3 upload test failed');
res.status(500).json({ error: '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' }); return res.status(400).json({ error: 'Unknown backup type' });
} }
} catch (error) { } catch (error) {
logger.error('Failed to download backup:', error); errorResponse(res, error, 500, 'Failed to download backup');
res.status(500).json({ error: 'Failed to download backup' });
} }
}); });
@@ -838,8 +824,7 @@ router.get('/checksums', adminAuth, requirePermission('backup.view'), async (req
path: targetPath || '/' path: targetPath || '/'
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get file checksums:', error); errorResponse(res, error, 500, 'Failed to get file checksums');
res.status(500).json({ error: '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'] : [] warnings: totalSize > 10 * 1024 * 1024 * 1024 ? ['Backup size exceeds 10GB, may take significant time'] : []
}); });
} catch (error) { } catch (error) {
logger.error('Failed to estimate backup size:', error); errorResponse(res, error, 500, 'Failed to estimate backup size');
res.status(500).json({ error: 'Failed to estimate backup size' });
} }
}); });
+6 -5
View File
@@ -7,6 +7,7 @@ const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth'); const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { validateFileType } = require('../utils/fileSecurityUtils'); const { validateFileType } = require('../utils/fileSecurityUtils');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); 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'); const pages = await db('cms_pages').select('*').orderBy('slug', 'asc');
res.json(pages); res.json(pages);
} catch (error) { } 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' }); 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); res.json(page);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to fetch page' });
} }
}); });
@@ -144,7 +145,7 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
res.json(updated); res.json(updated);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to update page' });
} }
}); });
@@ -184,7 +185,7 @@ router.post(
res.json({ logo_url: logoUrl }); res.json({ logo_url: logoUrl });
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to upload logo' });
} }
} }
@@ -208,7 +209,7 @@ router.delete(
res.json({ logo_url: null }); res.json({ logo_url: null });
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to clear logo' });
} }
} }
+7 -6
View File
@@ -4,6 +4,7 @@ const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat'); const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth'); const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get all global categories // Get all global categories
@@ -15,7 +16,7 @@ router.get('/global', adminAuth, requirePermission('settings.view'), async (req,
res.json(categories); res.json(categories);
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error); logger.error('Error fetching categories:', error);
res.status(500).json({ error: 'Failed to fetch categories' }); 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); res.json(categories);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to fetch categories' });
} }
}); });
@@ -101,7 +102,7 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [
res.json(category); res.json(category);
} catch (error) { } catch (error) {
console.error('Error creating category:', error); logger.error('Error creating category:', error);
res.status(500).json({ error: 'Failed to create category' }); res.status(500).json({ error: 'Failed to create category' });
} }
}); });
@@ -165,7 +166,7 @@ router.put('/:id', adminAuth, requirePermission('settings.edit'), [
res.json(updated); res.json(updated);
} catch (error) { } catch (error) {
console.error('Error updating category:', error); logger.error('Error updating category:', error);
res.status(500).json({ error: 'Failed to update category' }); res.status(500).json({ error: 'Failed to update category' });
} }
}); });
@@ -214,7 +215,7 @@ router.put('/:id/hero', adminAuth, requirePermission('settings.edit'), [
res.json(updated); res.json(updated);
} catch (error) { } 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' }); 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' }); res.json({ message: 'Category deleted successfully' });
} catch (error) { } catch (error) {
console.error('Error deleting category:', error); logger.error('Error deleting category:', error);
res.status(500).json({ error: 'Failed to delete category' }); res.status(500).json({ error: 'Failed to delete category' });
} }
}); });
+6 -5
View File
@@ -11,6 +11,7 @@ const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { sanitizeCSS, validateCSS, MAX_CSS_SIZE } = require('../utils/cssSanitizer'); const { sanitizeCSS, validateCSS, MAX_CSS_SIZE } = require('../utils/cssSanitizer');
const { DEFAULT_CSS_TEMPLATE } = require('../../migrations/core/052_add_css_templates'); const { DEFAULT_CSS_TEMPLATE } = require('../../migrations/core/052_add_css_templates');
const logger = require('../utils/logger');
/** /**
* GET /admin/css-templates * GET /admin/css-templates
@@ -23,7 +24,7 @@ router.get('/', adminAuth, requirePermission('branding.view'), async (req, res)
); );
res.json({ success: true, templates }); res.json({ success: true, templates });
} catch (error) { } 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' }); 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 }); res.json({ success: true, templates });
} catch (error) { } 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' }); 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 }); res.json({ success: true, template });
} catch (error) { } catch (error) {
console.error('Get template error:', error); logger.error('Get template error:', error);
res.status(500).json({ error: 'Failed to fetch template' }); res.status(500).json({ error: 'Failed to fetch template' });
} }
}); });
@@ -150,7 +151,7 @@ router.put('/:slotNumber', adminAuth, requirePermission('branding.edit'), [
sanitization_warnings: warnings sanitization_warnings: warnings
}); });
} catch (error) { } catch (error) {
console.error('Update template error:', error); logger.error('Update template error:', error);
res.status(500).json({ error: 'Failed to update template' }); 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 }); res.json({ success: true, template });
} catch (error) { } catch (error) {
console.error('Reset template error:', error); logger.error('Reset template error:', error);
res.status(500).json({ error: 'Failed to reset template' }); res.status(500).json({ error: 'Failed to reset template' });
} }
}); });
+8 -11
View File
@@ -6,6 +6,7 @@ const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
const { formatBoolean } = require('../utils/dbCompat'); const { formatBoolean } = require('../utils/dbCompat');
const { resolveAdapter } = require('../services/trackers'); const { resolveAdapter } = require('../services/trackers');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse, getPagination } = require('../utils/routeHelpers');
const router = express.Router(); const router = express.Router();
/** /**
@@ -126,15 +127,14 @@ router.get('/stats', adminAuth, requirePermission('analytics.view'), async (req,
totalEvents: totalEvents.count || 0 totalEvents: totalEvents.count || 0
}); });
} catch (error) { } catch (error) {
console.error('Dashboard stats error:', error); errorResponse(res, error, 500, 'Failed to fetch dashboard statistics');
res.status(500).json({ error: 'Failed to fetch dashboard statistics' });
} }
}); });
// Get recent activity // Get recent activity
router.get('/activity', adminAuth, requirePermission('analytics.view'), async (req, res) => { router.get('/activity', adminAuth, requirePermission('analytics.view'), async (req, res) => {
try { try {
const limit = parseInt(req.query.limit) || 10; const { limit } = getPagination(req, { limit: 10 });
const activities = await db('activity_logs') const activities = await db('activity_logs')
.select('activity_logs.*', 'events.event_name') .select('activity_logs.*', 'events.event_name')
@@ -155,7 +155,7 @@ router.get('/activity', adminAuth, requirePermission('analytics.view'), async (r
if (typeof activity.metadata === 'object') return activity.metadata; if (typeof activity.metadata === 'object') return activity.metadata;
return JSON.parse(activity.metadata); return JSON.parse(activity.metadata);
} catch (e) { } 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 {}; return {};
} }
})(), })(),
@@ -164,8 +164,7 @@ router.get('/activity', adminAuth, requirePermission('analytics.view'), async (r
res.json(formattedActivities); res.json(formattedActivities);
} catch (error) { } catch (error) {
console.error('Activity log error:', error); errorResponse(res, error, 500, 'Failed to fetch activity log');
res.status(500).json({ error: 'Failed to fetch activity log' });
} }
}); });
@@ -233,7 +232,7 @@ router.get('/health', adminAuth, requirePermission('settings.view'), async (req,
} }
}); });
} catch (error) { } catch (error) {
console.error('Health check error:', error); logger.error('Health check error:', error);
res.status(500).json({ res.status(500).json({
overall: 'error', overall: 'error',
error: 'Failed to check system health' error: 'Failed to check system health'
@@ -400,8 +399,7 @@ router.get('/analytics', adminAuth, requirePermission('analytics.view'), async (
} }
}); });
} catch (error) { } catch (error) {
console.error('Analytics error:', error); errorResponse(res, error, 500, 'Failed to fetch analytics data');
res.status(500).json({ error: 'Failed to fetch analytics data' });
} }
}); });
@@ -586,8 +584,7 @@ router.get('/crm-stats', adminAuth, async (req, res) => {
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
}); });
} catch (error) { } catch (error) {
require('../utils/logger').error('CRM stats error:', error); errorResponse(res, error, 500, 'Failed to load CRM stats');
res.status(500).json({ error: 'Failed to load CRM stats' });
} }
}); });
+2 -3
View File
@@ -5,6 +5,7 @@ const { requirePermission } = require('../middleware/permissions');
const { databaseBackupService } = require('../services/databaseBackup'); const { databaseBackupService } = require('../services/databaseBackup');
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { getPagination } = require('../utils/routeHelpers');
// All routes require admin authentication // All routes require admin authentication
router.use(adminAuth); router.use(adminAuth);
@@ -154,9 +155,7 @@ router.get('/progress', requirePermission('backup.view'), async (req, res) => {
*/ */
router.get('/history', requirePermission('backup.view'), async (req, res) => { router.get('/history', requirePermission('backup.view'), async (req, res) => {
try { try {
const page = parseInt(req.query.page) || 1; const { page, limit, offset } = getPagination(req);
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const [backups, totalCount] = await Promise.all([ const [backups, totalCount] = await Promise.all([
db('database_backup_runs') db('database_backup_runs')
+22 -30
View File
@@ -5,6 +5,8 @@ const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth'); const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { wrapEmailHtml, processEmailQueue } = require('../services/emailProcessor'); const { wrapEmailHtml, processEmailQueue } = require('../services/emailProcessor');
const { errorResponse } = require('../utils/routeHelpers');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get email configuration // Get email configuration
@@ -31,8 +33,7 @@ router.get('/config', adminAuth, requirePermission('email.view'), async (req, re
smtp_pass: config.smtp_pass ? '********' : '' smtp_pass: config.smtp_pass ? '********' : ''
}); });
} catch (error) { } catch (error) {
console.error('Email config fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch email configuration');
res.status(500).json({ error: 'Failed to fetch email configuration' });
} }
}); });
@@ -113,8 +114,7 @@ router.post('/config', [
res.json({ message: 'Email configuration updated successfully' }); res.json({ message: 'Email configuration updated successfully' });
} catch (error) { } catch (error) {
console.error('Email config update error:', error); errorResponse(res, error, 500, 'Failed to update email configuration');
res.status(500).json({ error: 'Failed to update email configuration' });
} }
}); });
@@ -131,8 +131,7 @@ router.get('/incoming-config', adminAuth, requirePermission('email.view'), async
imap_folder: c?.imap_folder || 'INBOX', imap_folder: c?.imap_folder || 'INBOX',
}); });
} catch (error) { } catch (error) {
console.error('Incoming mail config fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch incoming mail configuration');
res.status(500).json({ error: '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 }); 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' }); res.json({ message: 'Incoming mail configuration updated successfully' });
} catch (error) { } catch (error) {
console.error('Incoming mail config update error:', error); errorResponse(res, error, 500, 'Failed to update incoming mail configuration');
res.status(500).json({ error: 'Failed to update incoming mail configuration' });
} }
}); });
@@ -192,7 +190,7 @@ router.post('/incoming-config/folders', adminAuth, requirePermission('email.view
); );
res.json({ folders }); res.json({ folders });
} catch (error) { } 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.` }); 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); res.json(result);
} catch (error) { } 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.` }); 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) return res.status(result.reason === 'not_received' ? 504 : 400)
.json({ error: map[result.reason] || 'Round-trip test failed.', sent: !!result.sent, recipient: result.recipient }); .json({ error: map[result.reason] || 'Round-trip test failed.', sent: !!result.sent, recipient: result.recipient });
} catch (error) { } 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.` }); 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(); const result = await emailIntakeService.pollOnce();
res.json(result); // { processed } or { skipped: 'disabled'|'unconfigured'|'busy' } res.json(result); // { processed } or { skipped: 'disabled'|'unconfigured'|'busy' }
} catch (error) { } catch (error) {
console.error('Manual poll error:', error); logger.error('Manual poll error:', error);
res.status(422).json({ error: `Mailbox poll failed (${error.message}).` }); 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); 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) } }); res.json({ items, pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) } });
} catch (error) { } catch (error) {
console.error('Received emails fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch received emails');
res.status(500).json({ error: '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' debug: process.env.NODE_ENV === 'development'
}; };
console.log('Creating email transporter with config:', { logger.info('Creating email transporter with config:', {
host: transportConfig.host, host: transportConfig.host,
port: transportConfig.port, port: transportConfig.port,
secure: transportConfig.secure, secure: transportConfig.secure,
@@ -356,8 +353,8 @@ router.post('/test', adminAuth, requirePermission('email.send'), async (req, res
res.json({ message: 'Test email sent successfully' }); res.json({ message: 'Test email sent successfully' });
} catch (error) { } catch (error) {
console.error('Test email error:', error); logger.error('Test email error:', error);
console.error('Error stack:', error.stack); logger.error('Error stack:', error.stack);
// Provide more specific error messages with translation keys // Provide more specific error messages with translation keys
let errorMessage = 'Error sending email'; 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 */ } } catch (_) { /* activity logging is best-effort */ }
res.json({ message: 'Email queue flushed', ...summary }); res.json({ message: 'Email queue flushed', ...summary });
} catch (error) { } 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 }); 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 }, pagination: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) || 1 },
}); });
} catch (error) { } 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 }); 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; if (typeof template.variables === 'object') return template.variables;
return JSON.parse(template.variables); return JSON.parse(template.variables);
} catch (e) { } 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 []; return [];
} }
} }
@@ -611,8 +608,7 @@ router.get('/templates', adminAuth, requirePermission('email.view'), async (req,
res.json(formattedTemplates); res.json(formattedTemplates);
} catch (error) { } catch (error) {
console.error('Email templates fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch email templates');
res.status(500).json({ error: 'Failed to fetch email templates' });
} }
}); });
@@ -641,8 +637,7 @@ router.get('/templates/:key', adminAuth, requirePermission('email.view'), async
updated_at: template.updated_at, updated_at: template.updated_at,
}); });
} catch (error) { } catch (error) {
console.error('Email template fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch email template');
res.status(500).json({ error: 'Failed to fetch email template' });
} }
}); });
@@ -730,8 +725,7 @@ router.put('/templates/:key', [
res.json({ message: 'Email template updated successfully' }); res.json({ message: 'Email template updated successfully' });
} catch (error) { } catch (error) {
console.error('Email template update error:', error); errorResponse(res, error, 500, 'Failed to update email template');
res.status(500).json({ error: 'Failed to update email template' });
} }
}); });
@@ -818,8 +812,7 @@ router.post('/templates', [
return res.status(201).json({ template_key: templateKey, id: templateId }); return res.status(201).json({ template_key: templateKey, id: templateId });
} catch (error) { } catch (error) {
console.error('Email template create error:', error); return errorResponse(res, error, 500, 'Failed to create email template');
return res.status(500).json({ error: 'Failed to create email template' });
} }
}); });
@@ -897,8 +890,7 @@ router.post('/templates/:key/preview', adminAuth, requirePermission('email.view'
language language
}); });
} catch (error) { } catch (error) {
console.error('Email template preview error:', error); errorResponse(res, error, 500, 'Failed to preview email template');
res.status(500).json({ error: 'Failed to preview email template' });
} }
}); });
+3 -2
View File
@@ -8,6 +8,7 @@ const { body, validationResult } = require('express-validator');
const { adminAuth } = require('../middleware/auth'); const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const eventRenameService = require('../services/eventRenameService'); const eventRenameService = require('../services/eventRenameService');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
/** /**
@@ -50,7 +51,7 @@ router.post('/:eventId/rename', adminAuth, requirePermission('events.edit'), [
data: result.data data: result.data
}); });
} catch (error) { } 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' }); 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); res.json(validation);
} catch (error) { } catch (error) {
console.error('Error validating rename:', error); logger.error('Error validating rename:', error);
res.status(500).json({ valid: false, error: 'Validation failed' }); res.status(500).json({ valid: false, error: 'Validation failed' });
} }
}); });
+22 -38
View File
@@ -18,6 +18,7 @@ const { escapeLikePattern } = require('../utils/sqlSecurity');
// formatDate import removed - dates are formatted by email processor // formatDate import removed - dates are formatted by email processor
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation'); const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse } = require('../utils/routeHelpers');
const { buildShareLinkVariants } = require('../services/shareLinkService'); const { buildShareLinkVariants } = require('../services/shareLinkService');
const { parseBooleanInput, parseStringInput } = require('../utils/parsers'); const { parseBooleanInput, parseStringInput } = require('../utils/parsers');
const eventTypeService = require('../services/eventTypeService'); const eventTypeService = require('../services/eventTypeService');
@@ -441,7 +442,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
logger.debug('Create event request body', { body: req.body }); logger.debug('Create event request body', { body: req.body });
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
console.error('Validation errors:', errors.array()); logger.error('Validation errors:', errors.array());
return res.status(400).json({ 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() created_at: new Date().toISOString()
}); });
} catch (error) { } catch (error) {
console.error('Error creating event:', error); errorResponse(res, error, 500, 'Failed to create event');
res.status(500).json({ error: 'Failed to create event' });
} }
}); });
@@ -1022,8 +1022,7 @@ router.get('/', adminAuth, requirePermission('events.view'), async (req, res) =>
} }
}); });
} catch (error) { } catch (error) {
console.error('Error fetching events:', error); errorResponse(res, error, 500, 'Failed to fetch events');
res.status(500).json({ error: 'Failed to fetch events' });
} }
}); });
@@ -1105,8 +1104,7 @@ router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res)
})), })),
})); }));
} catch (error) { } catch (error) {
console.error('Error fetching event:', error); errorResponse(res, error, 500, 'Failed to fetch event details');
res.status(500).json({ error: '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 }); res.json({ message: 'Event published successfully', is_draft: false });
} catch (error) { } catch (error) {
logger.error('Error publishing event:', { error: error.message }); errorResponse(res, error, 500, 'Failed to publish event');
res.status(500).json({ error: 'Failed to publish event' });
} }
}); });
@@ -1451,8 +1448,7 @@ router.post('/:id/duplicate', adminAuth, requirePermission('events.create'), req
is_draft: true, is_draft: true,
}); });
} catch (error) { } catch (error) {
logger.error('Error duplicating event:', { error: error.message }); errorResponse(res, error, 500, 'Failed to duplicate event');
res.status(500).json({ error: 'Failed to duplicate event' });
} }
}); });
@@ -1831,8 +1827,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
res.json({ message: 'Event updated successfully' }); res.json({ message: 'Event updated successfully' });
} catch (error) { } catch (error) {
console.error('Error updating event:', error); errorResponse(res, error, 500, 'Failed to update event');
res.status(500).json({ error: 'Failed to update event' });
} }
}); });
@@ -1891,8 +1886,7 @@ router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), r
is_active: newStatus is_active: newStatus
}); });
} catch (error) { } catch (error) {
console.error('Error toggling event status:', error); errorResponse(res, error, 500, 'Failed to toggle event status');
res.status(500).json({ error: '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) slideshow_url: await buildSlideshowUrl(event.slug, token)
}); });
} catch (error) { } catch (error) {
logger.error('Error generating slideshow link', { error: error.message }); errorResponse(res, error, 500, 'Failed to generate slideshow link');
res.status(500).json({ error: 'Failed to generate slideshow link' });
} }
}); });
@@ -1984,8 +1977,7 @@ router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit'
res.json({ show_share_token: null }); res.json({ show_share_token: null });
} catch (error) { } catch (error) {
logger.error('Error disabling slideshow link', { error: error.message }); errorResponse(res, error, 500, 'Failed to disable slideshow link');
res.status(500).json({ error: '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' show_colorfilter: updates.show_colorfilter ?? event.show_colorfilter ?? 'none'
}); });
} catch (error) { } catch (error) {
logger.error('Error updating slideshow settings', { error: error.message }); errorResponse(res, error, 500, 'Failed to update slideshow settings');
res.status(500).json({ error: 'Failed to update slideshow settings' });
} }
}); });
@@ -2125,8 +2116,7 @@ router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'),
emailSent: sendEmail emailSent: sendEmail
}); });
} catch (error) { } catch (error) {
console.error('Error resetting password:', error); errorResponse(res, error, 500, 'Failed to reset password');
res.status(500).json({ error: 'Failed to reset password' });
} }
}); });
@@ -2202,7 +2192,7 @@ router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), re
name: req.admin.username name: req.admin.username
}); });
} catch (logError) { } 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 // 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' message: 'Creation email has been queued for sending'
}); });
} catch (error) { } catch (error) {
console.error('Error resending creation email:', error); logger.error('Error resending creation email:', error);
console.error('Stack trace:', error.stack); errorResponse(res, error, 500, 'Failed to resend creation email');
res.status(500).json({ error: 'Failed to resend creation email' });
} }
}); });
@@ -2243,8 +2232,7 @@ router.post('/:id/archive', adminAuth, requirePermission('events.archive'), requ
res.json({ message: 'Event archived successfully' }); res.json({ message: 'Event archived successfully' });
} catch (error) { } catch (error) {
console.error('Error archiving event:', error); errorResponse(res, error, 500, 'Failed to archive event');
res.status(500).json({ error: 'Failed to archive event' });
} }
}); });
@@ -2297,7 +2285,7 @@ router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
name: event.event_name name: event.event_name
}); });
} catch (error) { } catch (error) {
console.error(`Failed to archive event ${event.id}:`, error); logger.error(`Failed to archive event ${event.id}:`, error);
results.failed.push({ results.failed.push({
id: event.id, id: event.id,
name: event.event_name, name: event.event_name,
@@ -2322,8 +2310,7 @@ router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
results results
}); });
} catch (error) { } catch (error) {
console.error('Error in bulk archive:', error); errorResponse(res, error, 500, 'Failed to perform bulk archive');
res.status(500).json({ error: 'Failed to perform bulk archive' });
} }
}); });
@@ -2391,8 +2378,7 @@ router.post('/bulk-delete', adminAuth, requirePermission('events.delete'), [
results results
}); });
} catch (error) { } catch (error) {
logger.error('Error in bulk delete', { error: error.message }); errorResponse(res, error, 500, 'Failed to perform bulk delete');
res.status(500).json({ error: 'Failed to perform bulk delete' });
} }
}); });
@@ -2446,8 +2432,7 @@ router.post('/:id/logo', adminAuth, requirePermission('events.edit'), requireEve
hero_logo_url: logoUrl hero_logo_url: logoUrl
}); });
} catch (error) { } catch (error) {
logger.error('Error uploading event logo:', { error: error.message, eventId: req.params.id }); errorResponse(res, error, 500, 'Failed to upload event logo');
res.status(500).json({ error: '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' }); res.json({ message: 'Event logo removed successfully' });
} catch (error) { } catch (error) {
logger.error('Error deleting event logo:', { error: error.message, eventId: req.params.id }); errorResponse(res, error, 500, 'Failed to delete event logo');
res.status(500).json({ error: 'Failed to delete event logo' });
} }
}); });
+10 -18
View File
@@ -8,6 +8,7 @@ const { requirePermission } = require('../middleware/permissions');
const { requireEventOwnership } = require('../middleware/ownership'); const { requireEventOwnership } = require('../middleware/ownership');
const feedbackService = require('../services/feedbackService'); const feedbackService = require('../services/feedbackService');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse } = require('../utils/routeHelpers');
const FRONTEND_URL = process.env.FRONTEND_URL || ''; const FRONTEND_URL = process.env.FRONTEND_URL || '';
@@ -94,8 +95,7 @@ router.get(
res.json({ guests }); res.json({ guests });
} catch (error) { } catch (error) {
logger.error('Error listing guests:', error); errorResponse(res, error, 500, 'Failed to list guests');
res.status(500).json({ error: 'Failed to list guests' });
} }
} }
); );
@@ -144,8 +144,7 @@ router.get(
})), })),
}); });
} catch (error) { } catch (error) {
logger.error('Error fetching aggregate view:', error); errorResponse(res, error, 500, 'Failed to fetch aggregate view');
res.status(500).json({ error: 'Failed to fetch aggregate view' });
} }
} }
); );
@@ -196,8 +195,7 @@ router.get(
res.json({ invites }); res.json({ invites });
} catch (error) { } catch (error) {
logger.error('Error listing invites:', error); errorResponse(res, error, 500, 'Failed to list invites');
res.status(500).json({ error: 'Failed to list invites' });
} }
} }
); );
@@ -266,8 +264,7 @@ router.post(
}, },
}); });
} catch (error) { } catch (error) {
logger.error('Error creating invite:', error); errorResponse(res, error, 500, 'Failed to create invite');
res.status(500).json({ error: 'Failed to create invite' });
} }
} }
); );
@@ -302,8 +299,7 @@ router.delete(
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logger.error('Error revoking invite:', error); errorResponse(res, error, 500, 'Failed to revoke invite');
res.status(500).json({ error: 'Failed to revoke invite' });
} }
} }
); );
@@ -457,8 +453,7 @@ router.get(
selections, selections,
}); });
} catch (error) { } catch (error) {
logger.error('Error fetching guest detail:', error); errorResponse(res, error, 500, 'Failed to fetch guest detail');
res.status(500).json({ error: 'Failed to fetch guest detail' });
} }
} }
); );
@@ -509,8 +504,7 @@ router.get(
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
return res.send(selections.map((s) => s.original_filename || s.filename).join('\n')); return res.send(selections.map((s) => s.original_filename || s.filename).join('\n'));
} catch (error) { } catch (error) {
logger.error('Error exporting guest:', error); errorResponse(res, error, 500, 'Failed to export guest');
res.status(500).json({ error: 'Failed to export guest' });
} }
} }
); );
@@ -548,8 +542,7 @@ router.delete(
res.json({ success: true, ...result }); res.json({ success: true, ...result });
} catch (error) { } catch (error) {
logger.error('Error deleting guest:', error); errorResponse(res, error, 500, 'Failed to delete guest');
res.status(500).json({ error: 'Failed to delete guest' });
} }
} }
); );
@@ -600,8 +593,7 @@ router.post(
res.json({ success: true, ...result }); res.json({ success: true, ...result });
} catch (error) { } catch (error) {
logger.error('Error merging guests:', error); errorResponse(res, error, 500, 'Failed to merge guests');
res.status(500).json({ error: 'Failed to merge guests' });
} }
} }
); );
+6 -5
View File
@@ -2,6 +2,7 @@ const express = require('express');
const { db, logActivity } = require('../database/db'); const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth'); const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get notifications (unread activity logs) // 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; if (typeof notification.metadata === 'object') return notification.metadata;
return JSON.parse(notification.metadata); return JSON.parse(notification.metadata);
} catch (e) { } 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 {}; return {};
} }
})(), })(),
@@ -59,7 +60,7 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res)
unreadCount: unreadCount.count || 0 unreadCount: unreadCount.count || 0
}); });
} catch (error) { } catch (error) {
console.error('Notifications fetch error:', error); logger.error('Notifications fetch error:', error);
res.status(500).json({ error: 'Failed to fetch notifications' }); 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' }); res.json({ message: 'Notification marked as read' });
} catch (error) { } 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' }); 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' }); res.json({ message: 'All notifications marked as read' });
} catch (error) { } 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' }); 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(); const deletedCount = await db('activity_logs').delete();
res.json({ message: 'All notifications cleared', deletedCount }); res.json({ message: 'All notifications cleared', deletedCount });
} catch (error) { } catch (error) {
console.error('Clear notifications error:', error); logger.error('Clear notifications error:', error);
res.status(500).json({ error: 'Failed to clear notifications' }); res.status(500).json({ error: 'Failed to clear notifications' });
} }
}); });
+6 -5
View File
@@ -11,7 +11,9 @@ const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { requireEventOwnership } = require('../middleware/ownership'); const { requireEventOwnership } = require('../middleware/ownership');
const { PhotoFilterBuilder } = require('../utils/photoFilterBuilder'); const { PhotoFilterBuilder } = require('../utils/photoFilterBuilder');
const { getPagination } = require('../utils/routeHelpers');
const { PhotoExportService } = require('../services/photoExportService'); const { PhotoExportService } = require('../services/photoExportService');
const logger = require('../utils/logger');
const exportService = new PhotoExportService(); const exportService = new PhotoExportService();
@@ -66,8 +68,7 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), re
const sort = req.query.sort || 'date'; const sort = req.query.sort || 'date';
const order = req.query.order || 'desc'; const order = req.query.order || 'desc';
const page = parseInt(req.query.page) || 1; const { page, limit } = getPagination(req, { limit: 50 });
const limit = parseInt(req.query.limit) || 50;
// Build filtered query // Build filtered query
const filterBuilder = new PhotoFilterBuilder( const filterBuilder = new PhotoFilterBuilder(
@@ -124,7 +125,7 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), re
} }
}); });
} catch (error) { } catch (error) {
console.error('Filter photos error:', error); logger.error('Filter photos error:', error);
res.status(500).json({ error: 'Failed to filter photos' }); res.status(500).json({ error: 'Failed to filter photos' });
} }
}); });
@@ -146,7 +147,7 @@ router.get('/:eventId/filter-summary', adminAuth, requirePermission('photos.view
data: summary data: summary
}); });
} catch (error) { } catch (error) {
console.error('Filter summary error:', error); logger.error('Filter summary error:', error);
res.status(500).json({ error: 'Failed to get filter summary' }); 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); res.send(result.content);
} }
} catch (error) { } 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' }); res.status(500).json({ error: error.message || 'Failed to export photos' });
} }
}); });
+47 -61
View File
@@ -22,6 +22,8 @@ const downloadZipService = require('../services/downloadZipService');
const { findReplacementCandidate, replacePhoto } = require('../services/photoReplacementService'); const { findReplacementCandidate, replacePhoto } = require('../services/photoReplacementService');
const { requireEventOwnership } = require('../middleware/ownership'); const { requireEventOwnership } = require('../middleware/ownership');
const { getStorage } = require('../services/storage'); const { getStorage } = require('../services/storage');
const { errorResponse } = require('../utils/routeHelpers');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get storage path from environment or default // 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 // IMPORTANT: Using synchronous functions to prevent file corruption
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: (req, file, cb) => { 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; const { eventId } = req.params;
// We'll validate the event exists in the route handler // We'll validate the event exists in the route handler
@@ -40,7 +42,7 @@ const storage = multer.diskStorage({
// Create directory synchronously // Create directory synchronously
require('fs').mkdirSync(tempPath, { recursive: true }); require('fs').mkdirSync(tempPath, { recursive: true });
console.log('Temp destination path:', tempPath); logger.info('Temp destination path:', tempPath);
// Store temp path for cleanup // Store temp path for cleanup
req.tempUploadPath = tempPath; req.tempUploadPath = tempPath;
@@ -48,10 +50,10 @@ const storage = multer.diskStorage({
cb(null, tempPath); cb(null, tempPath);
}, },
filename: (req, file, cb) => { 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 // Use a simple temporary filename
const tempName = `temp_${Date.now()}_${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`; 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); cb(null, tempName);
} }
}); });
@@ -89,7 +91,7 @@ const resolveAllowedTypes = async (req, res, next) => {
try { try {
req.allowedMimeTypes = await getAllowedMimeTypes(); req.allowedMimeTypes = await getAllowedMimeTypes();
} catch (error) { } 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']; req.allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
} }
next(); next();
@@ -111,7 +113,7 @@ const uploadTimeout = (timeout = 300000) => { // 5 minutes default
return (req, res, next) => { return (req, res, next) => {
// Set timeout for the request // Set timeout for the request
req.setTimeout(timeout, () => { req.setTimeout(timeout, () => {
console.error('Upload request timed out'); logger.error('Upload request timed out');
if (!res.headersSent) { if (!res.headersSent) {
res.status(408).json({ error: 'Upload request timed out' }); 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 // Set response timeout as well
res.setTimeout(timeout, () => { res.setTimeout(timeout, () => {
console.error('Upload response timed out'); logger.error('Upload response timed out');
}); });
next(); next();
@@ -133,13 +135,12 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r
try { try {
maxFilesPerUpload = await getMaxFilesPerUpload(); maxFilesPerUpload = await getMaxFilesPerUpload();
} catch (error) { } catch (error) {
console.error('Failed to resolve max files per upload:', error); return errorResponse(res, error, 500, 'Unable to determine upload limits');
return res.status(500).json({ error: 'Unable to determine upload limits' });
} }
upload.array('photos', maxFilesPerUpload)(req, res, (err) => { upload.array('photos', maxFilesPerUpload)(req, res, (err) => {
if (err) { if (err) {
console.error('Multer error:', err); logger.error('Multer error:', err);
if (err instanceof multer.MulterError) { if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') { if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large. Maximum size is 10GB per file.' }); 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 { try {
await fs.rm(req.tempUploadPath, { recursive: true, force: true }); await fs.rm(req.tempUploadPath, { recursive: true, force: true });
} catch (e) { } 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); 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 { category_id, replace_by_name } = req.body;
const replaceByName = replace_by_name === 'true' || replace_by_name === true; const replaceByName = replace_by_name === 'true' || replace_by_name === true;
console.log('Upload request received for event:', eventId); logger.info('Upload request received for event:', eventId);
console.log('Body:', req.body); logger.info('Body:', req.body);
console.log('Files:', req.files ? req.files.length : 'none'); logger.info('Files:', req.files ? req.files.length : 'none');
console.log('File details:', req.files?.map(f => ({ name: f.originalname, size: f.size, mimetype: f.mimetype }))); logger.info('File details:', req.files?.map(f => ({ name: f.originalname, size: f.size, mimetype: f.mimetype })));
console.log('Category ID received:', category_id); logger.info('Category ID received:', category_id);
// Verify event exists and admin has access // Verify event exists and admin has access
const event = await db('events').where({ id: eventId }).first(); const event = await db('events').where({ id: eventId }).first();
if (!event) { if (!event) {
console.error('Event not found:', eventId); logger.error('Event not found:', eventId);
return res.status(404).json({ error: 'Event not found' }); 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) { if (!req.files || req.files.length === 0) {
console.error('No files in request. req.files:', req.files); logger.error('No files in request. req.files:', req.files);
console.error('Request body keys:', Object.keys(req.body)); logger.error('Request body keys:', Object.keys(req.body));
return res.status(400).json({ error: 'No files uploaded' }); 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, category_id: parsedCategoryId,
}); });
} catch (err) { } 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 }); 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. // 202 Accepted — files stored, processing happens in background.
res.status(202).json(response); res.status(202).json(response);
} catch (error) { } catch (error) {
console.error('Error uploading photos:', error);
// Temp directory cleanup is handled by the response finish/close // Temp directory cleanup is handled by the response finish/close
// listeners above, regardless of which exit path fires. // 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), ...summariseUpload(group.photos),
}); });
} catch (error) { } catch (error) {
console.error('Error reading upload status:', error); errorResponse(res, error, 500, 'Failed to read upload status');
res.status(500).json({ error: 'Failed to read upload status' });
} }
} }
); );
@@ -575,7 +574,7 @@ router.get(
return; return;
} }
} catch (e) { } 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' }); res.json({ id: photo.id, status: 'pending' });
} catch (error) { } catch (error) {
console.error('Error retrying photo processing:', error); errorResponse(res, error, 500, 'Failed to retry photo processing');
res.status(500).json({ error: 'Failed to retry photo processing' });
} }
} }
); );
@@ -651,7 +649,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.
const originalKey = resolvePhotoStorageKey(event, photo); const originalKey = resolvePhotoStorageKey(event, photo);
if (originalKey) await storage.delete(originalKey); if (originalKey) await storage.delete(originalKey);
} catch (error) { } 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 // photo.thumbnail_path is stored as the canonical storage key
@@ -660,7 +658,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.
try { try {
await storage.delete(photo.thumbnail_path); await storage.delete(photo.thumbnail_path);
} catch (error) { } catch (error) {
console.error('Error deleting thumbnail:', error); logger.error('Error deleting thumbnail:', error);
} }
} }
if (photo.hero_path) { if (photo.hero_path) {
@@ -700,8 +698,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.
downloadZipService.invalidate(parseInt(eventId)); downloadZipService.invalidate(parseInt(eventId));
res.json({ message: 'Photo deleted successfully' }); res.json({ message: 'Photo deleted successfully' });
} catch (error) { } catch (error) {
console.error('Error deleting photo:', error); errorResponse(res, error, 500, 'Failed to delete photo');
res.status(500).json({ error: 'Failed to delete photo' });
} }
}); });
@@ -763,8 +760,7 @@ router.patch('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.e
photo: updatedPhoto photo: updatedPhoto
}); });
} catch (error) { } catch (error) {
console.error('Error updating photo:', error); errorResponse(res, error, 500, 'Failed to update photo');
res.status(500).json({ error: 'Failed to update photo' });
} }
}); });
@@ -797,7 +793,7 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, requirePermission('photos
const originalKey = resolvePhotoStorageKey(event, photo); const originalKey = resolvePhotoStorageKey(event, photo);
if (originalKey) await storage.delete(originalKey); if (originalKey) await storage.delete(originalKey);
} catch (error) { } catch (error) {
console.error('Error deleting photo file:', error); logger.error('Error deleting photo file:', error);
} }
if (photo.thumbnail_path) { if (photo.thumbnail_path) {
@@ -842,8 +838,7 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, requirePermission('photos
downloadZipService.invalidate(parseInt(eventId)); downloadZipService.invalidate(parseInt(eventId));
res.json({ message: `${photos.length} photos deleted successfully` }); res.json({ message: `${photos.length} photos deleted successfully` });
} catch (error) { } catch (error) {
console.error('Error bulk deleting photos:', error); errorResponse(res, error, 500, 'Failed to delete photos');
res.status(500).json({ error: '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` }); res.json({ message: `${photoIds.length} photos updated successfully` });
} catch (error) { } catch (error) {
console.error('Error bulk updating photos:', error); errorResponse(res, error, 500, 'Failed to update photos');
res.status(500).json({ error: 'Failed to update photos' });
} }
}); });
@@ -961,8 +955,7 @@ router.get('/:eventId/photos/:photoId/download', adminAuth, requirePermission('p
}); });
res.sendFile(filePath); res.sendFile(filePath);
} catch (error) { } catch (error) {
console.error('Error downloading photo:', error); errorResponse(res, error, 500, 'Failed to download photo');
res.status(500).json({ error: 'Failed to download photo' });
} }
}); });
@@ -1096,8 +1089,7 @@ router.get('/:eventId/photos', adminAuth, requirePermission('photos.view'), requ
})) }))
}); });
} catch (error) { } catch (error) {
console.error('Error fetching photos:', error); errorResponse(res, error, 500, 'Failed to fetch photos');
res.status(500).json({ error: 'Failed to fetch photos' });
} }
}); });
@@ -1153,8 +1145,7 @@ router.get('/:eventId/photo/:photoId', adminAuth, requirePermission('photos.view
} }
res.sendFile(path.resolve(filePath)); res.sendFile(path.resolve(filePath));
} catch (error) { } catch (error) {
console.error('Error serving photo:', error); errorResponse(res, error, 500, 'Failed to serve photo');
res.status(500).json({ error: 'Failed to serve photo' });
} }
}); });
@@ -1168,7 +1159,7 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, requirePermission('photos.
.first(); .first();
if (!photo) { 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' }); 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); const thumbnailPath = await ensureThumbnail(photo);
if (!thumbnailPath) { 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' }); 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); const stream = await storage.get(thumbnailPath);
stream.pipe(res); stream.pipe(res);
} catch (error) { } catch (error) {
console.error('Error serving thumbnail:', error); logger.error('Error serving thumbnail:', error);
console.error('Photo ID:', req.params.photoId); logger.error('Photo ID:', req.params.photoId);
console.error('Event ID:', req.params.eventId); errorResponse(res, error, 500, 'Failed to serve thumbnail');
res.status(500).json({ error: 'Failed to serve thumbnail' });
} }
}); });
@@ -1232,8 +1222,7 @@ router.get('/:eventId/debug', adminAuth, requirePermission('photos.view'), requi
storagePath: getStoragePath() storagePath: getStoragePath()
}); });
} catch (error) { } catch (error) {
console.error('Error fetching admin photo debug data:', error); errorResponse(res, error, 500, 'Failed to fetch photo debug data');
res.status(500).json({ error: 'Failed to fetch photo debug data' });
} }
}); });
@@ -1275,8 +1264,7 @@ router.post('/:eventId/chunked-upload/init', adminAuth, requirePermission('photo
res.json(result); res.json(result);
} catch (error) { } catch (error) {
console.error('Error initializing chunked upload:', error); errorResponse(res, error, 500, 'Failed to initialize upload');
res.status(500).json({ error: 'Failed to initialize upload' });
} }
}); });
@@ -1296,7 +1284,7 @@ router.post('/:eventId/chunked-upload/:uploadId/chunk/:chunkIndex', adminAuth, r
res.json(result); res.json(result);
} catch (error) { } 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' }); 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 { try {
await fs.rm(mergedFile.tempDir, { recursive: true, force: true }); await fs.rm(mergedFile.tempDir, { recursive: true, force: true });
} catch (cleanupErr) { } catch (cleanupErr) {
console.warn('Failed to clean up temp directory:', cleanupErr.message); logger.warn('Failed to clean up temp directory:', cleanupErr.message);
} }
res.json({ res.json({
@@ -1338,7 +1326,7 @@ router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, requirePer
photos: uploadedPhotos photos: uploadedPhotos
}); });
} catch (error) { } 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' }); 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); res.json(status);
} catch (error) { } catch (error) {
console.error('Error getting upload status:', error); errorResponse(res, error, 500, 'Failed to get upload status');
res.status(500).json({ error: 'Failed to get upload status' });
} }
}); });
@@ -1370,8 +1357,7 @@ router.delete('/:eventId/chunked-upload/:uploadId', adminAuth, requirePermission
res.json({ success: true, message: 'Upload aborted' }); res.json({ success: true, message: 'Upload aborted' });
} catch (error) { } catch (error) {
console.error('Error aborting upload:', error); errorResponse(res, error, 500, 'Failed to abort upload');
res.status(500).json({ error: 'Failed to abort upload' });
} }
}); });
+2 -1
View File
@@ -5,6 +5,7 @@ const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions'); const { requirePermission } = require('../middleware/permissions');
const { body, query, validationResult } = require('express-validator'); const { body, query, validationResult } = require('express-validator');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { getPagination } = require('../utils/routeHelpers');
const { db } = require('../database/db'); const { db } = require('../database/db');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
@@ -48,7 +49,7 @@ function transformS3Config(body) {
*/ */
router.get('/status', requirePermission('backup.view'), async (req, res) => { router.get('/status', requirePermission('backup.view'), async (req, res) => {
try { try {
const limit = parseInt(req.query.limit) || 10; const { limit } = getPagination(req, { limit: 10 });
const history = await restoreService.getRestoreHistory(limit); const history = await restoreService.getRestoreHistory(limit);
const status = { const status = {
+42 -61
View File
@@ -22,6 +22,8 @@ const { sanitizeCss } = require('../utils/cssSanitizer');
const { upsertAppSetting } = require('../utils/appSettings'); const { upsertAppSetting } = require('../utils/appSettings');
const { clearShareLinkSettingsCache } = require('../services/shareLinkService'); const { clearShareLinkSettingsCache } = require('../services/shareLinkService');
const { resetSecurityConfigCache } = require('../utils/authSecurity'); const { resetSecurityConfigCache } = require('../utils/authSecurity');
const { errorResponse } = require('../utils/routeHelpers');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD } = require('../services/uploadSettings'); const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD } = require('../services/uploadSettings');
const watermarkService = require('../services/watermarkService'); const watermarkService = require('../services/watermarkService');
@@ -158,8 +160,7 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res)
res.json(settingsObject); res.json(settingsObject);
} catch (error) { } catch (error) {
console.error('Settings fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch settings');
res.status(500).json({ error: 'Failed to fetch settings' });
} }
}); });
@@ -200,8 +201,7 @@ router.get('/customer-surface', adminAuth, requirePermission('settings.view'), a
res.json(settings); res.json(settings);
} catch (error) { } catch (error) {
console.error('Customer surface settings fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch customer surface settings');
res.status(500).json({ error: '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) }); res.json({ message: 'Customer surface settings updated', updated: updates.map((u) => u.setting_key) });
} catch (error) { } catch (error) {
console.error('Customer surface settings save error:', error); errorResponse(res, error, 500, 'Failed to save customer surface settings');
res.status(500).json({ error: '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) }); res.json({ message: 'Accounting settings updated', updated: updates.map((u) => u.setting_key) });
} catch (error) { } catch (error) {
console.error('Accounting settings save error:', error); errorResponse(res, error, 500, 'Failed to save accounting settings');
res.status(500).json({ error: 'Failed to save accounting settings' });
} }
}); });
@@ -360,8 +358,7 @@ router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (r
require('../utils/slideshowGlobals').invalidateSlideshowGlobals(); require('../utils/slideshowGlobals').invalidateSlideshowGlobals();
res.json({ message: 'Slideshow settings updated', updated: updates.map((u) => u.setting_key) }); res.json({ message: 'Slideshow settings updated', updated: updates.map((u) => u.setting_key) });
} catch (error) { } catch (error) {
console.error('Slideshow settings save error:', error); errorResponse(res, error, 500, 'Failed to save slideshow settings');
res.status(500).json({ error: 'Failed to save slideshow settings' });
} }
}); });
@@ -413,8 +410,7 @@ router.get('/:type', adminAuth, requirePermission('settings.view'), async (req,
res.json(settingsObject); res.json(settingsObject);
} catch (error) { } catch (error) {
console.error('Settings fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch settings');
res.status(500).json({ error: 'Failed to fetch settings' });
} }
}); });
@@ -434,8 +430,7 @@ router.get('/password/complexity', adminAuth, requirePermission('settings.view')
config config
}); });
} catch (error) { } catch (error) {
console.error('Password complexity settings fetch error:', error); errorResponse(res, error, 500, 'Failed to fetch password complexity settings');
res.status(500).json({ error: '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); const faviconPath = path.join(getStoragePath(), relativePath);
try { try {
await fs.unlink(faviconPath); await fs.unlink(faviconPath);
console.log('Deleted favicon file:', faviconPath); logger.info('Deleted favicon file:', faviconPath);
} catch (err) { } 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); const logoPath = path.join(getStoragePath(), relativePath);
try { try {
await fs.unlink(logoPath); await fs.unlink(logoPath);
console.log('Deleted logo file:', logoPath); logger.info('Deleted logo file:', logoPath);
} catch (err) { } 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) { if (currentSettings && currentSettings.enabled) {
// Start background regeneration of all watermarks // Start background regeneration of all watermarks
console.log('Watermark settings changed, starting background regeneration'); logger.info('Watermark settings changed, starting background regeneration');
watermarkGeneratorService.regenerateAll() watermarkGeneratorService.regenerateAll()
.then(result => { .then(result => {
console.log(`Watermark regeneration completed: ${result.success}/${result.total} successful`); logger.info(`Watermark regeneration completed: ${result.success}/${result.total} successful`);
}) })
.catch(err => { .catch(err => {
console.error('Watermark regeneration failed:', err); logger.error('Watermark regeneration failed:', err);
}); });
watermarkRegenerationStarted = true; watermarkRegenerationStarted = true;
} else { } else {
// Watermarking was disabled, clear all pre-generated watermarks // 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() 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 watermarkRegenerationStarted
}); });
} catch (error) { } catch (error) {
console.error('Branding update error:', error); errorResponse(res, error, 500, 'Failed to update branding settings');
res.status(500).json({ error: 'Failed to update branding settings' });
} }
}); });
@@ -711,7 +705,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
} }
await fs.unlink(oldPath); await fs.unlink(oldPath);
} catch (error) { } 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 logoUrl: publicPath
}); });
} catch (error) { } catch (error) {
console.error('Logo upload error:', error); errorResponse(res, error, 500, 'Failed to upload logo');
res.status(500).json({ error: 'Failed to upload logo' });
} }
}); });
@@ -772,7 +765,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req
if (p.startsWith('"')) p = JSON.parse(p); if (p.startsWith('"')) p = JSON.parse(p);
await fs.unlink(p); await fs.unlink(p);
} catch (error) { } catch (error) {
console.error('Failed to delete logo file:', error); logger.error('Failed to delete logo file:', error);
} }
} }
await db('app_settings') await db('app_settings')
@@ -781,8 +774,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req
res.json({ message: 'Logo removed' }); res.json({ message: 'Logo removed' });
} catch (error) { } catch (error) {
console.error('Logo delete error:', error); errorResponse(res, error, 500, 'Failed to remove logo');
res.status(500).json({ error: 'Failed to remove logo' });
} }
}); });
@@ -812,7 +804,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e
try { try {
await fs.unlink(oldPath); await fs.unlink(oldPath);
} catch (error) { } 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; let watermarkRegenerationStarted = false;
if (currentSettings && currentSettings.enabled) { if (currentSettings && currentSettings.enabled) {
console.log('Watermark logo changed, starting background regeneration'); logger.info('Watermark logo changed, starting background regeneration');
watermarkGeneratorService.regenerateAll() watermarkGeneratorService.regenerateAll()
.then(result => { .then(result => {
console.log(`Watermark regeneration completed: ${result.success}/${result.total} successful`); logger.info(`Watermark regeneration completed: ${result.success}/${result.total} successful`);
}) })
.catch(err => { .catch(err => {
console.error('Watermark regeneration failed:', err); logger.error('Watermark regeneration failed:', err);
}); });
watermarkRegenerationStarted = true; watermarkRegenerationStarted = true;
} }
@@ -871,8 +863,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e
watermarkRegenerationStarted watermarkRegenerationStarted
}); });
} catch (error) { } catch (error) {
console.error('Watermark logo upload error:', error); errorResponse(res, error, 500, 'Failed to upload watermark logo');
res.status(500).json({ error: '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' }); res.json({ message: 'Theme settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Theme update error:', error); errorResponse(res, error, 500, 'Failed to update theme settings');
res.status(500).json({ error: 'Failed to update theme settings' });
} }
}); });
@@ -1005,7 +995,7 @@ router.put('/general', adminAuth, requirePermission('settings.edit'), async (req
require('../services/downloadFilenameService').clearCache(); require('../services/downloadFilenameService').clearCache();
require('../services/downloadZipService').invalidateAll(); require('../services/downloadZipService').invalidateAll();
} catch (e) { } 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' }); res.json({ message: 'General settings updated successfully' });
} catch (error) { } catch (error) {
console.error('General settings update error:', error); errorResponse(res, error, 500, 'Failed to update general settings');
res.status(500).json({ error: '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' }); res.json({ message: 'Security settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Security settings update error:', error); errorResponse(res, error, 500, 'Failed to update security settings');
res.status(500).json({ error: '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' }); res.json({ message: 'Analytics settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Analytics settings update error:', error); errorResponse(res, error, 500, 'Failed to update analytics settings');
res.status(500).json({ error: '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' }); res.json({ message: 'SEO settings updated successfully' });
} catch (error) { } catch (error) {
console.error('SEO settings update error:', error); errorResponse(res, error, 500, 'Failed to update SEO settings');
res.status(500).json({ error: 'Failed to update SEO settings' });
} }
}); });
@@ -1216,7 +1202,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async
const stats = await fs.stat(fullArchivePath); const stats = await fs.stat(fullArchivePath);
archiveStorage += stats.size; archiveStorage += stats.size;
} catch (error) { } 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); rawDiskFree = Number(diskStats.bsize) * Number(diskStats.bfree);
rawDiskAvailable = Number(diskStats.bsize) * Number(diskStats.bavail); rawDiskAvailable = Number(diskStats.bsize) * Number(diskStats.bavail);
} catch (diskError) { } catch (diskError) {
console.error('Disk stats error:', diskError.message); logger.error('Disk stats error:', diskError.message);
} }
const clampDiskValue = (value) => { const clampDiskValue = (value) => {
@@ -1333,7 +1319,7 @@ router.get('/storage/info', adminAuth, requirePermission('settings.view'), async
} }
}); });
} catch (error) { } 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'); 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 disk_override_source: overrideSource
}); });
} catch (error) { } catch (error) {
console.error('Storage info error:', error); errorResponse(res, error, 500, 'Failed to fetch storage information');
res.status(500).json({ error: 'Failed to fetch storage information' });
} }
}); });
@@ -1445,8 +1430,7 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp
res.json({ faviconUrl }); res.json({ faviconUrl });
} catch (error) { } catch (error) {
console.error('Error uploading favicon:', error); errorResponse(res, error, 500, 'Failed to upload favicon');
res.status(500).json({ error: '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' }); res.json({ message: 'Rate limit settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Rate limit settings update error:', error); errorResponse(res, error, 500, 'Failed to update rate limit settings');
res.status(500).json({ error: 'Failed to update rate limit settings' });
} }
}); });
@@ -1530,8 +1513,7 @@ router.get('/public-site/default', adminAuth, requirePermission('settings.view')
} }
}); });
} catch (error) { } catch (error) {
console.error('Failed to load public site defaults:', error); errorResponse(res, error, 500, 'Failed to load defaults');
res.status(500).json({ error: 'Failed to load defaults' });
} }
}); });
@@ -1584,8 +1566,7 @@ router.post('/public-site/reset', adminAuth, requirePermission('settings.edit'),
branding: defaults.branding branding: defaults.branding
}); });
} catch (error) { } catch (error) {
console.error('Failed to reset public site template:', error); errorResponse(res, error, 500, 'Failed to reset template');
res.status(500).json({ error: 'Failed to reset template' });
} }
}); });
+7 -7
View File
@@ -29,7 +29,7 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req
const packageJson = JSON.parse(packageContent); const packageJson = JSON.parse(packageContent);
backendVersion = packageJson.version || '1.0.0'; backendVersion = packageJson.version || '1.0.0';
} catch (err) { } catch (err) {
console.error('Could not read package.json:', err); logger.error('Could not read package.json:', err);
} }
const channel = getCurrentChannel(backendVersion); const channel = getCurrentChannel(backendVersion);
@@ -42,7 +42,7 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req
channel: channel channel: channel
}); });
} catch (error) { } catch (error) {
console.error('Error fetching version:', error); logger.error('Error fetching version:', error);
res.status(500).json({ error: 'Failed to fetch version information' }); res.status(500).json({ error: 'Failed to fetch version information' });
} }
}); });
@@ -231,7 +231,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req,
`, [dbName]); `, [dbName]);
dbSize = result.rows[0]?.size || 0; dbSize = result.rows[0]?.size || 0;
} catch (error) { } catch (error) {
console.error('Error getting PostgreSQL database size:', error); logger.error('Error getting PostgreSQL database size:', error);
} }
} else { } else {
// SQLite - check file size // SQLite - check file size
@@ -240,7 +240,7 @@ router.get('/status', adminAuth, requirePermission('settings.view'), async (req,
const stats = await fs.stat(dbPath); const stats = await fs.stat(dbPath);
dbSize = stats.size; dbSize = stats.size;
} catch (error) { } 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); const stats = await fs.stat(fullArchivePath);
archiveStorage += stats.size; archiveStorage += stats.size;
} catch (error) { } 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); res.json(status);
} catch (error) { } 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' }); 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() timestamp: new Date()
}); });
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to fetch database information' });
} }
}); });
+8 -14
View File
@@ -15,6 +15,7 @@ const {
const { endSession } = require('../middleware/sessionTimeout'); const { endSession } = require('../middleware/sessionTimeout');
const { revokeToken } = require('../utils/tokenRevocation'); const { revokeToken } = require('../utils/tokenRevocation');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse } = require('../utils/routeHelpers');
const { const {
setAdminAuthCookie, setAdminAuthCookie,
clearAdminAuthCookie, clearAdminAuthCookie,
@@ -132,8 +133,7 @@ router.post('/admin/login', [
} }
}); });
} catch (error) { } catch (error) {
logger.error('Login error:', error); errorResponse(res, error, 500, 'Login failed');
res.status(500).json({ error: 'Login failed' });
} }
}); });
@@ -175,8 +175,7 @@ router.post('/logout', async (req, res) => {
res.json({ message: 'Logged out successfully' }); res.json({ message: 'Logged out successfully' });
} catch (error) { } catch (error) {
logger.error('Logout error:', error); errorResponse(res, error, 500, 'Logout failed');
res.status(500).json({ error: 'Logout failed' });
} }
}); });
@@ -288,8 +287,7 @@ router.post('/gallery/verify', [
} }
}); });
} catch (error) { } catch (error) {
logger.error('Gallery verification error:', error); errorResponse(res, error, 500, 'Verification failed');
res.status(500).json({ error: 'Verification failed' });
} }
}); });
@@ -364,8 +362,7 @@ router.post('/gallery/:slug/client-login', [
accessLevel: 'client' accessLevel: 'client'
}); });
} catch (error) { } catch (error) {
logger.error('Client login error:', error); errorResponse(res, error, 500, 'Authentication failed');
res.status(500).json({ error: 'Authentication failed' });
} }
}); });
@@ -451,8 +448,7 @@ router.post('/gallery/share-login', [
} }
}); });
} catch (error) { } catch (error) {
logger.error('Share link authentication error:', error); errorResponse(res, error, 500, 'Share link login failed');
res.status(500).json({ error: 'Share link login failed' });
} }
}); });
@@ -467,8 +463,7 @@ router.post('/gallery/logout', async (req, res) => {
clearGalleryAuthCookies(res, slug); clearGalleryAuthCookies(res, slug);
res.json({ message: 'Logged out successfully' }); res.json({ message: 'Logged out successfully' });
} catch (error) { } catch (error) {
logger.error('Gallery logout error:', error); errorResponse(res, error, 500, 'Logout failed');
res.status(500).json({ error: 'Logout failed' });
} }
}); });
@@ -682,8 +677,7 @@ router.post('/admin/change-password', [
score: passwordValidation.score score: passwordValidation.score
}); });
} catch (error) { } catch (error) {
logger.error('Password change error:', error); errorResponse(res, error, 500, 'Failed to change password');
res.status(500).json({ error: 'Failed to change password' });
} }
}); });
+12 -22
View File
@@ -19,6 +19,7 @@ const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat'); const { formatBoolean } = require('../utils/dbCompat');
const { getBcryptRounds } = require('../utils/passwordValidation'); const { getBcryptRounds } = require('../utils/passwordValidation');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { errorResponse } = require('../utils/routeHelpers');
const { getClientIp } = require('../utils/requestIp'); const { getClientIp } = require('../utils/requestIp');
const { customerAuth } = require('../middleware/customerAuth'); const { customerAuth } = require('../middleware/customerAuth');
const { setGalleryAuthCookies } = require('../utils/tokenUtils'); const { setGalleryAuthCookies } = require('../utils/tokenUtils');
@@ -117,8 +118,7 @@ router.get('/events', customerAuth, async (req, res) => {
})), })),
}); });
} catch (error) { } catch (error) {
logger.error('Customer event list error:', error); errorResponse(res, error, 500, 'Failed to load events');
res.status(500).json({ error: 'Failed to load events' });
} }
}); });
@@ -225,8 +225,7 @@ router.get('/events/:slug/access-token', [
}, },
}); });
} catch (error) { } catch (error) {
logger.error('Customer access-token exchange error:', error); errorResponse(res, error, 500, 'Failed to issue access token');
res.status(500).json({ error: 'Failed to issue access token' });
} }
}); });
@@ -248,8 +247,7 @@ router.get('/profile', customerAuth, async (req, res) => {
} }
res.json({ profile: shapeProfile(row) }); res.json({ profile: shapeProfile(row) });
} catch (error) { } catch (error) {
logger.error('Customer profile read error:', error); errorResponse(res, error, 500, 'Failed to load profile');
res.status(500).json({ error: 'Failed to load profile' });
} }
}); });
@@ -316,8 +314,7 @@ router.put('/profile', [
res.json({ profile: shapeProfile(row) }); res.json({ profile: shapeProfile(row) });
} catch (error) { } catch (error) {
logger.error('Customer profile update error:', error); errorResponse(res, error, 500, 'Failed to update profile');
res.status(500).json({ error: 'Failed to update profile' });
} }
}); });
@@ -376,8 +373,7 @@ router.post('/profile/password', [
res.json({ message: 'Password updated' }); res.json({ message: 'Password updated' });
} catch (error) { } catch (error) {
logger.error('Customer password change error:', error); errorResponse(res, error, 500, 'Failed to change password');
res.status(500).json({ error: 'Failed to change password' });
} }
}); });
@@ -457,8 +453,7 @@ router.get('/quotes', customerAuth, async (req, res) => {
})), })),
}); });
} catch (error) { } catch (error) {
logger.error('Customer quotes list error:', error); errorResponse(res, error, 500, 'Failed to load quotes');
res.status(500).json({ error: 'Failed to load quotes' });
} }
}); });
@@ -545,8 +540,7 @@ router.get('/invoices', customerAuth, async (req, res) => {
})), })),
}); });
} catch (error) { } catch (error) {
logger.error('Customer invoice list error:', error); errorResponse(res, error, 500, 'Failed to load invoices');
res.status(500).json({ error: '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.set('Content-Disposition', `inline; filename="${filename}"`);
res.send(buf); res.send(buf);
} catch (error) { } catch (error) {
logger.error('Customer quote PDF error:', error); errorResponse(res, error, 500, 'Failed to render quote PDF');
res.status(500).json({ error: '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.set('Content-Disposition', `inline; filename="${filename}"`);
res.send(buf); res.send(buf);
} catch (error) { } catch (error) {
logger.error('Customer invoice PDF error:', error); errorResponse(res, error, 500, 'Failed to render invoice PDF');
res.status(500).json({ error: 'Failed to render invoice PDF' });
} }
}); });
@@ -679,8 +671,7 @@ router.get('/contracts', customerAuth, async (req, res) => {
})), })),
}); });
} catch (error) { } catch (error) {
logger.error('Customer contracts list error:', error); errorResponse(res, error, 500, 'Failed to load contracts');
res.status(500).json({ error: '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)}"`); res.set('Content-Disposition', `inline; filename="${path.basename(filePath)}"`);
fs.createReadStream(filePath).pipe(res); fs.createReadStream(filePath).pipe(res);
} catch (error) { } catch (error) {
logger.error('Customer contract PDF error:', error); errorResponse(res, error, 500, 'Failed to render contract PDF');
res.status(500).json({ error: 'Failed to render contract PDF' });
} }
}); });
+3 -2
View File
@@ -14,6 +14,7 @@ const { buildShareLinkVariants } = require('../services/shareLinkService');
const { parseBooleanInput, parseStringInput } = require('../utils/parsers'); const { parseBooleanInput, parseStringInput } = require('../utils/parsers');
const eventTypeService = require('../services/eventTypeService'); const eventTypeService = require('../services/eventTypeService');
const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('../utils/emailNormalization'); const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('../utils/emailNormalization');
const logger = require('../utils/logger');
// Use parseStringInput from shared parsers for customer data extraction // Use parseStringInput from shared parsers for customer data extraction
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name); const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
@@ -235,7 +236,7 @@ router.post('/', adminAuth, [
}); });
} }
} catch (waError) { } 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 customer_email: customerEmail
}); });
} catch (error) { } catch (error) {
console.error(error); logger.error(error);
res.status(500).json({ error: 'Failed to create event' }); res.status(500).json({ error: 'Failed to create event' });
} }
}); });
+16 -51
View File
@@ -14,7 +14,7 @@ const secureImageService = require('../services/secureImageService');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { resolvePhotoFilePath } = require('../services/photoResolver'); const { resolvePhotoFilePath } = require('../services/photoResolver');
const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService'); const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService');
const { handleAsync } = require('../utils/routeHelpers'); const { handleAsync, errorResponse } = require('../utils/routeHelpers');
const { NotFoundError } = require('../utils/errors'); const { NotFoundError } = require('../utils/errors');
const { ensureThumbnail, ensureHeroImage, ensurePreviewImage, withLocalCopy } = require('../services/imageProcessor'); const { ensureThumbnail, ensureHeroImage, ensurePreviewImage, withLocalCopy } = require('../services/imageProcessor');
const downloadZipService = require('../services/downloadZipService'); const downloadZipService = require('../services/downloadZipService');
@@ -213,8 +213,7 @@ router.get('/:slug/info', async (req, res) => {
promo_markdown: event.promo_markdown || null promo_markdown: event.promo_markdown || null
}); });
} catch (error) { } catch (error) {
console.error('Error fetching gallery info:', error); errorResponse(res, error, 500, 'Failed to fetch gallery info');
res.status(500).json({ error: 'Failed to fetch gallery info' });
} }
}); });
@@ -739,8 +738,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res)
}) })
}); });
} catch (error) { } catch (error) {
console.error('Error fetching photos:', error); errorResponse(res, error, 500, 'Failed to fetch photos');
res.status(500).json({ error: 'Failed to fetch photos' });
} }
}); });
@@ -772,8 +770,7 @@ router.patch('/:slug/photos/:photoId/visibility', verifyGalleryAccess, async (re
res.json({ message: 'Photo visibility updated', visibility }); res.json({ message: 'Photo visibility updated', visibility });
} catch (error) { } catch (error) {
logger.error('Error updating photo visibility:', error); errorResponse(res, error, 500, 'Failed to update photo visibility');
res.status(500).json({ error: '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 }); res.json({ message: `${count} photos updated`, visibility });
} catch (error) { } catch (error) {
logger.error('Error bulk updating photo visibility:', error); errorResponse(res, error, 500, 'Failed to update photo visibility');
res.status(500).json({ error: 'Failed to update photo visibility' });
} }
}); });
@@ -916,13 +912,7 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, denySlideshowToken,
}); });
} }
} catch (error) { } catch (error) {
logger.error('Unexpected error processing gallery download', { errorResponse(res, error, 500, 'Failed to download photo');
slug: req.params.slug,
photoId: req.params.photoId,
eventId: req.event?.id,
error: error.message,
});
res.status(500).json({ error: 'Failed to download photo' });
} }
}); });
@@ -1093,12 +1083,7 @@ router.get('/:slug/download-all', verifyGalleryAccess, denySlideshowToken, async
action: 'download_all' action: 'download_all'
}); });
} catch (error) { } catch (error) {
logger.error('Error creating bulk gallery download', { errorResponse(res, error, 500, 'Failed to create download archive');
slug: req.params.slug,
eventId: req.event?.id,
error: error.message,
});
res.status(500).json({ error: 'Failed to create download archive' });
} }
}); });
@@ -1215,12 +1200,7 @@ router.post('/:slug/download-selected', verifyGalleryAccess, denySlideshowToken,
action: 'download_selected' action: 'download_selected'
}); });
} catch (error) { } catch (error) {
logger.error('Error in download-selected:', { errorResponse(res, error, 500, 'Failed to download selected photos');
slug: req.params.slug,
eventId: req.event?.id,
error: error.message,
});
res.status(500).json({ error: 'Failed to download selected photos' });
} }
}); });
@@ -1452,13 +1432,7 @@ router.get('/:slug/photo/:photoId',
} }
} }
} catch (error) { } catch (error) {
logger.error('Error serving photo:', { errorResponse(res, error, 500, 'Failed to serve photo');
error: error.message,
stack: error.stack,
photoId: req.params.photoId,
eventId: req.event?.id
});
res.status(500).json({ error: 'Failed to serve photo' });
} }
} }
); );
@@ -1549,12 +1523,7 @@ router.get('/:slug/thumbnail/:photoId',
stream.pipe(res); stream.pipe(res);
} }
} catch (error) { } catch (error) {
logger.error('Error serving thumbnail:', { errorResponse(res, error, 500, 'Failed to serve thumbnail');
error: error.message,
photoId: req.params.photoId,
eventId: req.event?.id
});
res.status(500).json({ error: 'Failed to serve thumbnail' });
} }
} }
); );
@@ -1763,8 +1732,7 @@ router.get('/:slug/feedback-settings', verifyGalleryAccess, async (req, res) =>
identity_mode: settings.identity_mode || 'simple' identity_mode: settings.identity_mode || 'simple'
}); });
} catch (error) { } catch (error) {
console.error('Error fetching feedback settings:', error); errorResponse(res, error, 500, 'Failed to fetch feedback settings');
res.status(500).json({ error: 'Failed to fetch feedback settings' });
} }
}); });
@@ -1826,8 +1794,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
fs.mkdirSync(tempUploadDir, { recursive: true, mode: 0o755 }); fs.mkdirSync(tempUploadDir, { recursive: true, mode: 0o755 });
logger.info('Created temp upload directory:', tempUploadDir); logger.info('Created temp upload directory:', tempUploadDir);
} catch (mkdirErr) { } catch (mkdirErr) {
logger.error('Failed to create temp upload directory:', mkdirErr); return errorResponse(res, mkdirErr, 500, 'Server configuration error: unable to create upload directory');
return res.status(500).json({ error: 'Server configuration error: unable to create upload directory' });
} }
} }
@@ -1877,7 +1844,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
// Handle upload // Handle upload
upload(req, res, async (err) => { upload(req, res, async (err) => {
if (err) { if (err) {
console.error('Upload error:', err); logger.error('Upload error:', err);
return res.status(400).json({ error: err.message }); 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, errors: result.errors.length > 0 ? result.errors : undefined,
}); });
} catch (processError) { } catch (processError) {
console.error('Photo processing error:', processError); errorResponse(res, processError, 500, 'Failed to process photos');
res.status(500).json({ error: 'Failed to process photos' });
} }
}); });
} catch (error) { } catch (error) {
console.error('Upload route error:', error); errorResponse(res, error, 500, 'Failed to upload photos');
res.status(500).json({ error: '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.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour cache
res.send(template.css_content); res.send(template.css_content);
} catch (error) { } catch (error) {
console.error('Get CSS template error:', error); logger.error('Get CSS template error:', error);
res.status(500).send('/* Error loading template */'); res.status(500).send('/* Error loading template */');
} }
}); });
+5 -4
View File
@@ -8,6 +8,7 @@ const { getStorage } = require('../services/storage');
const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('../services/photoResolver'); const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('../services/photoResolver');
const { withLocalCopy } = require('../services/imageProcessor'); const { withLocalCopy } = require('../services/imageProcessor');
const crypto = require('crypto'); const crypto = require('crypto');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
@@ -164,7 +165,7 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, async (req, res) =
res.send(finalImage); res.send(finalImage);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to serve image' });
} }
}); });
@@ -208,7 +209,7 @@ router.post('/:slug/photo/:photoId/generate-secure-token', verifyGalleryAccess,
}); });
} catch (error) { } 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' }); 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) { } 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' }); 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); res.send(imageBuffer);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to serve image' });
} }
}); });
+2 -1
View File
@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get public CMS page // Get public CMS page
@@ -41,7 +42,7 @@ router.get('/pages/:slug', async (req, res) => {
updated_at: page.updated_at updated_at: page.updated_at
}); });
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to fetch page' });
} }
}); });
+2 -1
View File
@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const { db, withRetry } = require('../database/db'); const { db, withRetry } = require('../database/db');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get public settings (branding and theme) // Get public settings (branding and theme)
@@ -205,7 +206,7 @@ router.get('/', async (req, res) => {
res.json(publicSettings); res.json(publicSettings);
} catch (error) { } 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' }); res.status(500).json({ error: 'Failed to fetch settings' });
} }
}); });
+2 -2
View File
@@ -412,7 +412,7 @@ async function getRecentAccessStats() {
return acc; return acc;
}, {}); }, {});
} catch (error) { } catch (error) {
console.error('Error getting recent access stats:', error); logger.error('Error getting recent access stats:', error);
return {}; return {};
} }
} }
@@ -440,7 +440,7 @@ async function getSuspiciousActivityStats() {
uniqueIPs: parseInt(uniqueIPs.count) uniqueIPs: parseInt(uniqueIPs.count)
}; };
} catch (error) { } catch (error) {
console.error('Error getting suspicious activity stats:', error); logger.error('Error getting suspicious activity stats:', error);
return { suspiciousEvents: 0, uniqueIPs: 0 }; return { suspiciousEvents: 0, uniqueIPs: 0 };
} }
} }
+14 -14
View File
@@ -11,14 +11,14 @@ const logger = require('../utils/logger');
function normalizeFiles(files) { function normalizeFiles(files) {
// Handle null, undefined, or falsy values // Handle null, undefined, or falsy values
if (!files) { if (!files) {
console.log('[normalizeFiles] No files provided'); logger.info('[normalizeFiles] No files provided');
return []; return [];
} }
// Handle arrays // Handle arrays
if (Array.isArray(files)) { if (Array.isArray(files)) {
const validFiles = files.filter(Boolean); 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; return validFiles;
} }
@@ -26,11 +26,11 @@ function normalizeFiles(files) {
try { try {
if (typeof files === 'object' && typeof files[Symbol.iterator] === 'function') { if (typeof files === 'object' && typeof files[Symbol.iterator] === 'function') {
const validFiles = Array.from(files).filter(Boolean); 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; return validFiles;
} }
} catch (err) { } 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) // Handle plain objects (multer fieldname mapping)
@@ -39,16 +39,16 @@ function normalizeFiles(files) {
const validFiles = Object.values(files) const validFiles = Object.values(files)
.flatMap((value) => (Array.isArray(value) ? value : [value])) .flatMap((value) => (Array.isArray(value) ? value : [value]))
.filter(Boolean); .filter(Boolean);
console.log(`[normalizeFiles] Normalized ${validFiles.length} files from object`); logger.info(`[normalizeFiles] Normalized ${validFiles.length} files from object`);
return validFiles; return validFiles;
} catch (err) { } catch (err) {
console.warn('[normalizeFiles] Failed to process files object:', err.message); logger.warn('[normalizeFiles] Failed to process files object:', err.message);
return []; return [];
} }
} }
// Unexpected type // Unexpected type
console.warn('[normalizeFiles] Unexpected files type:', typeof files); logger.warn('[normalizeFiles] Unexpected files type:', typeof files);
return []; return [];
} }
@@ -115,7 +115,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
try { try {
await fs.access(tempPath); await fs.access(tempPath);
} catch (accessErr) { } catch (accessErr) {
console.error(`Temp file not accessible: ${tempPath}`, { logger.error(`Temp file not accessible: ${tempPath}`, {
originalname: file?.originalname, originalname: file?.originalname,
error: accessErr.message error: accessErr.message
}); });
@@ -156,7 +156,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
}; };
} }
} catch (metadataError) { } 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, contentType: file.mimetype,
}); });
} catch (uploadErr) { } 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}`); throw new Error(`Failed to upload to storage: ${uploadErr.message}`);
} finally { } finally {
try { try {
await fs.unlink(tempPath); await fs.unlink(tempPath);
} catch (unlinkErr) { } catch (unlinkErr) {
if (unlinkErr?.code !== 'ENOENT') { 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, error: unlinkErr.message,
code: unlinkErr.code code: unlinkErr.code
}); });
@@ -262,9 +262,9 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
type: photoType type: photoType
}); });
console.log(`Successfully processed file ${file.originalname} (ID: ${photoId})`); logger.info(`Successfully processed file ${file.originalname} (ID: ${photoId})`);
} catch (error) { } catch (error) {
console.error(`Error processing file ${file.originalname}:`, { logger.error(`Error processing file ${file.originalname}:`, {
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
originalname: file.originalname, originalname: file.originalname,
@@ -277,7 +277,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
try { try {
await trx.rollback(); await trx.rollback();
} catch (rollbackErr) { } catch (rollbackErr) {
console.error('Failed to rollback transaction:', rollbackErr); logger.error('Failed to rollback transaction:', rollbackErr);
} }
} }
+3 -2
View File
@@ -1,5 +1,6 @@
const axios = require('axios'); const axios = require('axios');
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger');
async function verifyRecaptcha(token) { async function verifyRecaptcha(token) {
// Check if reCAPTCHA is enabled // Check if reCAPTCHA is enabled
@@ -32,7 +33,7 @@ async function verifyRecaptcha(token) {
// If no secret key configured, fail closed // If no secret key configured, fail closed
if (!secretKey) { 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; return false;
} }
@@ -50,7 +51,7 @@ async function verifyRecaptcha(token) {
return response.data.success === true; return response.data.success === true;
} catch (error) { } catch (error) {
console.error('reCAPTCHA verification error:', error); logger.error('reCAPTCHA verification error:', error);
return false; return false;
} }
} }
+9 -8
View File
@@ -4,6 +4,7 @@ const { db } = require('../database/db');
const watermarkService = require('./watermarkService'); const watermarkService = require('./watermarkService');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const logger = require('../utils/logger');
class SecureImageService { class SecureImageService {
constructor() { constructor() {
@@ -268,7 +269,7 @@ class SecureImageService {
return buffer; return buffer;
} catch (error) { } catch (error) {
console.error('Error processing protected image:', error); logger.error('Error processing protected image:', error);
// Return original on error // Return original on error
return await fs.readFile(imagePath); return await fs.readFile(imagePath);
} }
@@ -346,7 +347,7 @@ class SecureImageService {
} }
} catch (error) { } catch (error) {
console.error('Error logging image access:', error); logger.error('Error logging image access:', error);
} }
} }
@@ -386,7 +387,7 @@ class SecureImageService {
} }
} catch (error) { } 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, clientFingerprint,
photoId, photoId,
details details
@@ -432,12 +433,12 @@ class SecureImageService {
.first(); .first();
if (parseInt(recentSuspicious.count) >= 3) { 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 // This would be handled by the middleware's blocking system
} }
} catch (error) { } 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 // Flag if more than 10 accesses to same photo in 5 minutes
if (accessCount > 10) { 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 true;
} }
return false; return false;
} catch (error) { } catch (error) {
console.error('Error detecting suspicious activity:', error); logger.error('Error detecting suspicious activity:', error);
return false; return false;
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat'); const { formatBoolean } = require('../utils/dbCompat');
const { extractShareToken, isPotentialShareToken, buildSharePath } = require('../utils/shareLinkUtils'); const { extractShareToken, isPotentialShareToken, buildSharePath } = require('../utils/shareLinkUtils');
const { getFrontendBaseUrl } = require('../utils/frontendUrl'); const { getFrontendBaseUrl } = require('../utils/frontendUrl');
const logger = require('../utils/logger');
const SETTING_KEY = 'general_short_gallery_urls'; const SETTING_KEY = 'general_short_gallery_urls';
const CACHE_TTL_MS = 60_000; 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(); const setting = await db('app_settings').where({ setting_key: SETTING_KEY }).first();
return setting?.setting_value ?? null; return setting?.setting_value ?? null;
} catch (error) { } catch (error) {
console.error('Failed to read gallery URL setting:', error.message); logger.error('Failed to read gallery URL setting:', error.message);
return null; return null;
} }
}; };
+3 -2
View File
@@ -1,4 +1,5 @@
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger');
const DEFAULT_MAX_FILES_PER_UPLOAD = 500; const DEFAULT_MAX_FILES_PER_UPLOAD = 500;
const MAX_ALLOWED_FILES_PER_UPLOAD = 2000; const MAX_ALLOWED_FILES_PER_UPLOAD = 2000;
@@ -87,7 +88,7 @@ const getMaxFilesPerUpload = async () => {
cacheExpiresAt = Date.now() + CACHE_TTL_MS; cacheExpiresAt = Date.now() + CACHE_TTL_MS;
return parsedValue; return parsedValue;
} catch (error) { } 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; cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD;
cacheExpiresAt = Date.now() + CACHE_TTL_MS; cacheExpiresAt = Date.now() + CACHE_TTL_MS;
return DEFAULT_MAX_FILES_PER_UPLOAD; return DEFAULT_MAX_FILES_PER_UPLOAD;
@@ -147,7 +148,7 @@ const getAllowedMimeTypes = async () => {
allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS; allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS;
return mimeTypes; return mimeTypes;
} catch (error) { } 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); const fallback = extensionsToMimeTypes(DEFAULT_ALLOWED_FILE_TYPES);
cachedAllowedTypes = fallback; cachedAllowedTypes = fallback;
allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS; allowedTypesCacheExpiresAt = Date.now() + CACHE_TTL_MS;
@@ -12,6 +12,7 @@ const { db } = require('../database/db');
const watermarkService = require('./watermarkService'); const watermarkService = require('./watermarkService');
const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('./photoResolver'); const { resolvePhotoStorageKey, resolvePhotoFilePath } = require('./photoResolver');
const { withLocalCopy } = require('./imageProcessor'); const { withLocalCopy } = require('./imageProcessor');
const logger = require('../utils/logger');
class WatermarkGeneratorService { class WatermarkGeneratorService {
constructor() { constructor() {
@@ -79,7 +80,7 @@ class WatermarkGeneratorService {
return result; return result;
} catch (error) { } 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 }; return { success: false, error: error.message };
} }
} }
@@ -155,7 +156,7 @@ class WatermarkGeneratorService {
return results; return results;
} catch (error) { } 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] }; return { ...results, errors: [...results.errors, error.message] };
} }
} }
@@ -232,7 +233,7 @@ class WatermarkGeneratorService {
return results; return results;
} }
console.log(`Starting watermark regeneration for ${photos.length} photos`); logger.info(`Starting watermark regeneration for ${photos.length} photos`);
// Process in batches // Process in batches
for (let i = 0; i < photos.length; i += this.batchSize) { for (let i = 0; i < photos.length; i += this.batchSize) {
@@ -281,11 +282,11 @@ class WatermarkGeneratorService {
} }
results.status = 'completed'; 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; return results;
} catch (error) { } catch (error) {
console.error('Error during watermark regeneration:', error); logger.error('Error during watermark regeneration:', error);
results.status = 'failed'; results.status = 'failed';
results.errors.push(error.message); results.errors.push(error.message);
return results; return results;
@@ -320,10 +321,10 @@ class WatermarkGeneratorService {
watermark_generated_at: null watermark_generated_at: null
}); });
console.log(`Cleared ${photos.length} watermarks`); logger.info(`Cleared ${photos.length} watermarks`);
return { success: true, cleared: photos.length }; return { success: true, cleared: photos.length };
} catch (error) { } catch (error) {
console.error('Error clearing watermarks:', error); logger.error('Error clearing watermarks:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }
@@ -350,7 +351,7 @@ class WatermarkGeneratorService {
return { success: true }; return { success: true };
} catch (error) { } 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 }; return { success: false, error: error.message };
} }
} }
+6 -5
View File
@@ -3,6 +3,7 @@ const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const { db } = require('../database/db'); const { db } = require('../database/db');
const { getStorage } = require('./storage'); const { getStorage } = require('./storage');
const logger = require('../utils/logger');
class WatermarkService { class WatermarkService {
constructor() { constructor() {
@@ -44,7 +45,7 @@ class WatermarkService {
companyName: settingsObj.branding_company_name || 'Photo Gallery' companyName: settingsObj.branding_company_name || 'Photo Gallery'
}; };
} catch (error) { } catch (error) {
console.error('Error fetching watermark settings:', error); logger.error('Error fetching watermark settings:', error);
return null; return null;
} }
} }
@@ -138,7 +139,7 @@ class WatermarkService {
watermarkMetadata = { width: targetWidth, height: targetHeight }; watermarkMetadata = { width: targetWidth, height: targetHeight };
} catch (error) { } catch (error) {
console.error('Error processing watermark logo:', error); logger.error('Error processing watermark logo:', error);
watermarkBuffer = null; watermarkBuffer = null;
} }
} }
@@ -209,7 +210,7 @@ class WatermarkService {
return watermarkedBuffer; return watermarkedBuffer;
} catch (error) { } catch (error) {
console.error('Error applying watermark:', error); logger.error('Error applying watermark:', error);
// Return original image on error // Return original image on error
return await fs.readFile(imagePath); return await fs.readFile(imagePath);
} }
@@ -287,7 +288,7 @@ class WatermarkService {
error: null error: null
}; };
} catch (error) { } catch (error) {
console.error(`Error generating watermark for photo ${photo.id}:`, error); logger.error(`Error generating watermark for photo ${photo.id}:`, error);
return { return {
success: false, success: false,
watermarkPath: null, watermarkPath: null,
@@ -308,7 +309,7 @@ class WatermarkService {
await getStorage().delete(watermarkPath); await getStorage().delete(watermarkPath);
return true; return true;
} catch (error) { } catch (error) {
console.error('Error deleting watermark file:', error); logger.error('Error deleting watermark file:', error);
return false; return false;
} }
} }
+4 -3
View File
@@ -1,4 +1,5 @@
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('./logger');
// Default date format settings // Default date format settings
const DEFAULT_FORMAT = { const DEFAULT_FORMAT = {
@@ -19,7 +20,7 @@ async function formatDate(date, language = 'en') {
try { try {
dateConfig = JSON.parse(setting.setting_value); dateConfig = JSON.parse(setting.setting_value);
} catch (e) { } 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; dateConfig = DEFAULT_FORMAT;
} }
} else { } else {
@@ -46,7 +47,7 @@ async function formatDate(date, language = 'en') {
// Check if date is valid // Check if date is valid
if (isNaN(dateObj.getTime())) { 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'); throw new Error('Invalid date');
} }
@@ -91,7 +92,7 @@ async function formatDate(date, language = 'en') {
}); });
} }
} catch (error) { } catch (error) {
console.error('Error formatting date:', error); logger.error('Error formatting date:', error);
// Fallback to basic formatting // Fallback to basic formatting
return date instanceof Date ? date.toLocaleDateString() : new Date(date).toLocaleDateString(); return date instanceof Date ? date.toLocaleDateString() : new Date(date).toLocaleDateString();
} }
+2 -1
View File
@@ -5,6 +5,7 @@
// Note: Requiring db here creates circular dependency // Note: Requiring db here creates circular dependency
// db should be passed as parameter or required where needed // db should be passed as parameter or required where needed
const logger = require('./logger');
/** /**
* Get database client type * Get database client type
@@ -91,7 +92,7 @@ async function getDatabaseSize(db, dbName) {
const stats = await fs.stat(dbPath); const stats = await fs.stat(dbPath);
return stats.size; return stats.size;
} catch (error) { } catch (error) {
console.error('Error getting SQLite database size:', error); logger.error('Error getting SQLite database size:', error);
return 0; return 0;
} }
} }
+4 -3
View File
@@ -1,5 +1,6 @@
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const logger = require('./logger');
/** /**
* Secure file security utilities to prevent path traversal and validate file types * Secure file security utilities to prevent path traversal and validate file types
@@ -174,7 +175,7 @@ async function validateFileContent(filePath, expectedMimeType) {
return true; return true;
}); });
} catch (error) { } catch (error) {
console.error('Error validating file content:', error); logger.error('Error validating file content:', error);
return false; return false;
} }
} }
@@ -239,7 +240,7 @@ function createFileUploadValidator(options = {}) {
try { try {
await fs.unlink(file.path); await fs.unlink(file.path);
} catch (err) { } catch (err) {
console.error('Error removing invalid file:', err); logger.error('Error removing invalid file:', err);
} }
return res.status(400).json({ return res.status(400).json({
error: `File content does not match declared type: ${file.originalname}` error: `File content does not match declared type: ${file.originalname}`
@@ -250,7 +251,7 @@ function createFileUploadValidator(options = {}) {
next(); next();
} catch (error) { } catch (error) {
console.error('File validation error:', error); logger.error('File validation error:', error);
res.status(500).json({ error: 'File validation failed' }); res.status(500).json({ error: 'File validation failed' });
} }
}; };
+15 -13
View File
@@ -5,6 +5,7 @@
const { validationResult } = require('express-validator'); const { validationResult } = require('express-validator');
const { ValidationError } = require('./errors'); const { ValidationError } = require('./errors');
const logger = require('./logger');
/** /**
* Wraps an async route handler to catch errors and pass them to the error handler. * 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. * Logs an error and sends a standardized error response of shape `{ error: <string> }`.
* Note: Prefer throwing custom errors and letting the error handler format the response.
* *
* @param {Response} res - Express response object * @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 {number} [statusCode=500] - HTTP status code
* @param {string} [code] - Optional error code * @param {string} [publicMessage] - Message sent to the client; falls back to the error's message
* @param {*} [details] - Optional additional error details
* *
* @example * @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 errorResponse = (res, error, statusCode = 500, publicMessage) => {
const response = { const message = publicMessage || (error instanceof Error ? error.message : String(error));
error: message, const route = res.req ? `${res.req.method} ${res.req.originalUrl}` : null;
...(code && { code }), logger.error(route ? `${route} - ${message}` : message, {
...(details && { details }) error: error instanceof Error ? error.message : error,
}; stack: error instanceof Error ? error.stack : undefined
res.status(statusCode).json(response); });
res.status(statusCode).json({ error: message });
}; };
/** /**