const express = require('express'); const cors = require('cors'); const Database = require('better-sqlite3'); 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(','); // Initialize SQLite database const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'data', 'forms.db'); const db = new Database(dbPath); // 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); `); // 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); } 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' }); } // Basic email validation 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; const stmt = db.prepare(` INSERT INTO submissions (form_name, email, source, ip, user_agent) VALUES (?, ?, ?, ?, ?) `); const result = stmt.run(form_name, email.toLowerCase().trim(), source, ip, userAgent); res.json({ success: true, id: result.lastInsertRowid, message: 'Submission received' }); } catch (error) { console.error('Submit error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Auth middleware for protected routes const requireAuth = (req, res, next) => { if (!API_KEY) return next(); // No auth if no API_KEY set const authHeader = req.headers.authorization; if (!authHeader || authHeader !== `Bearer ${API_KEY}`) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }; // List submissions (protected) 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 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(); res.json({ submissions, total: countQuery.count, 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) 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); } query += ' ORDER BY created_at DESC'; const submissions = db.prepare(query).all(...params); const csv = [ 'email,form_name,source,created_at', ...submissions.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 (protected) app.get('/stats', requireAuth, (req, res) => { try { const stats = 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(); const total = db.prepare('SELECT COUNT(*) as count FROM submissions').get(); res.json({ total: total.count, 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'}`); });