feat(security): add rate limiting to public POST routes
Add rateLimit protection to /api/paypal/create, /api/paypal/capture, /api/tokens, /api/radio/shouts, and /api/articles/[slug]/comment to prevent abuse and spamming.
This commit is contained in:
1 parent
1caef76f82
commit
ec742a3f72
5 files changed
+31
No files matched your search
@@ -10,6 +10,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { apiError, apiJson } from "@/lib/api";
|
import { apiError, apiJson } from "@/lib/api";
|
||||||
import { bearerUserId } from "@/lib/api-auth";
|
import { bearerUserId } from "@/lib/api-auth";
|
||||||
import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db";
|
import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -18,6 +19,10 @@ export async function POST(
|
|||||||
const uid = await bearerUserId(req);
|
const uid = await bearerUserId(req);
|
||||||
if (!uid) return apiError("Unauthorized", 401);
|
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 { slug } = await params;
|
||||||
|
|
||||||
const body = (await req.json().catch(() => ({}))) as { comment?: unknown };
|
const body = (await req.json().catch(() => ({}))) as { comment?: unknown };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { sessionUserId } from "@/lib/auth/session-user";
|
|||||||
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
import { logServerError } from "@/lib/server-log";
|
import { logServerError } from "@/lib/server-log";
|
||||||
import {
|
import {
|
||||||
type CaptureResult,
|
type CaptureResult,
|
||||||
@@ -50,6 +51,13 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
return NextResponse.json({ error: "Invalid session." }, { status: 401 });
|
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()) {
|
if (!isPayPalConfigured()) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { sessionUserId } from "@/lib/auth/session-user";
|
|||||||
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
import {
|
import {
|
||||||
createOrder,
|
createOrder,
|
||||||
creditsPerUnit,
|
creditsPerUnit,
|
||||||
@@ -36,6 +37,13 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
return NextResponse.json({ error: "Invalid session." }, { status: 401 });
|
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.
|
// Fail fast (and clearly) when the sandbox/live keys aren't set.
|
||||||
if (!isPayPalConfigured()) {
|
if (!isPayPalConfigured()) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { desc, inArray } from "drizzle-orm";
|
|||||||
import { apiError, apiJson } from "@/lib/api";
|
import { apiError, apiJson } from "@/lib/api";
|
||||||
import { bearerUserId } from "@/lib/api-auth";
|
import { bearerUserId } from "@/lib/api-auth";
|
||||||
import { db, RadioShouts, User } from "@/lib/db";
|
import { db, RadioShouts, User } from "@/lib/db";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
|
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
|
||||||
|
|
||||||
// Latest 50 radio shouts with their author's username/look resolved. Mirrors the
|
// 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);
|
const uid = await bearerUserId(req);
|
||||||
if (!uid) return apiError("Unauthorized", 401);
|
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 body = (await req.json().catch(() => ({}))) as { message?: unknown };
|
||||||
const message = typeof body.message === "string" ? body.message.trim() : "";
|
const message = typeof body.message === "string" ? body.message.trim() : "";
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { apiError, apiJson } from "@/lib/api";
|
|||||||
import { issueToken } from "@/lib/api-auth";
|
import { issueToken } from "@/lib/api-auth";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { sessionUserId } from "@/lib/auth/session-user";
|
import { sessionUserId } from "@/lib/auth/session-user";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@@ -17,6 +18,10 @@ export async function POST(req: Request) {
|
|||||||
return apiError("Unauthorized", 401);
|
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 body = (await req.json().catch(() => ({}))) as { name?: unknown };
|
||||||
const rawName = typeof body.name === "string" ? body.name.trim() : "";
|
const rawName = typeof body.name === "string" ? body.name.trim() : "";
|
||||||
const name = rawName ? rawName.slice(0, 100) : "api";
|
const name = rawName ? rawName.slice(0, 100) : "api";
|
||||||
|
|||||||
Reference in new issue
Block a user