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:
co-authored by
Claude Fable 5
parent
f30ed614e4
commit
e73b4f7e34
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user