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 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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user