Compare commits
2 Commits
e46c1c7063
...
f30ed614e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f30ed614e4 | ||
|
|
f6d55376f8 |
186
index.js
186
index.js
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const Database = require('better-sqlite3');
|
const initSqlJs = require('sql.js');
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -8,12 +9,38 @@ const PORT = process.env.PORT || 3000;
|
|||||||
const API_KEY = process.env.API_KEY || null;
|
const API_KEY = process.env.API_KEY || null;
|
||||||
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://chatlabsai.com').split(',');
|
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://chatlabsai.com').split(',');
|
||||||
|
|
||||||
// Initialize SQLite database
|
// Database setup
|
||||||
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'data', 'forms.db');
|
const dataDir = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||||
const db = new Database(dbPath);
|
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
|
// Create tables
|
||||||
db.exec(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS submissions (
|
CREATE TABLE IF NOT EXISTS submissions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
form_name TEXT NOT NULL,
|
form_name TEXT NOT NULL,
|
||||||
@@ -22,25 +49,29 @@ db.exec(`
|
|||||||
ip TEXT,
|
ip TEXT,
|
||||||
user_agent TEXT,
|
user_agent TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
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);
|
|
||||||
`);
|
`);
|
||||||
|
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
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
// Allow requests with no origin (mobile apps, curl, etc.)
|
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) {
|
if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
}
|
}
|
||||||
return callback(new Error('Not allowed by CORS'));
|
return callback(new Error('Not allowed by CORS'));
|
||||||
},
|
},
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -58,7 +89,6 @@ app.post('/submit', (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Email is required' });
|
return res.status(400).json({ error: 'Email is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic email validation
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(email)) {
|
if (!emailRegex.test(email)) {
|
||||||
return res.status(400).json({ error: 'Invalid email format' });
|
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 ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||||
const userAgent = req.headers['user-agent'] || null;
|
const userAgent = req.headers['user-agent'] || null;
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
db.run(
|
||||||
INSERT INTO submissions (form_name, email, source, ip, user_agent)
|
`INSERT INTO submissions (form_name, email, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
||||||
VALUES (?, ?, ?, ?, ?)
|
[form_name, email.toLowerCase().trim(), source, ip, userAgent]
|
||||||
`);
|
);
|
||||||
|
|
||||||
const result = stmt.run(form_name, email.toLowerCase().trim(), source, ip, userAgent);
|
saveDb();
|
||||||
|
|
||||||
res.json({
|
res.json({ success: true, message: 'Submission received' });
|
||||||
success: true,
|
|
||||||
id: result.lastInsertRowid,
|
|
||||||
message: 'Submission received'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit error:', error);
|
console.error('Submit error:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auth middleware for protected routes
|
// Auth middleware
|
||||||
const requireAuth = (req, res, next) => {
|
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;
|
const authHeader = req.headers.authorization;
|
||||||
if (!authHeader || authHeader !== `Bearer ${API_KEY}`) {
|
if (!authHeader || authHeader !== `Bearer ${API_KEY}`) {
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
@@ -96,7 +121,7 @@ const requireAuth = (req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
// List submissions (protected)
|
// List submissions
|
||||||
app.get('/emails', requireAuth, (req, res) => {
|
app.get('/emails', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { form_name, limit = 100, offset = 0 } = req.query;
|
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 ?';
|
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||||
params.push(parseInt(limit), parseInt(offset));
|
params.push(parseInt(limit), parseInt(offset));
|
||||||
|
|
||||||
const submissions = db.prepare(query).all(...params);
|
const stmt = db.prepare(query);
|
||||||
const countQuery = form_name
|
stmt.bind(params);
|
||||||
? 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({
|
const submissions = [];
|
||||||
submissions,
|
while (stmt.step()) {
|
||||||
total: countQuery.count,
|
submissions.push(stmt.getAsObject());
|
||||||
limit: parseInt(limit),
|
}
|
||||||
offset: parseInt(offset)
|
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) {
|
} catch (error) {
|
||||||
console.error('List error:', error);
|
console.error('List error:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export as CSV (protected)
|
// Export CSV
|
||||||
app.get('/export', requireAuth, (req, res) => {
|
app.get('/export', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { form_name } = req.query;
|
const { form_name } = req.query;
|
||||||
|
|
||||||
let query = 'SELECT email, form_name, source, created_at FROM submissions';
|
let query = 'SELECT email, form_name, source, created_at FROM submissions';
|
||||||
const params = [];
|
if (form_name) query += ' WHERE form_name = ?';
|
||||||
|
|
||||||
if (form_name) {
|
|
||||||
query += ' WHERE form_name = ?';
|
|
||||||
params.push(form_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
query += ' ORDER BY created_at DESC';
|
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 = [
|
const csv = [
|
||||||
'email,form_name,source,created_at',
|
'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');
|
].join('\n');
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv');
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
@@ -160,41 +193,54 @@ app.get('/export', requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stats (protected)
|
// Stats
|
||||||
app.get('/stats', requireAuth, (req, res) => {
|
app.get('/stats', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stats = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
SELECT
|
SELECT form_name, COUNT(*) as count,
|
||||||
form_name,
|
|
||||||
COUNT(*) as count,
|
|
||||||
MIN(created_at) as first_submission,
|
MIN(created_at) as first_submission,
|
||||||
MAX(created_at) as last_submission
|
MAX(created_at) as last_submission
|
||||||
FROM submissions
|
FROM submissions GROUP BY form_name ORDER BY count DESC
|
||||||
GROUP BY form_name
|
`);
|
||||||
ORDER BY count DESC
|
|
||||||
`).all();
|
|
||||||
|
|
||||||
const total = db.prepare('SELECT COUNT(*) as count FROM submissions').get();
|
const stats = [];
|
||||||
|
while (stmt.step()) {
|
||||||
|
stats.push(stmt.getAsObject());
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
|
||||||
res.json({
|
const totalStmt = db.prepare('SELECT COUNT(*) as count FROM submissions');
|
||||||
total: total.count,
|
totalStmt.step();
|
||||||
by_form: stats
|
const total = totalStmt.getAsObject().count;
|
||||||
});
|
totalStmt.free();
|
||||||
|
|
||||||
|
res.json({ total, by_form: stats });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Stats error:', error);
|
console.error('Stats error:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Delete all submissions (protected)
|
||||||
const fs = require('fs');
|
app.delete('/clear', requireAuth, (req, res) => {
|
||||||
const dataDir = path.dirname(dbPath);
|
try {
|
||||||
if (!fs.existsSync(dataDir)) {
|
db.run('DELETE FROM submissions');
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
saveDb();
|
||||||
|
res.json({ success: true, message: 'All submissions deleted' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Clear error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Form capture service running on port ${PORT}`);
|
// Start server after DB init
|
||||||
console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`);
|
initDb().then(() => {
|
||||||
console.log(`API key protection: ${API_KEY ? 'enabled' : 'disabled'}`);
|
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.7.0",
|
"sql.js": "^1.11.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user