diff --git a/src/app/api/articles/[slug]/comment/route.ts b/src/app/api/articles/[slug]/comment/route.ts index 7c5ee8d2..9125eb56 100644 --- a/src/app/api/articles/[slug]/comment/route.ts +++ b/src/app/api/articles/[slug]/comment/route.ts @@ -10,6 +10,7 @@ import { eq } from "drizzle-orm"; import { apiError, apiJson } from "@/lib/api"; import { bearerUserId } from "@/lib/api-auth"; import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db"; +import { rateLimit } from "@/lib/rate-limit"; export async function POST( req: Request, @@ -18,6 +19,10 @@ export async function POST( const uid = await bearerUserId(req); if (!uid) return apiError("Unauthorized", 401); + if (!(await rateLimit(`article-comment:${uid}`, 10, 60_000)).ok) { + return apiError("Too many comments. Please wait a minute.", 429); + } + const { slug } = await params; const body = (await req.json().catch(() => ({}))) as { comment?: unknown }; diff --git a/src/app/api/paypal/capture/route.ts b/src/app/api/paypal/capture/route.ts index 1cb99a57..b2b4ff19 100644 --- a/src/app/api/paypal/capture/route.ts +++ b/src/app/api/paypal/capture/route.ts @@ -6,6 +6,7 @@ import { sessionUserId } from "@/lib/auth/session-user"; import { db, WebsitePaypalTransactions } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; import { logger } from "@/lib/logger"; +import { rateLimit } from "@/lib/rate-limit"; import { logServerError } from "@/lib/server-log"; import { type CaptureResult, @@ -50,6 +51,13 @@ export async function POST(req: Request): Promise { return NextResponse.json({ error: "Invalid session." }, { status: 401 }); } + if (!(await rateLimit(`paypal-capture:${userId}`, 5, 60_000)).ok) { + return NextResponse.json( + { error: "Too many capture requests. Please wait a minute." }, + { status: 429 }, + ); + } + if (!isPayPalConfigured()) { return NextResponse.json( { diff --git a/src/app/api/paypal/create/route.ts b/src/app/api/paypal/create/route.ts index fd14dcb4..027aac3f 100644 --- a/src/app/api/paypal/create/route.ts +++ b/src/app/api/paypal/create/route.ts @@ -5,6 +5,7 @@ import { sessionUserId } from "@/lib/auth/session-user"; import { db, WebsitePaypalTransactions } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; import { logger } from "@/lib/logger"; +import { rateLimit } from "@/lib/rate-limit"; import { createOrder, creditsPerUnit, @@ -36,6 +37,13 @@ export async function POST(req: Request): Promise { return NextResponse.json({ error: "Invalid session." }, { status: 401 }); } + if (!(await rateLimit(`paypal-create:${userId}`, 5, 60_000)).ok) { + return NextResponse.json( + { error: "Too many top-up requests. Please wait a minute." }, + { status: 429 }, + ); + } + // Fail fast (and clearly) when the sandbox/live keys aren't set. if (!isPayPalConfigured()) { return NextResponse.json( diff --git a/src/app/api/radio/shouts/route.ts b/src/app/api/radio/shouts/route.ts index fc1fc33d..8688fe3e 100644 --- a/src/app/api/radio/shouts/route.ts +++ b/src/app/api/radio/shouts/route.ts @@ -2,6 +2,7 @@ import { desc, inArray } from "drizzle-orm"; import { apiError, apiJson } from "@/lib/api"; import { bearerUserId } from "@/lib/api-auth"; import { db, RadioShouts, User } from "@/lib/db"; +import { rateLimit } from "@/lib/rate-limit"; import { apiCacheKey, redisCache } from "@/lib/redis-cache"; // Latest 50 radio shouts with their author's username/look resolved. Mirrors the @@ -71,6 +72,10 @@ export async function POST(req: Request) { const uid = await bearerUserId(req); if (!uid) return apiError("Unauthorized", 401); + if (!(await rateLimit(`radio-shout:${uid}`, 10, 60_000)).ok) { + return apiError("Too many shouts. Please wait a minute.", 429); + } + const body = (await req.json().catch(() => ({}))) as { message?: unknown }; const message = typeof body.message === "string" ? body.message.trim() : ""; diff --git a/src/app/api/tokens/route.ts b/src/app/api/tokens/route.ts index 4f6a00d5..54a238d9 100644 --- a/src/app/api/tokens/route.ts +++ b/src/app/api/tokens/route.ts @@ -9,6 +9,7 @@ import { apiError, apiJson } from "@/lib/api"; import { issueToken } from "@/lib/api-auth"; import { auth } from "@/lib/auth"; import { sessionUserId } from "@/lib/auth/session-user"; +import { rateLimit } from "@/lib/rate-limit"; export async function POST(req: Request) { const session = await auth(); @@ -17,6 +18,10 @@ export async function POST(req: Request) { return apiError("Unauthorized", 401); } + if (!(await rateLimit(`tokens-issue:${id}`, 5, 60_000)).ok) { + return apiError("Too many token requests. Please wait a minute.", 429); + } + const body = (await req.json().catch(() => ({}))) as { name?: unknown }; const rawName = typeof body.name === "string" ? body.name.trim() : ""; const name = rawName ? rawName.slice(0, 100) : "api";