Add optional message field to /submit for iOS review intercept

- /submit now requires email OR message (email-only calls unchanged)
- message trimmed + capped at 10k chars; stored via guarded ALTER TABLE migration
- /export CSV gains message column with proper quoting/escaping
- test suite (node --test): 9 tests covering compat, validation, truncation, CSV escaping

Ticket: T2 of review-flow-10x epic (chatai-ios#136)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WPnLnXS4YRz9vVt1BEdeR
This commit is contained in:
Krishna Kumar
2026-07-22 12:22:30 -05:00
co-authored by Claude Fable 5
parent f30ed614e4
commit e73b4f7e34
5 changed files with 1033 additions and 14 deletions
+33 -10
View File
@@ -54,6 +54,15 @@ async function initDb() {
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)`);
// Migration: add message column for free-text submissions (e.g. review intercept)
const columns = [];
const pragma = db.prepare(`PRAGMA table_info(submissions)`);
while (pragma.step()) columns.push(pragma.getAsObject().name);
pragma.free();
if (!columns.includes('message')) {
db.run(`ALTER TABLE submissions ADD COLUMN message TEXT`);
}
saveDb();
// Auto-save every 30 seconds
@@ -84,22 +93,30 @@ app.get('/health', (req, res) => {
app.post('/submit', (req, res) => {
try {
const { email, form_name = 'default', source = null } = req.body;
let { message = null } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
if (message != null) {
message = String(message).trim().slice(0, 10000) || null;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email format' });
// Either a valid email or a free-text message is required
if (!email && !message) {
return res.status(400).json({ error: 'Email or message is required' });
}
if (email) {
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]
`INSERT INTO submissions (form_name, email, source, ip, user_agent, message, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`,
[form_name, email ? email.toLowerCase().trim() : '', source, ip, userAgent, message]
);
saveDb();
@@ -166,7 +183,7 @@ app.get('/export', requireAuth, (req, res) => {
try {
const { form_name } = req.query;
let query = 'SELECT email, form_name, source, created_at FROM submissions';
let query = 'SELECT email, form_name, source, message, created_at FROM submissions';
if (form_name) query += ' WHERE form_name = ?';
query += ' ORDER BY created_at DESC';
@@ -179,9 +196,15 @@ app.get('/export', requireAuth, (req, res) => {
}
stmt.free();
// Quote fields that may contain commas, quotes, or newlines
const csvEscape = (v) => {
const s = v == null ? '' : String(v);
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
const csv = [
'email,form_name,source,created_at',
...rows.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`)
'email,form_name,source,message,created_at',
...rows.map(s => [s.email, s.form_name, s.source, s.message, s.created_at].map(csvEscape).join(','))
].join('\n');
res.setHeader('Content-Type', 'text/csv');