feat: enhance complaint reporting with IP tracking and rate limiting
Build Images and Deploy / Update-PROD-Stack (push) Successful in 19s

This commit is contained in:
2026-04-29 22:48:57 -04:00
parent 6d6d48b301
commit 6b5b8350af
3 changed files with 39 additions and 5 deletions
+2 -1
View File
@@ -259,7 +259,8 @@ const ready = new Promise(resolve => { _readyResolve = resolve; });
'ALTER TABLE users ADD COLUMN is_organizer INTEGER DEFAULT 0', 'ALTER TABLE users ADD COLUMN is_organizer INTEGER DEFAULT 0',
'ALTER TABLE hunts ADD COLUMN start_date DATETIME', 'ALTER TABLE hunts ADD COLUMN start_date DATETIME',
'ALTER TABLE hunts ADD COLUMN hidden_until_start INTEGER DEFAULT 0', 'ALTER TABLE hunts ADD COLUMN hidden_until_start INTEGER DEFAULT 0',
'ALTER TABLE users ADD COLUMN display_name TEXT' 'ALTER TABLE users ADD COLUMN display_name TEXT',
'ALTER TABLE complaint_reports ADD COLUMN reporter_ip TEXT'
]; ];
for (const m of migrations) { for (const m of migrations) {
try { _db.run(m); } catch (e) { /* column already exists */ } try { _db.run(m); } catch (e) { /* column already exists */ }
+23 -4
View File
@@ -536,12 +536,31 @@ const OrganizerApplications = {
// ─── Complaint Reports ─────────────────────────────────── // ─── Complaint Reports ───────────────────────────────────
const ComplaintReports = { const ComplaintReports = {
submit({ huntId, packageId, reportedByUserId, reporterName, reporterContact, message }) { submit({ huntId, packageId, reportedByUserId, reporterName, reporterContact, message, reporterIp }) {
return db.prepare(` return db.prepare(`
INSERT INTO complaint_reports INSERT INTO complaint_reports
(hunt_id, package_id, reported_by_user_id, reporter_name, reporter_contact, message) (hunt_id, package_id, reported_by_user_id, reporter_name, reporter_contact, message, reporter_ip)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(huntId, packageId, reportedByUserId || null, reporterName || null, reporterContact || null, message).lastInsertRowid; `).run(huntId, packageId, reportedByUserId || null, reporterName || null, reporterContact || null, message, reporterIp || null).lastInsertRowid;
},
hasOpenFromIp(packageId, ip) {
if (!ip) return false;
const row = db.prepare(`
SELECT id FROM complaint_reports
WHERE package_id = ? AND reporter_ip = ? AND status = 'open'
LIMIT 1
`).get(packageId, ip);
return !!row;
},
countRecentFromIp(ip, windowMinutes = 60) {
if (!ip) return 0;
const row = db.prepare(`
SELECT COUNT(*) as cnt FROM complaint_reports
WHERE reporter_ip = ? AND created_at > datetime('now', ? || ' minutes')
`).get(ip, `-${windowMinutes}`);
return row ? row.cnt : 0;
}, },
getOpenByHunt(huntId) { getOpenByHunt(huntId) {
+14
View File
@@ -111,6 +111,19 @@ router.post('/hunt/:shortName/:cardNumber/report', (req, res) => {
const reporterName = (req.body.reporter_name || '').trim(); const reporterName = (req.body.reporter_name || '').trim();
const reporterContact = (req.body.reporter_contact || '').trim(); const reporterContact = (req.body.reporter_contact || '').trim();
const message = (req.body.message || '').trim(); const message = (req.body.message || '').trim();
const reporterIp = req.ip || null;
// Rate limit: max 5 reports per IP per hour
if (reporterIp && ComplaintReports.countRecentFromIp(reporterIp, 60) >= 5) {
req.session.flash = { type: 'danger', message: 'Too many reports submitted recently. Please wait a while before trying again.' };
return res.redirect(`/hunt/${pkg.hunt_short_name}/${pkg.card_number}/report`);
}
// Duplicate suppression: same IP already has an open complaint for this package
if (reporterIp && ComplaintReports.hasOpenFromIp(pkg.id, reporterIp)) {
req.session.flash = { type: 'warning', message: 'You\'ve already submitted a report for this item and it\'s still being reviewed.' };
return res.redirect(`/hunt/${pkg.hunt_short_name}/${pkg.card_number}`);
}
if (!message || message.length < 10) { if (!message || message.length < 10) {
req.session.flash = { type: 'danger', message: 'Please include a short description (at least 10 characters).' }; req.session.flash = { type: 'danger', message: 'Please include a short description (at least 10 characters).' };
@@ -127,6 +140,7 @@ router.post('/hunt/:shortName/:cardNumber/report', (req, res) => {
reportedByUserId: req.session && req.session.userId ? req.session.userId : null, reportedByUserId: req.session && req.session.userId ? req.session.userId : null,
reporterName: reporterName || null, reporterName: reporterName || null,
reporterContact: reporterContact || null, reporterContact: reporterContact || null,
reporterIp,
message message
}); });