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
+15 -4
View File
@@ -3,11 +3,11 @@ import { apiFetch } from './api.js'
export type FeedbackCategory = 'bug' | 'feature' | 'general'
export class FeedbackApiError extends Error {
code: 'NOT_CONFIGURED' | 'REQUEST_FAILED' | 'INVALID_EMAIL'
code: 'NOT_CONFIGURED' | 'REQUEST_FAILED' | 'INVALID_EMAIL' | 'RATE_LIMITED' | 'SPAM_DETECTED'
constructor(
message: string,
code: 'NOT_CONFIGURED' | 'REQUEST_FAILED' | 'INVALID_EMAIL' = 'REQUEST_FAILED'
code: 'NOT_CONFIGURED' | 'REQUEST_FAILED' | 'INVALID_EMAIL' | 'RATE_LIMITED' | 'SPAM_DETECTED' = 'REQUEST_FAILED'
) {
super(message)
this.name = 'FeedbackApiError'
@@ -27,6 +27,8 @@ export async function sendFeedback(payload: {
contactEmail?: string | null
logbookId?: string | null
logbookTitle?: string | null
openedAt: number
website?: string
}): Promise<void> {
const contactEmail = payload.contactEmail?.trim()
if (contactEmail && !isValidFeedbackEmail(contactEmail)) {
@@ -43,7 +45,9 @@ export async function sendFeedback(payload: {
logbookId: payload.logbookId || undefined,
logbookTitle: payload.logbookTitle || undefined,
appVersion: typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : undefined,
pageUrl: window.location.href
pageUrl: window.location.href,
openedAt: payload.openedAt,
website: payload.website || undefined
})
})
@@ -51,8 +55,15 @@ export async function sendFeedback(payload: {
throw new FeedbackApiError('Feedback is not configured on this server', 'NOT_CONFIGURED')
}
if (res.status === 429) {
throw new FeedbackApiError('Too many feedback submissions', 'RATE_LIMITED')
}
const data = await res.json().catch(() => ({}))
if (!res.ok) {
throw new FeedbackApiError(data.error || 'Failed to send feedback')
throw new FeedbackApiError(
data.error || 'Failed to send feedback',
data.code === 'SPAM_DETECTED' ? 'SPAM_DETECTED' : 'REQUEST_FAILED'
)
}
}