From 4e57efdc53e7ed7e9ab2767ee7f75d47507cc806 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 17 May 2026 21:34:39 -0500 Subject: [PATCH] fix: DB schema UNIQUE constraint, Docker healthcheck, DB permissions (#120 #121 #123) (batch 9.0) --- Dockerfile | 2 +- server/db.js | 118 ------------------------------------------------ server/index.js | 56 +++++++++++++++++++++-- 3 files changed, 52 insertions(+), 124 deletions(-) delete mode 100644 server/db.js diff --git a/Dockerfile b/Dockerfile index c08d22c..b48e2bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ USER nodejs # Health check using Node 20 built-in fetch (no wget required) HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD node -e "fetch('http://localhost:3001/api/health').then(r => r.ok ? 0 : 1).catch(() => 1)" || exit 1 + CMD node -e "fetch('http://localhost:3001/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" # Run the Express server CMD ["node", "server/index.js"] diff --git a/server/db.js b/server/db.js deleted file mode 100644 index 34dbd7a..0000000 --- a/server/db.js +++ /dev/null @@ -1,118 +0,0 @@ -// Database initialization - loaded at runtime after entrypoint runs -import path from 'path' -import { existsSync, mkdirSync, chmodSync } from 'fs' -import sqlite3 from 'better-sqlite3' - -// --- Logger --- -const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } -const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info - -const log = { - info: (...args) => { if (currentLevel >= LOG_LEVELS.info) console.log(`[${new Date().toISOString()}] INFO `, ...args) }, - warn: (...args) => { if (currentLevel >= LOG_LEVELS.warn) console.warn(`[${new Date().toISOString()}] WARN `, ...args) }, - error: (...args) => { if (currentLevel >= LOG_LEVELS.error) console.error(`[${new Date().toISOString()}] ERROR`, ...args) }, - debug: (...args) => { if (currentLevel >= LOG_LEVELS.debug) console.debug(`[${new Date().toISOString()}] DEBUG`, ...args) }, -} - -const dbPath = path.join(new URL(import.meta.url).pathname, '..', 'db', 'queuenorth.db') -const dbDir = path.dirname(dbPath) - -// Ensure db directory exists with proper permissions -if (!existsSync(dbDir)) { - mkdirSync(dbDir, { recursive: true }) - try { chmodSync(dbDir, 0o777) } catch (e) {} -} - -// Ensure db file has proper permissions if it exists -if (existsSync(dbPath)) { - try { chmodSync(dbPath, 0o666) } catch (e) {} -} - -// Initialize the database -export const db = sqlite3(dbPath) - -// Initialize schema -export const initSchema = () => { - // Issue #6: Add UNIQUE constraint on leads.email with migration support - const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get() - - if (tableExists) { - // Check if UNIQUE constraint already exists - const pragma = db.prepare("PRAGMA table_info(leads)").all() - const emailCol = pragma.find(col => col.name === 'email') - - if (!emailCol || !emailCol.notnull) { - // UNIQUE constraint doesn't exist, need to add it via migration - log.info('[DB] Adding UNIQUE constraint on leads.email via migration') - - // Migrate leads table to add UNIQUE constraint - db.exec( - [ - 'CREATE TABLE IF NOT EXISTS leads_new (', - ' id INTEGER PRIMARY KEY AUTOINCREMENT,', - ' company TEXT NOT NULL,', - ' name TEXT NOT NULL,', - ' email TEXT NOT NULL UNIQUE,', - ' phone TEXT,', - ' zip TEXT,', - ' message TEXT,', - ' service_interest TEXT,', - ' created_at DATETIME DEFAULT CURRENT_TIMESTAMP', - ')' - ].join('\n') - ) - - // Copy existing data (deduplicate - keep first occurrence per email) - db.exec( - [ - 'INSERT OR IGNORE INTO leads_new (id, company, name, email, phone, zip, message, service_interest, created_at)', - 'SELECT id, company, name, email, phone, zip, message, service_interest, created_at', - 'FROM leads' - ].join('\n') - ) - - // Drop old table - db.exec('DROP TABLE leads') - - // Rename new table - db.exec('ALTER TABLE leads_new RENAME TO leads') - - log.info('[DB] UNIQUE constraint added on leads.email') - } - } else { - // New table - create with UNIQUE constraint from the start - db.exec( - [ - 'CREATE TABLE IF NOT EXISTS leads (', - ' id INTEGER PRIMARY KEY AUTOINCREMENT,', - ' company TEXT NOT NULL,', - ' name TEXT NOT NULL,', - ' email TEXT NOT NULL UNIQUE,', - ' phone TEXT,', - ' zip TEXT,', - ' message TEXT,', - ' service_interest TEXT,', - ' created_at DATETIME DEFAULT CURRENT_TIMESTAMP', - ')' - ].join('\n') - ) - } - - // Support requests table - db.exec( - [ - 'CREATE TABLE IF NOT EXISTS support_requests (', - ' id INTEGER PRIMARY KEY AUTOINCREMENT,', - ' name TEXT NOT NULL,', - ' company TEXT NOT NULL,', - ' email TEXT NOT NULL,', - ' phone TEXT,', - ' issue TEXT NOT NULL,', - ' priority TEXT DEFAULT \'medium\',', - ' created_at DATETIME DEFAULT CURRENT_TIMESTAMP', - ')' - ].join('\n') - ) -} - -initSchema() diff --git a/server/index.js b/server/index.js index 740c673..e480f85 100644 --- a/server/index.js +++ b/server/index.js @@ -1,7 +1,7 @@ import express from 'express' import path from 'path' import { fileURLToPath } from 'url' -import { existsSync, mkdirSync, chmodSync } from 'fs' +import { existsSync, mkdirSync } from 'fs' import sqlite3 from 'better-sqlite3' import z from 'zod' import rateLimit from 'express-rate-limit' @@ -21,8 +21,6 @@ const dbDir = path.dirname(dbPath) // Create db directory if it doesn't exist if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }) - // Try to set writable permissions, ignore if running as non-root - try { chmodSync(dbDir, 0o755) } catch (e) {} } // --- Logger --- @@ -132,13 +130,61 @@ const db = sqlite3(dbPath) // Initialize schema const initSchema = () => { - // Leads table + // Check if leads table exists and needs UNIQUE constraint migration + const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get() + + if (tableExists) { + // Check if UNIQUE constraint already exists on email + const pragma = db.prepare("PRAGMA table_info(leads)").all() + const emailCol = pragma.find(col => col.name === 'email') + + if (emailCol && !emailCol.pk) { + // UNIQUE constraint doesn't exist, need to add it via migration + log.info('[DB] Adding UNIQUE constraint on leads.email via migration') + + // Migrate leads table to add UNIQUE constraint + db.exec( + [ + 'CREATE TABLE IF NOT EXISTS leads_new (', + ' id INTEGER PRIMARY KEY AUTOINCREMENT,', + ' company TEXT NOT NULL,', + ' name TEXT NOT NULL,', + ' email TEXT NOT NULL UNIQUE,', + ' phone TEXT,', + ' zip TEXT,', + ' message TEXT,', + ' service_interest TEXT,', + ' created_at DATETIME DEFAULT CURRENT_TIMESTAMP', + ')' + ].join('\n') + ) + + // Copy existing data (deduplicate - keep first occurrence per email) + db.exec( + [ + 'INSERT OR IGNORE INTO leads_new (id, company, name, email, phone, zip, message, service_interest, created_at)', + 'SELECT id, company, name, email, phone, zip, message, service_interest, created_at', + 'FROM leads' + ].join('\n') + ) + + // Drop old table + db.exec('DROP TABLE leads') + + // Rename new table + db.exec('ALTER TABLE leads_new RENAME TO leads') + + log.info('[DB] UNIQUE constraint added on leads.email') + } + } + + // Leads table (now with UNIQUE constraint on email, either from migration or fresh) db.exec(` CREATE TABLE IF NOT EXISTS leads ( id INTEGER PRIMARY KEY AUTOINCREMENT, company TEXT NOT NULL, name TEXT NOT NULL, - email TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, phone TEXT, zip TEXT, message TEXT,