feat(feedback): Rate-Limit und Spam-Erkennung für Feedback-Formular

Schützt den Feedback-Endpunkt vor Missbrauch durch pro-Nutzer-Limits, Honeypot, Zeitprüfung und einfache Inhaltsheuristiken.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-30 14:09:43 +02:00
parent f5f12f50f5
commit d98e2e8dc0
7 changed files with 164 additions and 11 deletions
+29 -3
View File
@@ -1,6 +1,7 @@
import { Router } from 'express'
import { isNtfyConfigured, sendFeedbackViaNtfy } from '../services/ntfyNotify.js'
import { requireUser } from '../middleware/auth.js'
import { analyzeFeedbackSpam, feedbackLimiter } from '../middleware/feedbackProtection.js'
const router = Router()
@@ -25,14 +26,24 @@ router.get('/status', requireUser, (_req, res) => {
res.json({ enabled: isNtfyConfigured() })
})
router.post('/', requireUser, async (req: any, res) => {
router.post('/', requireUser, feedbackLimiter, async (req: any, res) => {
try {
if (!isNtfyConfigured()) {
return res.status(503).json({ error: 'Feedback is not configured on this server' })
}
const { category, message, username, contactEmail, logbookId, logbookTitle, appVersion, pageUrl } =
req.body ?? {}
const {
category,
message,
username,
contactEmail,
logbookId,
logbookTitle,
appVersion,
pageUrl,
website,
openedAt
} = req.body ?? {}
if (typeof category !== 'string' || !VALID_CATEGORIES.has(category)) {
return res.status(400).json({ error: 'Invalid category' })
@@ -55,6 +66,21 @@ router.post('/', requireUser, async (req: any, res) => {
}
}
const spamVerdict = analyzeFeedbackSpam(req.userId, {
message: trimmedMessage,
website,
openedAt
})
if (spamVerdict === 'silent_reject') {
return res.json({ ok: true })
}
if (spamVerdict === 'reject') {
return res.status(400).json({
error: 'This feedback could not be sent. Please change your message and try again.',
code: 'SPAM_DETECTED'
})
}
await sendFeedbackViaNtfy({
category,
message: trimmedMessage,