feat: enhance complaint reporting with IP tracking and rate limiting
Build Images and Deploy / Update-PROD-Stack (push) Successful in 19s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 19s
This commit is contained in:
@@ -259,7 +259,8 @@ const ready = new Promise(resolve => { _readyResolve = resolve; });
|
||||
'ALTER TABLE users ADD COLUMN is_organizer INTEGER DEFAULT 0',
|
||||
'ALTER TABLE hunts ADD COLUMN start_date DATETIME',
|
||||
'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) {
|
||||
try { _db.run(m); } catch (e) { /* column already exists */ }
|
||||
|
||||
+23
-4
@@ -536,12 +536,31 @@ const OrganizerApplications = {
|
||||
|
||||
// ─── Complaint Reports ───────────────────────────────────
|
||||
const ComplaintReports = {
|
||||
submit({ huntId, packageId, reportedByUserId, reporterName, reporterContact, message }) {
|
||||
submit({ huntId, packageId, reportedByUserId, reporterName, reporterContact, message, reporterIp }) {
|
||||
return db.prepare(`
|
||||
INSERT INTO complaint_reports
|
||||
(hunt_id, package_id, reported_by_user_id, reporter_name, reporter_contact, message)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(huntId, packageId, reportedByUserId || null, reporterName || null, reporterContact || null, message).lastInsertRowid;
|
||||
(hunt_id, package_id, reported_by_user_id, reporter_name, reporter_contact, message, reporter_ip)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).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) {
|
||||
|
||||
@@ -111,6 +111,19 @@ router.post('/hunt/:shortName/:cardNumber/report', (req, res) => {
|
||||
const reporterName = (req.body.reporter_name || '').trim();
|
||||
const reporterContact = (req.body.reporter_contact || '').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) {
|
||||
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,
|
||||
reporterName: reporterName || null,
|
||||
reporterContact: reporterContact || null,
|
||||
reporterIp,
|
||||
message
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user