From d98e2e8dc0b4076cfe8523e6608a45c2d63c93b3 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 30 May 2026 14:09:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(feedback):=20Rate-Limit=20und=20Spam-Erken?= =?UTF-8?q?nung=20f=C3=BCr=20Feedback-Formular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schützt den Feedback-Endpunkt vor Missbrauch durch pro-Nutzer-Limits, Honeypot, Zeitprüfung und einfache Inhaltsheuristiken. Co-authored-by: Cursor --- client/src/App.css | 10 +++ client/src/components/FeedbackModal.tsx | 27 ++++++- client/src/i18n/locales/de.json | 4 +- client/src/i18n/locales/en.json | 4 +- client/src/services/feedback.ts | 19 +++-- server/src/middleware/feedbackProtection.ts | 79 +++++++++++++++++++++ server/src/routes/feedback.ts | 32 ++++++++- 7 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 server/src/middleware/feedbackProtection.ts diff --git a/client/src/App.css b/client/src/App.css index e8fcbec..3421dd9 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -443,6 +443,16 @@ html.scheme-dark .themed-select-option.is-selected { gap: 16px; } +.feedback-form__honeypot { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; + opacity: 0; + pointer-events: none; +} + .feedback-form__field { display: flex; flex-direction: column; diff --git a/client/src/components/FeedbackModal.tsx b/client/src/components/FeedbackModal.tsx index d0deee5..fb7f56e 100644 --- a/client/src/components/FeedbackModal.tsx +++ b/client/src/components/FeedbackModal.tsx @@ -26,9 +26,11 @@ export default function FeedbackModal({ const [category, setCategory] = useState('general') const [contactEmail, setContactEmail] = useState('') const [message, setMessage] = useState('') + const [website, setWebsite] = useState('') const [submitState, setSubmitState] = useState('idle') const [statusMessage, setStatusMessage] = useState(null) const closeTimerRef = useRef(null) + const openedAtRef = useRef(Date.now()) const isBusy = submitState === 'submitting' || submitState === 'success' @@ -58,9 +60,12 @@ export default function FeedbackModal({ setCategory('general') setContactEmail('') setMessage('') + setWebsite('') setSubmitState('idle') setStatusMessage(null) + return } + openedAtRef.current = Date.now() }, [open]) const handleSubmit = async (event: React.FormEvent) => { @@ -76,7 +81,9 @@ export default function FeedbackModal({ message: message.trim(), contactEmail: contactEmail.trim() || undefined, logbookId, - logbookTitle + logbookTitle, + openedAt: openedAtRef.current, + website }) setSubmitState('success') setStatusMessage(t('feedback.success')) @@ -91,7 +98,11 @@ export default function FeedbackModal({ ? t('feedback.error_not_configured') : error instanceof FeedbackApiError && error.code === 'INVALID_EMAIL' ? t('feedback.error_invalid_email') - : t('feedback.error_send') + : error instanceof FeedbackApiError && error.code === 'RATE_LIMITED' + ? t('feedback.error_rate_limited') + : error instanceof FeedbackApiError && error.code === 'SPAM_DETECTED' + ? t('feedback.error_spam') + : t('feedback.error_send') ) } } @@ -139,6 +150,18 @@ export default function FeedbackModal({ )}
+ +