From 484ac564d2c42be0eb1158cc549820d516306a40 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Wed, 17 Jun 2026 16:11:40 +0700 Subject: [PATCH] Validate modules pagination params --- .../app/api/modules/__tests__/route.test.ts | 33 +++++++++++++++++++ apps/web/src/app/api/modules/route.ts | 13 ++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/api/modules/__tests__/route.test.ts b/apps/web/src/app/api/modules/__tests__/route.test.ts index ab8e105..6f28a00 100644 --- a/apps/web/src/app/api/modules/__tests__/route.test.ts +++ b/apps/web/src/app/api/modules/__tests__/route.test.ts @@ -108,6 +108,39 @@ describe("GET /api/modules", () => { expect(mockRange).toHaveBeenCalledWith(10, 19); }); + it("falls back to defaults for invalid pagination values", async () => { + const req = makeRequest("http://localhost/api/modules?page=nope&limit=Infinity"); + const res = await GET(req); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.page).toBe(1); + expect(body.limit).toBe(20); + expect(mockRange).toHaveBeenCalledWith(0, 19); + }); + + it("does not truncate fractional pagination values", async () => { + const req = makeRequest("http://localhost/api/modules?page=2.5&limit=10.9"); + const res = await GET(req); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.page).toBe(1); + expect(body.limit).toBe(20); + expect(mockRange).toHaveBeenCalledWith(0, 19); + }); + + it("caps valid limit values at 50", async () => { + const req = makeRequest("http://localhost/api/modules?page=2&limit=99"); + const res = await GET(req); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.page).toBe(2); + expect(body.limit).toBe(50); + expect(mockRange).toHaveBeenCalledWith(50, 99); + }); + it("handles database errors", async () => { resetChain({ rangeResult: { data: null, error: { message: "DB error" }, count: null }, diff --git a/apps/web/src/app/api/modules/route.ts b/apps/web/src/app/api/modules/route.ts index 2a56e03..0a4b582 100644 --- a/apps/web/src/app/api/modules/route.ts +++ b/apps/web/src/app/api/modules/route.ts @@ -1,6 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; import { getSupabaseAdmin, slugify } from "@/lib/supabase"; +function parsePositiveInteger(value: string | null, fallback: number, max?: number) { + if (value === null) return fallback; + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) return fallback; + + return max === undefined ? parsed : Math.min(max, parsed); +} + async function getAuthenticatedUser(request: NextRequest) { const sb = getSupabaseAdmin(); const authHeader = request.headers.get("authorization"); @@ -26,8 +35,8 @@ export async function GET(request: NextRequest) { const search = searchParams.get("search") || ""; const category = searchParams.get("category") || ""; const sort = searchParams.get("sort") || "newest"; // newest | popular | top-rated - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const limit = Math.min(50, Math.max(1, parseInt(searchParams.get("limit") || "20", 10))); + const page = parsePositiveInteger(searchParams.get("page"), 1); + const limit = parsePositiveInteger(searchParams.get("limit"), 20, 50); const offset = (page - 1) * limit; const sb = getSupabaseAdmin();