Compare commits

..

No commits in common. "7d476f36e84bf27b9c0130372fec3564e3c4fae6" and "54dc893ec5f7c4498d0866d0e551bd0d6a2515de" have entirely different histories.

8 changed files with 49 additions and 177 deletions

View File

@ -40,8 +40,9 @@ ENV ZOHO_REDIRECT_URI=
# Create app directory structure # Create app directory structure
RUN mkdir -p /app/db /app/logs RUN mkdir -p /app/db /app/logs
# Set permissions for db directory (before USER switch) # Copy entrypoint script
RUN chown -R nodejs:nodejs /app/db /app/logs COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Copy from builder - built artifacts and package manifests # Copy from builder - built artifacts and package manifests
COPY --from=builder /app/package.json /app/package-lock.json* ./ COPY --from=builder /app/package.json /app/package-lock.json* ./
@ -51,15 +52,17 @@ COPY --from=builder /app/server ./server
# Install production dependencies only in runtime stage # Install production dependencies only in runtime stage
RUN npm ci --omit=dev 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 backend port
EXPOSE 3001 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) # Health check using Node 20 built-in fetch (no wget required)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ 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 => r.ok ? 0 : 1).catch(() => 1)" || exit 1
# Run the Express server # Run the Express server via entrypoint (runs as root, then switches to nodejs)
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server/index.js"] CMD ["node", "server/index.js"]

View File

@ -5,10 +5,6 @@
set -e set -e
log_error() {
echo "[$(date -Iseconds)] ERROR $1" >&2
}
# Create directories if they don't exist # Create directories if they don't exist
mkdir -p /app/db mkdir -p /app/db
mkdir -p /app/logs mkdir -p /app/logs
@ -17,12 +13,5 @@ mkdir -p /app/logs
chmod 777 /app/db chmod 777 /app/db
chmod 777 /app/logs 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 # 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 exec su-exec nodejs node server/index.js

View File

@ -3,17 +3,6 @@ import path from 'path'
import { existsSync, mkdirSync, chmodSync } from 'fs' import { existsSync, mkdirSync, chmodSync } from 'fs'
import sqlite3 from 'better-sqlite3' 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 dbPath = path.join(new URL(import.meta.url).pathname, '..', 'db', 'queuenorth.db')
const dbDir = path.dirname(dbPath) const dbDir = path.dirname(dbPath)
@ -33,86 +22,34 @@ export const db = sqlite3(dbPath)
// Initialize schema // Initialize schema
export const initSchema = () => { export const initSchema = () => {
// Issue #6: Add UNIQUE constraint on leads.email with migration support // Leads table
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get() db.exec(`
CREATE TABLE IF NOT EXISTS leads (
if (tableExists) { id INTEGER PRIMARY KEY AUTOINCREMENT,
// Check if UNIQUE constraint already exists company TEXT NOT NULL,
const pragma = db.prepare("PRAGMA table_info(leads)").all() name TEXT NOT NULL,
const emailCol = pragma.find(col => col.name === 'email') email TEXT NOT NULL,
phone TEXT,
if (!emailCol || !emailCol.notnull) { zip TEXT,
// UNIQUE constraint doesn't exist, need to add it via migration message TEXT,
log.info('[DB] Adding UNIQUE constraint on leads.email via migration') service_interest TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
// 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 // Support requests table
db.exec( db.exec(`
[ CREATE TABLE IF NOT EXISTS support_requests (
'CREATE TABLE IF NOT EXISTS support_requests (', id INTEGER PRIMARY KEY AUTOINCREMENT,
' id INTEGER PRIMARY KEY AUTOINCREMENT,', name TEXT NOT NULL,
' name TEXT NOT NULL,', company TEXT NOT NULL,
' company TEXT NOT NULL,', email TEXT NOT NULL,
' email TEXT NOT NULL,', phone TEXT,
' phone TEXT,', issue TEXT NOT NULL,
' issue TEXT NOT NULL,', priority TEXT DEFAULT 'medium',
' priority TEXT DEFAULT \'medium\',', created_at DATETIME DEFAULT CURRENT_TIMESTAMP
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
) )
`)
} }
initSchema() initSchema()

View File

@ -167,9 +167,7 @@ const sanitizeString = (input, maxLength) => {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '') sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '')
sanitized = sanitized.replace(/<[^>]*>/g, '') sanitized = sanitized.replace(/<[^>]*>/g, '')
// Truncate to max length // Truncate to max length
sanitized = sanitized.substring(0, maxLength) return sanitized.substring(0, maxLength)
// Convert empty strings to undefined so they become NULL in DB
return sanitized === '' ? undefined : sanitized
} }
const sanitizePayload = (data, fields) => { const sanitizePayload = (data, fields) => {
@ -361,24 +359,6 @@ app.post('/api/leads', (req, res) => {
res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) res.json({ success: true, message: "Thanks! We'll be in touch shortly." })
} catch (err) { } 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) log.error('Error submitting lead:', err)
res.status(500).json({ error: 'Failed to submit lead' }) res.status(500).json({ error: 'Failed to submit lead' })
} }

View File

@ -49,11 +49,7 @@ const Footer = () => {
<span className="font-bold text-lg">Queue North</span> <span className="font-bold text-lg">Queue North</span>
</div> </div>
<p className="text-navy-light text-sm mb-3">{companyInfo.tagline}</p> <p className="text-navy-light text-sm mb-3">{companyInfo.tagline}</p>
<p className="text-navy-light text-sm mb-3">{companyInfo.address}</p> <p className="text-navy-light text-sm mb-4">{companyInfo.address}</p>
<div className="text-navy-light text-sm mb-3">
<p className="mb-1">Phone: (906) 482-6616</p>
<a href="/contact" className="text-cyan hover:underline">Contact Form</a>
</div>
<p className="text-navy-light text-xs">© {currentYear} Queue North Technologies. All rights reserved.</p> <p className="text-navy-light text-xs">© {currentYear} Queue North Technologies. All rights reserved.</p>
</div> </div>

View File

@ -121,12 +121,8 @@ const Contact = () => {
<p className="text-soft-text">Monday - Friday: 8:00 AM - 6:00 PM CT</p> <p className="text-soft-text">Monday - Friday: 8:00 AM - 6:00 PM CT</p>
</div> </div>
<div> <div>
<h3 className="font-semibold text-text mb-2">Phone</h3> <h3 className="font-semibold text-text mb-2">Email</h3>
<p className="text-soft-text">(906) 482-6616</p> <p className="text-soft-text">info@queuenorth.com</p>
</div>
<div>
<h3 className="font-semibold text-text mb-2">Contact Us</h3>
<p className="text-soft-text">Use the form on the right to get in touch with our team.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,40 +11,29 @@ const Home = () => {
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Hero Section */} {/* Hero Section */}
<section className="bg-gradient-to-br from-primary-navy via-primary-navy to-teal-900 text-white py-16 md:py-24"> <section className="bg-primary-navy text-white py-16 md:py-24">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div> <div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6"> <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
Reliable Business Communications Without the Runaround Modern Communications Infrastructure Without the Vendor Noise
</h1> </h1>
<p className="text-xl md:text-2xl text-section-alt mb-8 max-w-2xl"> <p className="text-xl md:text-2xl text-section-alt mb-8 max-w-2xl">
We handle your phones, internet, and IT so you can focus on running your business. 8x8 Certified Partner with 25+ years of proven reliability. We deliver UCaaS, Contact Center, deployment, and managed lifecycle support for SMB and enterprise organizations.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4"> <div className="flex flex-col sm:flex-row gap-4">
<Button variant="default" size="lg" className="bg-white text-primary-navy hover:bg-gray-100" onClick={() => navigate('/contact')}> <Button variant="default" size="lg" className="bg-white text-primary-navy hover:bg-gray-100">
Schedule Consultation Request Consultation
</Button> </Button>
<Button variant="outline" size="lg" className="border-white text-white hover:bg-white/10" onClick={() => navigate('/services')}> <Button variant="outline" size="lg" className="border-white text-white hover:bg-white/10">
View Services Explore Services
</Button> </Button>
</div> </div>
<div className="mt-10 flex flex-wrap gap-4"> <div className="mt-10 flex flex-wrap gap-4">
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium"> <span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">8x8 Certified Partner</span>
<img src="/assets/8x8_Logo_White.svg" alt="8x8" className="h-5 w-5" /> <span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">Veteran Owned</span>
<span>8x8 Certified Partner</span> <span className="px-4 py-2 bg-white/10 rounded-full text-sm font-numeric">25+ Years Experience</span>
</div> <span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">SMB to Enterprise</span>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium">
<div className="w-5 h-5 bg-white rounded-full flex items-center justify-center text-primary-navy font-bold">V</div>
<span>Veteran Owned</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-numeric">
<span>25+</span>
<span className="text-sm">Years Experience</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium">
<span>SMB to Enterprise</span>
</div>
</div> </div>
</div> </div>
<div className="hidden lg:block"> <div className="hidden lg:block">

View File

@ -119,24 +119,6 @@ const Support = () => {
We provide comprehensive support for all our services, including 24/7 monitoring, rapid response, and dedicated support engineers. We provide comprehensive support for all our services, including 24/7 monitoring, rapid response, and dedicated support engineers.
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
<div>
<h3 className="font-semibold text-text mb-2">Visit Support Center</h3>
<a
href="https://queuenorth.zoho.com/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center px-4 py-2 bg-primary-navy text-white font-medium rounded-md hover:bg-navy-darker transition-colors text-sm"
>
Visit Support Center
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
<div>
<h3 className="font-semibold text-text mb-2">Phone</h3>
<p className="text-soft-text">(906) 482-6616</p>
</div>
<div> <div>
<h3 className="font-semibold text-text mb-2">Support Hours</h3> <h3 className="font-semibold text-text mb-2">Support Hours</h3>
<p className="text-soft-text">24/7 Monitoring with rapid response SLAs</p> <p className="text-soft-text">24/7 Monitoring with rapid response SLAs</p>