diff --git a/index.js b/index.js index 0528211..2cbe5b3 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const express = require('express'); const cors = require('cors'); -const Database = require('better-sqlite3'); +const initSqlJs = require('sql.js'); +const fs = require('fs'); const path = require('path'); const app = express(); @@ -8,32 +9,62 @@ const PORT = process.env.PORT || 3000; const API_KEY = process.env.API_KEY || null; const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://chatlabsai.com').split(','); -// Initialize SQLite database -const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'data', 'forms.db'); -const db = new Database(dbPath); +// Database setup +const dataDir = process.env.DATA_DIR || path.join(__dirname, 'data'); +const dbPath = path.join(dataDir, 'forms.db'); +let db; -// Create tables -db.exec(` - CREATE TABLE IF NOT EXISTS submissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - form_name TEXT NOT NULL, - email TEXT NOT NULL, - source TEXT, - ip TEXT, - user_agent TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - CREATE INDEX IF NOT EXISTS idx_form_name ON submissions(form_name); - CREATE INDEX IF NOT EXISTS idx_email ON submissions(email); - CREATE INDEX IF NOT EXISTS idx_created_at ON submissions(created_at); -`); +// Ensure data directory exists +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +// Save database to file periodically +function saveDb() { + if (db) { + const data = db.export(); + fs.writeFileSync(dbPath, Buffer.from(data)); + } +} + +// Initialize database +async function initDb() { + const SQL = await initSqlJs(); + + // Load existing database or create new one + if (fs.existsSync(dbPath)) { + const fileBuffer = fs.readFileSync(dbPath); + db = new SQL.Database(fileBuffer); + } else { + db = new SQL.Database(); + } + + // Create tables + db.run(` + CREATE TABLE IF NOT EXISTS submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + form_name TEXT NOT NULL, + email TEXT NOT NULL, + source TEXT, + ip TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + db.run(`CREATE INDEX IF NOT EXISTS idx_form_name ON submissions(form_name)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_email ON submissions(email)`); + + saveDb(); + + // Auto-save every 30 seconds + setInterval(saveDb, 30000); +} // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors({ origin: (origin, callback) => { - // Allow requests with no origin (mobile apps, curl, etc.) if (!origin) return callback(null, true); if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) { return callback(null, true); @@ -58,7 +89,6 @@ app.post('/submit', (req, res) => { return res.status(400).json({ error: 'Email is required' }); } - // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ error: 'Invalid email format' }); @@ -67,28 +97,23 @@ app.post('/submit', (req, res) => { const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent'] || null; - const stmt = db.prepare(` - INSERT INTO submissions (form_name, email, source, ip, user_agent) - VALUES (?, ?, ?, ?, ?) - `); + db.run( + `INSERT INTO submissions (form_name, email, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))`, + [form_name, email.toLowerCase().trim(), source, ip, userAgent] + ); - const result = stmt.run(form_name, email.toLowerCase().trim(), source, ip, userAgent); + saveDb(); - res.json({ - success: true, - id: result.lastInsertRowid, - message: 'Submission received' - }); + res.json({ success: true, message: 'Submission received' }); } catch (error) { console.error('Submit error:', error); res.status(500).json({ error: 'Internal server error' }); } }); -// Auth middleware for protected routes +// Auth middleware const requireAuth = (req, res, next) => { - if (!API_KEY) return next(); // No auth if no API_KEY set - + if (!API_KEY) return next(); const authHeader = req.headers.authorization; if (!authHeader || authHeader !== `Bearer ${API_KEY}`) { return res.status(401).json({ error: 'Unauthorized' }); @@ -96,7 +121,7 @@ const requireAuth = (req, res, next) => { next(); }; -// List submissions (protected) +// List submissions app.get('/emails', requireAuth, (req, res) => { try { const { form_name, limit = 100, offset = 0 } = req.query; @@ -112,43 +137,51 @@ app.get('/emails', requireAuth, (req, res) => { query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); - const submissions = db.prepare(query).all(...params); - const countQuery = form_name - ? db.prepare('SELECT COUNT(*) as count FROM submissions WHERE form_name = ?').get(form_name) - : db.prepare('SELECT COUNT(*) as count FROM submissions').get(); + const stmt = db.prepare(query); + stmt.bind(params); - res.json({ - submissions, - total: countQuery.count, - limit: parseInt(limit), - offset: parseInt(offset) - }); + const submissions = []; + while (stmt.step()) { + submissions.push(stmt.getAsObject()); + } + stmt.free(); + + const countStmt = form_name + ? db.prepare('SELECT COUNT(*) as count FROM submissions WHERE form_name = ?') + : db.prepare('SELECT COUNT(*) as count FROM submissions'); + if (form_name) countStmt.bind([form_name]); + countStmt.step(); + const total = countStmt.getAsObject().count; + countStmt.free(); + + res.json({ submissions, total, limit: parseInt(limit), offset: parseInt(offset) }); } catch (error) { console.error('List error:', error); res.status(500).json({ error: 'Internal server error' }); } }); -// Export as CSV (protected) +// Export CSV app.get('/export', requireAuth, (req, res) => { try { const { form_name } = req.query; let query = 'SELECT email, form_name, source, created_at FROM submissions'; - const params = []; - - if (form_name) { - query += ' WHERE form_name = ?'; - params.push(form_name); - } - + if (form_name) query += ' WHERE form_name = ?'; query += ' ORDER BY created_at DESC'; - const submissions = db.prepare(query).all(...params); + const stmt = db.prepare(query); + if (form_name) stmt.bind([form_name]); + + const rows = []; + while (stmt.step()) { + rows.push(stmt.getAsObject()); + } + stmt.free(); const csv = [ 'email,form_name,source,created_at', - ...submissions.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`) + ...rows.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`) ].join('\n'); res.setHeader('Content-Type', 'text/csv'); @@ -160,41 +193,42 @@ app.get('/export', requireAuth, (req, res) => { } }); -// Stats (protected) +// Stats app.get('/stats', requireAuth, (req, res) => { try { - const stats = db.prepare(` - SELECT - form_name, - COUNT(*) as count, + const stmt = db.prepare(` + SELECT form_name, COUNT(*) as count, MIN(created_at) as first_submission, MAX(created_at) as last_submission - FROM submissions - GROUP BY form_name - ORDER BY count DESC - `).all(); + FROM submissions GROUP BY form_name ORDER BY count DESC + `); - const total = db.prepare('SELECT COUNT(*) as count FROM submissions').get(); + const stats = []; + while (stmt.step()) { + stats.push(stmt.getAsObject()); + } + stmt.free(); - res.json({ - total: total.count, - by_form: stats - }); + const totalStmt = db.prepare('SELECT COUNT(*) as count FROM submissions'); + totalStmt.step(); + const total = totalStmt.getAsObject().count; + totalStmt.free(); + + res.json({ total, by_form: stats }); } catch (error) { console.error('Stats error:', error); res.status(500).json({ error: 'Internal server error' }); } }); -// Ensure data directory exists -const fs = require('fs'); -const dataDir = path.dirname(dbPath); -if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir, { recursive: true }); -} - -app.listen(PORT, () => { - console.log(`Form capture service running on port ${PORT}`); - console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`); - console.log(`API key protection: ${API_KEY ? 'enabled' : 'disabled'}`); +// Start server after DB init +initDb().then(() => { + app.listen(PORT, () => { + console.log(`Form capture running on port ${PORT}`); + console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`); + console.log(`API key: ${API_KEY ? 'enabled' : 'disabled'}`); + }); +}).catch(err => { + console.error('Failed to init database:', err); + process.exit(1); }); diff --git a/package.json b/package.json index 85e5698..c8c8c41 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "node index.js" }, "dependencies": { - "better-sqlite3": "^11.7.0", + "sql.js": "^1.11.0", "cors": "^2.8.5", "express": "^4.21.2" },