diff --git a/Dockerfile b/Dockerfile index d3a361c..043eb40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,9 +40,8 @@ ENV ZOHO_REDIRECT_URI= # Create app directory structure RUN mkdir -p /app/db /app/logs -# Copy entrypoint script -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh +# Set permissions for db directory (before USER switch) +RUN chown -R nodejs:nodejs /app/db /app/logs # Copy from builder - built artifacts and package manifests COPY --from=builder /app/package.json /app/package-lock.json* ./ @@ -52,17 +51,15 @@ COPY --from=builder /app/server ./server # Install production dependencies only in runtime stage RUN npm ci --omit=dev -# Install su-exec for switching to non-root user -RUN apk add --no-cache su-exec && \ - rm -rf /var/cache/apk/* - # Expose backend port EXPOSE 3001 +# Switch to non-root user (standard approach, no su-exec needed) +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 -# Run the Express server via entrypoint (runs as root, then switches to nodejs) -ENTRYPOINT ["docker-entrypoint.sh"] +# Run the Express server CMD ["node", "server/index.js"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8b425cc..6c6b73f 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,6 +5,10 @@ set -e +log_error() { + echo "[$(date -Iseconds)] ERROR $1" >&2 +} + # Create directories if they don't exist mkdir -p /app/db mkdir -p /app/logs @@ -13,5 +17,12 @@ mkdir -p /app/logs chmod 777 /app/db chmod 777 /app/logs +# Issue #4: Check if nodejs user exists - if not, this is a Docker build error +if ! id nodejs >/dev/null 2>&1; then + log_error "nodejs user does not exist - this is a Docker build error" + exit 1 +fi + # Run the Express server as nodejs user +# Issue #4: Exit with error code 1 if su-exec fails instead of falling back to root exec su-exec nodejs node server/index.js diff --git a/server/db.js b/server/db.js index 6000698..34dbd7a 100644 --- a/server/db.js +++ b/server/db.js @@ -3,6 +3,17 @@ 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) @@ -22,34 +33,86 @@ export const db = sqlite3(dbPath) // Initialize schema export const initSchema = () => { - // Leads table - 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, - phone TEXT, - zip TEXT, - message TEXT, - service_interest TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP + // 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 - ) - `) + 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 920bfd8..8d54d10 100644 --- a/server/index.js +++ b/server/index.js @@ -167,7 +167,9 @@ const sanitizeString = (input, maxLength) => { sanitized = sanitized.replace(/]*>.*?<\/script>/gi, '') sanitized = sanitized.replace(/<[^>]*>/g, '') // Truncate to max length - return sanitized.substring(0, maxLength) + sanitized = sanitized.substring(0, maxLength) + // Convert empty strings to undefined so they become NULL in DB + return sanitized === '' ? undefined : sanitized } const sanitizePayload = (data, fields) => { @@ -359,6 +361,24 @@ app.post('/api/leads', (req, res) => { res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) } catch (err) { + // Issue #6: Handle duplicate email error with 409 Conflict + const errorMsg = err.message?.toLowerCase() || '' + if (errorMsg.includes('unique constraint') || errorMsg.includes('duplicate')) { + log.warn(`Duplicate lead email: ${sanitized.email}`) + + // Still forward to Zoho (non-blocking) for existing leads + try { + forwardToZoho(sanitized) + } catch (zohoErr) { + log.warn(`[Zoho] Skipped forwarding for duplicate lead: ${sanitized.email}`) + } + + return res.status(409).json({ + error: 'Duplicate lead', + message: 'A lead with this email already exists' + }) + } + log.error('Error submitting lead:', err) res.status(500).json({ error: 'Failed to submit lead' }) } diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 3aa24b0..4f8d3ed 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -11,29 +11,40 @@ const Home = () => { return (
{/* Hero Section */} -
+

- Modern Communications Infrastructure Without the Vendor Noise + Reliable Business Communications — Without the Runaround

- We deliver UCaaS, Contact Center, deployment, and managed lifecycle support for SMB and enterprise organizations. + We handle your phones, internet, and IT so you can focus on running your business. 8x8 Certified Partner with 25+ years of proven reliability.

- -
- 8x8 Certified Partner - Veteran Owned - 25+ Years Experience - SMB to Enterprise +
+ 8x8 + 8x8 Certified Partner +
+
+
V
+ Veteran Owned +
+
+ 25+ + Years Experience +
+
+ SMB to Enterprise +