const express = require('express'); const cors = require('cors'); const initSqlJs = require('sql.js'); const fs = require('fs'); const path = require('path'); const app = express(); 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(','); // Database setup const dataDir = process.env.DATA_DIR || path.join(__dirname, 'data'); const dbPath = path.join(dataDir, 'forms.db'); let db; // 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) => { if (!origin) return callback(null, true); if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) { return callback(null, true); } return callback(new Error('Not allowed by CORS')); }, methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'] })); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Submit form app.post('/submit', (req, res) => { try { const { email, form_name = 'default', source = null } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ error: 'Invalid email format' }); } const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent'] || null; 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] ); saveDb(); res.json({ success: true, message: 'Submission received' }); } catch (error) { console.error('Submit error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Auth middleware const requireAuth = (req, res, next) => { if (!API_KEY) return next(); const authHeader = req.headers.authorization; if (!authHeader || authHeader !== `Bearer ${API_KEY}`) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }; // List submissions app.get('/emails', requireAuth, (req, res) => { try { const { form_name, limit = 100, offset = 0 } = req.query; let query = 'SELECT * FROM submissions'; const params = []; if (form_name) { query += ' WHERE form_name = ?'; params.push(form_name); } query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); const stmt = db.prepare(query); stmt.bind(params); 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 CSV app.get('/export', requireAuth, (req, res) => { try { const { form_name } = req.query; let query = 'SELECT email, form_name, source, created_at FROM submissions'; if (form_name) query += ' WHERE form_name = ?'; query += ' ORDER BY created_at DESC'; 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', ...rows.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`) ].join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename=submissions-${Date.now()}.csv`); res.send(csv); } catch (error) { console.error('Export error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Stats app.get('/stats', requireAuth, (req, res) => { try { 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 `); const stats = []; while (stmt.step()) { stats.push(stmt.getAsObject()); } stmt.free(); 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' }); } }); // 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); });