refactor: remove all rate limiting across the application (Task 11)

This commit is contained in:
2026-05-28 00:24:33 +08:00
parent ea4907cf4b
commit 893f6356c6
8 changed files with 67 additions and 286 deletions
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest"
import fs from "node:fs"
import path from "node:path"
describe("rate limiting removal", () => {
const cwd = process.cwd()
it("lib/auth/rate-limit.ts no longer exports the rateLimit function", () => {
const filePath = path.join(cwd, "lib", "auth", "rate-limit.ts")
expect(fs.existsSync(filePath)).toBe(false)
})
it("lib/api/app.ts no longer imports or uses rateLimit", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "app.ts"),
"utf8",
)
expect(content).not.toContain('import { rateLimit }')
expect(content).not.toContain('.use(rateLimit(')
})
it("lib/auth.ts has Better Auth rateLimit disabled", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "auth.ts"),
"utf8",
)
const hasRateLimitBlock = content.includes("rateLimit:")
if (hasRateLimitBlock) {
expect(content).toContain("enabled: false")
}
})
it("lib/api/contact.ts no longer calls checkContactRateLimit", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "contact.ts"),
"utf8",
)
expect(content).not.toContain("checkContactRateLimit")
})
it("lib/api/comments.ts no longer enforces hourly cap", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "comments.ts"),
"utf8",
)
expect(content).not.toContain("hourly limit")
expect(content).not.toContain('"Duplicate comment detected')
})
it("lib/api/performance-submit.ts no longer enforces submission cooldown", () => {
const content = fs.readFileSync(
path.join(cwd, "lib", "api", "performance-submit.ts"),
"utf8",
)
expect(content).not.toContain("Submission cooldown")
expect(content).not.toContain("sixtySecondsAgo")
expect(content).not.toContain("Please wait before submitting")
})
})
+6 -12
View File
@@ -2,7 +2,6 @@ import { Elysia, t } from "elysia"
import { openapi } from "@elysia/openapi"
import { cron, Patterns } from "@elysia/cron"
import { auth } from "@/lib/auth"
import { rateLimit } from "@/lib/auth/rate-limit"
import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "@/lib/auth/domain-block"
import { taskRegistry } from "./cron"
import {
@@ -154,10 +153,9 @@ export const app = new Elysia({ prefix: "/api" })
error: code === "NOT_FOUND" ? "Not found" : "Internal server error",
}
})
// ── Auth routes (auth rate limit + betterAuth) ──────────────
// ── Auth routes ─────────────────────────────────────────────
.group("", (app) =>
app
.use(rateLimit("auth"))
.onBeforeHandle(async ({ request, set }) => {
const url = new URL(request.url)
const isSignUp =
@@ -214,10 +212,9 @@ export const app = new Elysia({ prefix: "/api" })
},
)
)
// ── Read-heavy public routes (read rate limit) ───────────────
// ── Read-heavy public routes ──────────────────────────────
.group("", (app) =>
app
.use(rateLimit("read"))
.use(healthRoutes)
.use(gamesRoutes)
.use(gameVersionsRoutes)
@@ -246,10 +243,9 @@ export const app = new Elysia({ prefix: "/api" })
.use(screenshotRoutes)
.use(mobileRoutes)
)
// ── Write routes (write rate limit + betterAuth) ─────────────
// ── Write routes ───────────────────────────────────────────
.group("", (app) =>
app
.use(rateLimit("write"))
.use(betterAuth)
.use(clientVersionRoutes)
.use(performanceVerifyRoutes)
@@ -262,17 +258,15 @@ export const app = new Elysia({ prefix: "/api" })
.use(adminStorageRoutes)
.use(adminAnalyticsRoutes)
)
// ── Strict rate limit (public forms, no auth) ───────────────
// ── Public forms (no auth) ─────────────────────────────────
.group("", (app) =>
app
.use(rateLimit("strict"))
.use(contactRoutes)
.use(communitySuggestionRoutes)
)
// ── Cron (no rate limit) ─────────────────────────────────────
// ── Cron ─────────────────────────────────────────────────────
.use(cronRoutes)
// ── Root (default rate limit) ────────────────────────────────
.use(rateLimit("default"))
// ── Root ────────────────────────────────────────────────────
.get("/", () => ({
name: "DeckyVault API",
version: "2026.2.2",
-46
View File
@@ -119,52 +119,6 @@ export const commentsRoutes = new Elysia({
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 (body.parentId) {
const [parent] = await db
+1 -46
View File
@@ -1,42 +1,5 @@
import { Elysia, t } from "elysia"
// ── In-memory rate limiter for contact form ──────────────────────
const contactLimiter = new Map<string, { count: number; resetAt: number }>()
const RATE_LIMIT_MAX = 3
const RATE_LIMIT_WINDOW = 60 * 60 * 1000 // 1 hour in ms
// Clean up expired entries every 5 minutes
setInterval(() => {
const now = Date.now()
for (const [key, entry] of contactLimiter) {
if (now > entry.resetAt) contactLimiter.delete(key)
}
}, 5 * 60 * 1000)
function getClientIP(request: Request): string {
const forwarded = request.headers.get("x-forwarded-for")
if (forwarded) return forwarded.split(",")[0].trim()
return "unknown"
}
function checkContactRateLimit(ip: string): { allowed: boolean; retryAfter: number } {
const now = Date.now()
const entry = contactLimiter.get(ip)
if (!entry || now > entry.resetAt) {
contactLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW })
return { allowed: true, retryAfter: 0 }
}
if (entry.count >= RATE_LIMIT_MAX) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000)
return { allowed: false, retryAfter }
}
entry.count++
return { allowed: true, retryAfter: 0 }
}
// ── Discord embed colors by category ─────────────────────────────
const CATEGORY_COLORS: Record<string, number> = {
bug: 0xe74c3c, // red
@@ -87,14 +50,6 @@ export const contactRoutes = new Elysia({ prefix: "/contact", detail: { tags: ["
}
}
// ── Rate limit ──────────────────────────────────────────────
const ip = getClientIP(request)
const rateCheck = checkContactRateLimit(ip)
if (!rateCheck.allowed) {
set.status = 429
return { error: "Too many submissions. Please try again later.", retryAfter: rateCheck.retryAfter }
}
// ── Validate category ──────────────────────────────────────
if (!VALID_CATEGORIES.includes(payload.category)) {
set.status = 400
@@ -149,7 +104,7 @@ export const contactRoutes = new Elysia({ prefix: "/contact", detail: { tags: ["
...(payload.name ? [{ name: "Name", value: payload.name, inline: true }] : []),
...(payload.email ? [{ name: "Email", value: payload.email, inline: true }] : []),
...(payload.gameUrl ? [{ name: "Game URL", value: payload.gameUrl, inline: false }] : []),
{ name: "IP Hash", value: `\`${ip.slice(0, 8)}...\``, inline: true },
{ name: "IP Hash", value: `\`${(request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown").slice(0, 8)}...\``, inline: true },
],
timestamp: new Date().toISOString(),
}
-25
View File
@@ -145,31 +145,6 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance", deta
return { error: "Missing required fields: versionId, hardwareSlug, fpsAvg" }
}
// ── Submission cooldown: 60 seconds between entries per user ──
const sixtySecondsAgo = new Date(Date.now() - 60 * 1000)
const [lastEntry] = await db
.select({ createdAt: performanceEntries.createdAt })
.from(performanceEntries)
.where(
and(
eq(performanceEntries.userId, guard.user.id),
sql`${performanceEntries.createdAt} >= ${sixtySecondsAgo}`,
),
)
.orderBy(sql`${performanceEntries.createdAt} DESC`)
.limit(1)
if (lastEntry) {
const retryAfter = Math.ceil(
(lastEntry.createdAt.getTime() + 60_000 - Date.now()) / 1000
)
set.status = 429
return {
error: "Please wait before submitting another benchmark",
retryAfter: Math.max(1, retryAfter),
}
}
// ── Validation: fpsAvg bounds ───────────────────────────────
if (typeof fpsAvg !== "number" || fpsAvg < 1 || fpsAvg > 500) {
set.status = 400