Queue-North-Website/server/index.js

222 lines
7.0 KiB
JavaScript
Raw Normal View History

2026-05-12 01:04:17 -05:00
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url'
import { existsSync, mkdirSync, chmodSync } from 'fs'
2026-05-12 01:04:17 -05:00
import sqlite3 from 'better-sqlite3'
import z from 'zod'
// --- Setup ---
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const dbPath = path.join(__dirname, '../db/queuenorth.db')
const dbDir = path.dirname(dbPath)
2026-05-12 01:04:17 -05:00
// 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) {}
2026-05-12 01:04:17 -05:00
}
// Middleware
app.use(express.json({ limit: '1mb' }))
app.use(express.urlencoded({ extended: true, limit: '1mb' }))
2026-05-12 01:04:17 -05:00
app.use(express.static(path.join(__dirname, '../dist')))
// --- Database ---
const db = sqlite3(dbPath)
// Initialize schema
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
)
`)
// 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
)
`)
}
initSchema()
// --- Sanitization Helper ---
const sanitizeString = (input, maxLength) => {
if (typeof input !== 'string') return input
// Trim whitespace
let sanitized = input.trim()
// Remove HTML/script tags to prevent XSS
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '')
sanitized = sanitized.replace(/<[^>]*>/g, '')
// Truncate to max length
return sanitized.substring(0, maxLength)
}
const sanitizePayload = (data, fields) => {
const result = { ...data }
for (const [field, maxLength] of Object.entries(fields)) {
if (result[field] !== undefined) {
result[field] = sanitizeString(result[field], maxLength)
}
}
return result
}
2026-05-12 01:04:17 -05:00
// --- Validation Schemas ---
const leadSchema = z.object({
company: z.string().min(1, 'Company name is required').trim().max(200, 'Company name must be 200 characters or less'),
name: z.string().min(1, 'Name is required').trim().max(100, 'Name must be 100 characters or less'),
email: z.string().email('Valid email is required').trim().max(254, 'Email must be 254 characters or less'),
phone: z.string().trim().max(50, 'Phone must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)),
zip: z.string().trim().max(10, 'ZIP code must be 10 characters or less').optional().or(z.literal('').transform(() => undefined)),
message: z.string().trim().max(5000, 'Message must be 5000 characters or less').optional().or(z.literal('').transform(() => undefined)),
service_interest: z.string().trim().max(50, 'Service interest must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)),
2026-05-12 01:04:17 -05:00
})
const supportSchema = z.object({
name: z.string().min(1, 'Name is required').trim().max(100, 'Name must be 100 characters or less'),
company: z.string().min(1, 'Company name is required').trim().max(200, 'Company name must be 200 characters or less'),
email: z.string().email('Valid email is required').trim().max(254, 'Email must be 254 characters or less'),
phone: z.string().trim().max(50, 'Phone must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)),
issue: z.string().min(10, 'Please provide at least 10 characters describing your issue').trim().max(5000, 'Issue description must be 5000 characters or less'),
priority: z.enum(['low', 'medium', 'high'], {
errorMap: () => ({ message: 'Priority must be low, medium, or high' }),
}).transform((val) => val?.toLowerCase() ?? undefined).optional().or(z.literal('').transform(() => undefined)),
2026-05-12 01:04:17 -05:00
})
// --- API Routes ---
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
// Submit lead
app.post('/api/leads', (req, res) => {
try {
const parsed = leadSchema.safeParse(req.body)
if (!parsed.success) {
const fieldErrors = {}
for (const issue of parsed.error.issues) {
if (issue.path[0]) {
fieldErrors[issue.path[0]] = issue.message
}
}
2026-05-12 01:04:17 -05:00
return res.status(400).json({
error: 'Validation failed',
fields: fieldErrors,
2026-05-12 01:04:17 -05:00
})
}
// Sanitize parsed data before insert (trim, strip tags, truncate)
const sanitized = sanitizePayload(parsed.data, {
company: 200,
name: 100,
email: 254,
phone: 50,
zip: 10,
message: 5000,
service_interest: 50,
})
2026-05-12 01:04:17 -05:00
const stmt = db.prepare(`
INSERT INTO leads (company, name, email, phone, zip, message, service_interest)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
stmt.run(
sanitized.company,
sanitized.name,
sanitized.email,
sanitized.phone || null,
sanitized.zip || null,
sanitized.message || null,
sanitized.service_interest || null
2026-05-12 01:04:17 -05:00
)
res.json({ success: true, message: 'Thanks! We\'ll be in touch shortly.' })
} catch (err) {
console.error('Error submitting lead:', err)
res.status(500).json({ error: 'Failed to submit lead' })
}
})
// Submit support request
app.post('/api/support', (req, res) => {
try {
const parsed = supportSchema.safeParse(req.body)
if (!parsed.success) {
const fieldErrors = {}
for (const issue of parsed.error.issues) {
if (issue.path[0]) {
fieldErrors[issue.path[0]] = issue.message
}
}
2026-05-12 01:04:17 -05:00
return res.status(400).json({
error: 'Validation failed',
fields: fieldErrors,
2026-05-12 01:04:17 -05:00
})
}
// Sanitize parsed data before insert (trim, strip tags, truncate)
const sanitized = sanitizePayload(parsed.data, {
name: 100,
company: 200,
email: 254,
phone: 50,
issue: 5000,
priority: 10,
})
2026-05-12 01:04:17 -05:00
const stmt = db.prepare(`
INSERT INTO support_requests (name, company, email, phone, issue, priority)
VALUES (?, ?, ?, ?, ?, ?)
`)
stmt.run(
sanitized.name,
sanitized.company,
sanitized.email,
sanitized.phone || null,
sanitized.issue,
sanitized.priority || 'medium'
2026-05-12 01:04:17 -05:00
)
res.json({ success: true, message: 'Thanks! We\'ll get back to you soon.' })
} catch (err) {
console.error('Error submitting support request:', err)
res.status(500).json({ error: 'Failed to submit support request' })
}
})
// --- Start Server ---
const PORT = process.env.SERVER_PORT || 3001
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`)
console.log(`Health check: http://localhost:${PORT}/api/health`)
})