feat(security): add comment anti-spam (content length cap, duplicate detection, hourly limit)

This commit is contained in:
2026-05-15 00:47:33 +08:00
parent b895e8714d
commit 470e374656
+53
View File
@@ -112,6 +112,59 @@ export const commentsRoutes = new Elysia({
return { error: guard.error } return { error: guard.error }
} }
// ── Anti-spam: content length cap ──────────────────────────
const contentStr = JSON.stringify(body.content)
if (contentStr.length > 50000) {
set.status = 413
return { error: "Comment content exceeds maximum size (50KB)" }
}
// ── Anti-spam: duplicate detection ─────────────────────────
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000)
const [duplicate] = await db
.select({ id: gameComments.id })
.from(gameComments)
.where(
and(
eq(gameComments.gameId, params.gameId),
eq(gameComments.userId, guard.user.id),
eq(gameComments.isRemoved, false),
sql`${gameComments.createdAt} >= ${fiveMinutesAgo}`,
),
)
.limit(1)
if (duplicate) {
const [recent] = await db
.select({ content: gameComments.content })
.from(gameComments)
.where(eq(gameComments.id, duplicate.id))
.limit(1)
if (recent && JSON.stringify(recent.content) === contentStr) {
set.status = 409
return { error: "Duplicate comment detected — you posted identical content in the last 5 minutes" }
}
}
// ── Anti-spam: per-user hourly cap ─────────────────────────
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000)
const [{ count: recentCount }] = await db
.select({ count: sql<number>`count(*)::int` })
.from(gameComments)
.where(
and(
eq(gameComments.userId, guard.user.id),
eq(gameComments.isRemoved, false),
sql`${gameComments.createdAt} >= ${oneHourAgo}`,
),
)
if (recentCount >= 30) {
set.status = 429
return { error: "Too many comments — you've reached the hourly limit of 30" }
}
// If parentId is provided, verify it exists and belongs to the same game // If parentId is provided, verify it exists and belongs to the same game
if (body.parentId) { if (body.parentId) {
const [parent] = await db const [parent] = await db