From e8d5ee1a7bb408c568f6772a520d1f63fbaa424c Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 14 Jul 2025 12:16:56 +0200 Subject: [PATCH] fix: multiple production issues with PostgreSQL and connection handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix trust proxy to use specific values instead of permissive 'true' - Fix clear old notifications to use database-agnostic date calculation - Fix database size check to support both PostgreSQL and SQLite - Add caching and better error handling for session timeout queries - Add query timeout to prevent hanging connections - Improve JSON parsing error handling for setting values These fixes address: - ERR_ERL_PERMISSIVE_TRUST_PROXY warning - PostgreSQL datetime function errors - ENOENT errors looking for SQLite file in PostgreSQL deployment - Connection terminated errors for session timeout checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/server.js | 3 +- backend/src/middleware/sessionTimeout.js | 41 +++++++++++++++++++++--- backend/src/routes/adminNotifications.js | 6 +++- backend/src/routes/adminSystem.js | 30 +++++++++++++---- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/backend/server.js b/backend/server.js index a22fe2a..c066eb9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -29,7 +29,8 @@ const app = express(); const PORT = process.env.PORT || 3000; // Trust proxy headers (required for Traefik/nginx) -app.set('trust proxy', true); +// Set to specific number of proxies or loopback to be more secure +app.set('trust proxy', 'loopback, linklocal, uniquelocal'); // Security middleware with custom CSP app.use(helmet({ diff --git a/backend/src/middleware/sessionTimeout.js b/backend/src/middleware/sessionTimeout.js index 362726c..a3966ec 100644 --- a/backend/src/middleware/sessionTimeout.js +++ b/backend/src/middleware/sessionTimeout.js @@ -7,6 +7,11 @@ const sessions = new Map(); // Default session timeout (60 minutes) const DEFAULT_SESSION_TIMEOUT = 60 * 60 * 1000; +// Cache for session timeout setting +let cachedTimeout = null; +let cacheExpiry = 0; +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + // Clean up expired sessions every 5 minutes setInterval(() => { const now = Date.now(); @@ -18,20 +23,46 @@ setInterval(() => { }, 5 * 60 * 1000); async function getSessionTimeout() { + const now = Date.now(); + + // Return cached value if still valid + if (cachedTimeout && now < cacheExpiry) { + return cachedTimeout; + } + try { const setting = await db('app_settings') .where('setting_key', 'security_session_timeout_minutes') - .first(); + .first() + .timeout(5000); // 5 second timeout if (setting && setting.setting_value) { - const minutes = parseInt(JSON.parse(setting.setting_value)); - return minutes * 60 * 1000; // Convert to milliseconds + let value = setting.setting_value; + // Handle both string and object values + if (typeof value === 'string') { + try { + value = JSON.parse(value); + } catch (e) { + // If it's not JSON, try to parse as number directly + value = parseInt(value); + } + } + const minutes = parseInt(value); + if (!isNaN(minutes) && minutes > 0) { + cachedTimeout = minutes * 60 * 1000; // Convert to milliseconds + cacheExpiry = now + CACHE_DURATION; + return cachedTimeout; + } } } catch (error) { - console.error('Error getting session timeout:', error); + // Only log if it's not a connection error (to avoid spam) + if (error.code !== 'ECONNRESET' && !error.message?.includes('Connection terminated')) { + console.error('Error getting session timeout:', error.message); + } } - return DEFAULT_SESSION_TIMEOUT; + // Use cached value if available, otherwise default + return cachedTimeout || DEFAULT_SESSION_TIMEOUT; } async function sessionTimeoutMiddleware(req, res, next) { diff --git a/backend/src/routes/adminNotifications.js b/backend/src/routes/adminNotifications.js index 2f3ea2e..865117b 100644 --- a/backend/src/routes/adminNotifications.js +++ b/backend/src/routes/adminNotifications.js @@ -100,9 +100,13 @@ router.put('/read-all', adminAuth, async (req, res) => { // Delete old notifications (older than 30 days and read) router.delete('/clear-old', adminAuth, async (req, res) => { try { + // Use database-agnostic date calculation + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const deletedCount = await db('activity_logs') .whereNotNull('read_at') - .where('created_at', '<', db.raw("datetime('now', '-30 days')")) + .where('created_at', '<', thirtyDaysAgo) .delete(); res.json({ diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index 2e5b772..b0bb088 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -35,14 +35,30 @@ router.get('/version', adminAuth, async (req, res) => { // Get comprehensive system status router.get('/status', adminAuth, async (req, res) => { try { - // Database size - const dbPath = path.join(__dirname, '../../data/photo_sharing.db'); + // Database size - check if PostgreSQL or SQLite let dbSize = 0; - try { - const stats = await fs.stat(dbPath); - dbSize = stats.size; - } catch (error) { - console.error('Error getting database size:', error); + const dbClient = process.env.DATABASE_CLIENT || 'sqlite3'; + + if (dbClient === 'pg') { + // PostgreSQL - query database size + try { + const dbName = process.env.DB_NAME || 'picpeak'; + const result = await db.raw(` + SELECT pg_database_size(?) as size + `, [dbName]); + dbSize = result.rows[0]?.size || 0; + } catch (error) { + console.error('Error getting PostgreSQL database size:', error); + } + } else { + // SQLite - check file size + const dbPath = path.join(__dirname, '../../data/photo_sharing.db'); + try { + const stats = await fs.stat(dbPath); + dbSize = stats.size; + } catch (error) { + console.error('Error getting SQLite database size:', error); + } } // Count various entities