fix: multiple production issues with PostgreSQL and connection handling
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m11s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-07-14 12:16:56 +02:00
parent 1761ebd531
commit e8d5ee1a7b
4 changed files with 66 additions and 14 deletions
+2 -1
View File
@@ -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({
+36 -5
View File
@@ -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) {
+5 -1
View File
@@ -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({
+23 -7
View File
@@ -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