diff --git a/src/app/api/directory/route.test.ts b/src/app/api/directory/route.test.ts index 65b90fb7..efab593c 100644 --- a/src/app/api/directory/route.test.ts +++ b/src/app/api/directory/route.test.ts @@ -51,4 +51,16 @@ describe("GET /api/directory", () => { "title.ilike.%100\\%\\_demo\\,\\(v1\\.2\\)\\*%,description.ilike.%100\\%\\_demo\\,\\(v1\\.2\\)\\*%" ); }); + + it("defaults partially numeric page params instead of accepting parseInt prefixes", async () => { + const chain = chainResult({ data: [], error: null, count: 0 }); + mockFrom.mockReturnValue(chain); + + const res = await GET(makeRequest({ page: "3abc" })); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(0, 19); + expect(body.page).toBe(1); + }); }); diff --git a/src/app/api/directory/route.ts b/src/app/api/directory/route.ts index b97cc47f..56d738e2 100644 --- a/src/app/api/directory/route.ts +++ b/src/app/api/directory/route.ts @@ -16,6 +16,7 @@ import { escapePostgrestSearchValue, sanitizeSearchParams, } from "@/lib/security/sanitize"; +import { parsePaginationParam } from "@/lib/api-pagination"; const LNBITS_INVOICE_KEY = process.env.LNBITS_INVOICE_KEY || ""; const MAX_DIRECTORY_PAGE = 10_000; @@ -38,10 +39,12 @@ export async function GET(request: NextRequest) { const url = new URL(request.url); const search = sanitizeSearchParams(url, "search"); const tag = sanitizeSearchParams(url, "tag"); - const parsedPage = parseInt(url.searchParams.get("page") || "1", 10); - const page = Number.isFinite(parsedPage) && parsedPage > 0 - ? Math.min(parsedPage, MAX_DIRECTORY_PAGE) - : 1; + const page = parsePaginationParam( + url.searchParams.get("page"), + 1, + 1, + MAX_DIRECTORY_PAGE + ); const limit = 20; const offset = (page - 1) * limit; diff --git a/src/app/api/prompts/route.test.ts b/src/app/api/prompts/route.test.ts index c84a5e1c..a0be8402 100644 --- a/src/app/api/prompts/route.test.ts +++ b/src/app/api/prompts/route.test.ts @@ -61,4 +61,16 @@ describe("GET /api/prompts", () => { "title.ilike.%ai\\%\\,foo\\_\\(v1\\)\\.%,description.ilike.%ai\\%\\,foo\\_\\(v1\\)\\.%,tagline.ilike.%ai\\%\\,foo\\_\\(v1\\)\\.%" ); }); + + it("defaults partially numeric page params instead of accepting parseInt prefixes", async () => { + const query = makePromptQuery(); + mockFrom.mockReturnValue(query); + + const response = await GET(makeGetRequest({ page: "4abc" })); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(query.range).toHaveBeenCalledWith(0, 19); + expect(body.page).toBe(1); + }); }); diff --git a/src/app/api/prompts/route.ts b/src/app/api/prompts/route.ts index 37db441f..eb477ac8 100644 --- a/src/app/api/prompts/route.ts +++ b/src/app/api/prompts/route.ts @@ -4,6 +4,7 @@ import { getAuthContext } from "@/lib/auth/get-user"; import { createServiceClient } from "@/lib/supabase/service"; import { promptListingSchema, slugify } from "@/lib/prompts/validation"; import { scanPrompt } from "@/lib/prompts/security-scan"; +import { parsePaginationParam } from "@/lib/api-pagination"; function escapePostgrestSearch(value: string) { return value @@ -26,8 +27,7 @@ export async function GET(request: NextRequest) { const category = url.searchParams.get("category") || ""; const tag = url.searchParams.get("tag") || ""; const sort = url.searchParams.get("sort") || "newest"; - const parsedPage = parseInt(url.searchParams.get("page") || "1", 10); - const page = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1; + const page = parsePaginationParam(url.searchParams.get("page"), 1, 1, 100_000); const limit = 20; const offset = (page - 1) * limit; diff --git a/src/lib/pagination.test.ts b/src/lib/pagination.test.ts index 5c30c846..f4f0e5b7 100644 --- a/src/lib/pagination.test.ts +++ b/src/lib/pagination.test.ts @@ -12,6 +12,7 @@ describe("parsePageParam", () => { expect(parsePageParam("-1")).toBe(1); expect(parsePageParam("0")).toBe(1); expect(parsePageParam("abc")).toBe(1); + expect(parsePageParam("2abc")).toBe(1); expect(parsePageParam("Infinity")).toBe(1); expect(parsePageParam("-Infinity")).toBe(1); }); diff --git a/src/lib/pagination.ts b/src/lib/pagination.ts index 3ef10d87..b6090974 100644 --- a/src/lib/pagination.ts +++ b/src/lib/pagination.ts @@ -4,8 +4,11 @@ export function parsePageParam( value: string | null | undefined, maxPage = DEFAULT_MAX_PAGE ) { - const parsed = parseInt(value || "1", 10); + const raw = value?.trim(); + if (!raw) return 1; + + const parsed = Number(raw); return Number.isFinite(parsed) - ? Math.min(Math.max(parsed, 1), maxPage) + ? Math.min(Math.max(Math.trunc(parsed), 1), maxPage) : 1; }