diff --git a/apps/web/src/app/api/orgs/[id]/detections/route.ts b/apps/web/src/app/api/orgs/[id]/detections/route.ts index 878bcec..41fcc8f 100644 --- a/apps/web/src/app/api/orgs/[id]/detections/route.ts +++ b/apps/web/src/app/api/orgs/[id]/detections/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { parsePaginationParam } from "@/lib/pagination"; import { getSupabaseAdmin } from "@/lib/supabase"; // GET /api/orgs/[id]/detections — List detections for org @@ -28,8 +29,8 @@ export async function GET( const ruleId = url.searchParams.get("rule_id"); const since = url.searchParams.get("since"); const until = url.searchParams.get("until"); - const limit = Math.min(parseInt(url.searchParams.get("limit") || "50"), 200); - const offset = parseInt(url.searchParams.get("offset") || "0"); + const limit = parsePaginationParam(url.searchParams.get("limit"), 50, { min: 1, max: 200 }); + const offset = parsePaginationParam(url.searchParams.get("offset"), 0); let query = admin.from("detections").select("*", { count: "exact" }) .eq("organization_id", orgId) diff --git a/apps/web/src/app/api/orgs/[id]/remediations/route.ts b/apps/web/src/app/api/orgs/[id]/remediations/route.ts index ab7cf1c..09bf563 100644 --- a/apps/web/src/app/api/orgs/[id]/remediations/route.ts +++ b/apps/web/src/app/api/orgs/[id]/remediations/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { parsePaginationParam } from "@/lib/pagination"; import { getSupabaseAdmin } from "@/lib/supabase"; // GET /api/orgs/[id]/remediations @@ -24,8 +25,8 @@ export async function GET( const url = new URL(req.url); const serverId = url.searchParams.get("server_id"); const status = url.searchParams.get("status"); - const limit = Math.min(parseInt(url.searchParams.get("limit") || "50"), 200); - const offset = parseInt(url.searchParams.get("offset") || "0"); + const limit = parsePaginationParam(url.searchParams.get("limit"), 50, { min: 1, max: 200 }); + const offset = parsePaginationParam(url.searchParams.get("offset"), 0); let query = admin.from("remediation_actions").select("*", { count: "exact" }) .eq("organization_id", orgId) diff --git a/apps/web/src/app/api/orgs/[id]/servers/[server_id]/detections/route.ts b/apps/web/src/app/api/orgs/[id]/servers/[server_id]/detections/route.ts index dd950df..ca3ae6c 100644 --- a/apps/web/src/app/api/orgs/[id]/servers/[server_id]/detections/route.ts +++ b/apps/web/src/app/api/orgs/[id]/servers/[server_id]/detections/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { parsePaginationParam } from "@/lib/pagination"; import { getSupabaseAdmin } from "@/lib/supabase"; // GET /api/orgs/[id]/servers/[server_id]/detections — Server-scoped detections @@ -24,8 +25,8 @@ export async function GET( const url = new URL(req.url); const severity = url.searchParams.get("severity"); const status = url.searchParams.get("status"); - const limit = Math.min(parseInt(url.searchParams.get("limit") || "50"), 200); - const offset = parseInt(url.searchParams.get("offset") || "0"); + const limit = parsePaginationParam(url.searchParams.get("limit"), 50, { min: 1, max: 200 }); + const offset = parsePaginationParam(url.searchParams.get("offset"), 0); let query = admin.from("detections").select("*", { count: "exact" }) .eq("organization_id", orgId) diff --git a/apps/web/src/lib/__tests__/pagination.test.ts b/apps/web/src/lib/__tests__/pagination.test.ts new file mode 100644 index 0000000..8a44332 --- /dev/null +++ b/apps/web/src/lib/__tests__/pagination.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { parsePaginationParam } from "@/lib/pagination"; + +describe("parsePaginationParam", () => { + it("returns the default for missing, malformed, or fractional values", () => { + expect(parsePaginationParam(null, 50, { min: 1, max: 200 })).toBe(50); + expect(parsePaginationParam("abc", 50, { min: 1, max: 200 })).toBe(50); + expect(parsePaginationParam("10.5", 50, { min: 1, max: 200 })).toBe(50); + }); + + it("clamps valid integers to the configured range", () => { + expect(parsePaginationParam("-5", 0)).toBe(0); + expect(parsePaginationParam("0", 50, { min: 1, max: 200 })).toBe(1); + expect(parsePaginationParam("500", 50, { min: 1, max: 200 })).toBe(200); + }); +}); + diff --git a/apps/web/src/lib/pagination.ts b/apps/web/src/lib/pagination.ts new file mode 100644 index 0000000..58c760b --- /dev/null +++ b/apps/web/src/lib/pagination.ts @@ -0,0 +1,19 @@ +export function parsePaginationParam( + value: string | null, + defaultValue: number, + options: { min?: number; max?: number } = {}, +) { + if (value === null || value.trim() === "") { + return defaultValue; + } + + const parsed = Number(value); + const min = options.min ?? 0; + const max = options.max ?? Number.MAX_SAFE_INTEGER; + + if (!Number.isInteger(parsed)) { + return defaultValue; + } + + return Math.min(Math.max(parsed, min), max); +}