diff --git a/src/server/lib/params.ts b/src/server/lib/params.ts new file mode 100644 index 0000000..b88d685 --- /dev/null +++ b/src/server/lib/params.ts @@ -0,0 +1,9 @@ +/** + * Parse a route parameter as a positive integer ID. + * Returns the number if valid, or null if the string is not a positive integer. + */ +export function parseId(raw: string): number | null { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) return null; + return id; +} diff --git a/src/server/routes/auth.ts b/src/server/routes/auth.ts index 268b725..f929b33 100644 --- a/src/server/routes/auth.ts +++ b/src/server/routes/auth.ts @@ -4,6 +4,7 @@ import { Hono } from "hono"; import { deleteCookie, getCookie, setCookie } from "hono/cookie"; import { z } from "zod"; import { users } from "../../db/schema.ts"; +import { parseId } from "../lib/params.ts"; import { requireAuth } from "../middleware/auth.ts"; import { changePassword, @@ -186,7 +187,8 @@ app.post( app.delete("/keys/:id", requireAuth, (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid key ID" }, 400); deleteApiKey(db, id); return c.json({ ok: true }); }); diff --git a/src/server/routes/categories.ts b/src/server/routes/categories.ts index 4a1f149..c3035a7 100644 --- a/src/server/routes/categories.ts +++ b/src/server/routes/categories.ts @@ -4,6 +4,7 @@ import { createCategorySchema, updateCategorySchema, } from "../../shared/schemas.ts"; +import { parseId } from "../lib/params.ts"; import { createCategory, deleteCategory, @@ -33,7 +34,8 @@ app.put( zValidator("json", updateCategorySchema.omit({ id: true })), (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid category ID" }, 400); const data = c.req.valid("json"); const cat = updateCategory(db, id, data); if (!cat) return c.json({ error: "Category not found" }, 404); @@ -43,7 +45,8 @@ app.put( app.delete("/:id", (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid category ID" }, 400); const result = deleteCategory(db, id); if (!result.success) { diff --git a/src/server/routes/items.ts b/src/server/routes/items.ts index f0beb30..1ed6dc2 100644 --- a/src/server/routes/items.ts +++ b/src/server/routes/items.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { createItemSchema, updateItemSchema } from "../../shared/schemas.ts"; +import { parseId } from "../lib/params.ts"; import { createItem, deleteItem, @@ -23,7 +24,8 @@ app.get("/", (c) => { app.get("/:id", (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid item ID" }, 400); const item = getItemById(db, id); if (!item) return c.json({ error: "Item not found" }, 404); return c.json(item); @@ -41,7 +43,8 @@ app.put( zValidator("json", updateItemSchema.omit({ id: true })), (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid item ID" }, 400); const data = c.req.valid("json"); const item = updateItem(db, id, data); if (!item) return c.json({ error: "Item not found" }, 404); @@ -51,7 +54,8 @@ app.put( app.delete("/:id", async (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid item ID" }, 400); const deleted = deleteItem(db, id); if (!deleted) return c.json({ error: "Item not found" }, 404); diff --git a/src/server/routes/setups.ts b/src/server/routes/setups.ts index fa773b0..d5e2af2 100644 --- a/src/server/routes/setups.ts +++ b/src/server/routes/setups.ts @@ -6,6 +6,7 @@ import { updateClassificationSchema, updateSetupSchema, } from "../../shared/schemas.ts"; +import { parseId } from "../lib/params.ts"; import { createSetup, deleteSetup, @@ -38,7 +39,8 @@ app.post("/", zValidator("json", createSetupSchema), (c) => { app.get("/:id", (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid setup ID" }, 400); const setup = getSetupWithItems(db, id); if (!setup) return c.json({ error: "Setup not found" }, 404); return c.json(setup); @@ -46,7 +48,8 @@ app.get("/:id", (c) => { app.put("/:id", zValidator("json", updateSetupSchema), (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid setup ID" }, 400); const data = c.req.valid("json"); const setup = updateSetup(db, id, data); if (!setup) return c.json({ error: "Setup not found" }, 404); @@ -55,7 +58,8 @@ app.put("/:id", zValidator("json", updateSetupSchema), (c) => { app.delete("/:id", (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid setup ID" }, 400); const deleted = deleteSetup(db, id); if (!deleted) return c.json({ error: "Setup not found" }, 404); return c.json({ success: true }); @@ -65,7 +69,8 @@ app.delete("/:id", (c) => { app.put("/:id/items", zValidator("json", syncSetupItemsSchema), (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid setup ID" }, 400); const { itemIds } = c.req.valid("json"); const setup = getSetupWithItems(db, id); @@ -80,8 +85,9 @@ app.patch( zValidator("json", updateClassificationSchema), (c) => { const db = c.get("db"); - const setupId = Number(c.req.param("id")); - const itemId = Number(c.req.param("itemId")); + const setupId = parseId(c.req.param("id")); + const itemId = parseId(c.req.param("itemId")); + if (!setupId || !itemId) return c.json({ error: "Invalid ID" }, 400); const { classification } = c.req.valid("json"); updateItemClassification(db, setupId, itemId, classification); return c.json({ success: true }); @@ -90,8 +96,9 @@ app.patch( app.delete("/:id/items/:itemId", (c) => { const db = c.get("db"); - const setupId = Number(c.req.param("id")); - const itemId = Number(c.req.param("itemId")); + const setupId = parseId(c.req.param("id")); + const itemId = parseId(c.req.param("itemId")); + if (!setupId || !itemId) return c.json({ error: "Invalid ID" }, 400); removeSetupItem(db, setupId, itemId); return c.json({ success: true }); }); diff --git a/src/server/routes/threads.ts b/src/server/routes/threads.ts index cfa2405..a2e2cb2 100644 --- a/src/server/routes/threads.ts +++ b/src/server/routes/threads.ts @@ -10,6 +10,7 @@ import { updateCandidateSchema, updateThreadSchema, } from "../../shared/schemas.ts"; +import { parseId } from "../lib/params.ts"; import { createCandidate, createThread, @@ -45,7 +46,8 @@ app.post("/", zValidator("json", createThreadSchema), (c) => { app.get("/:id", (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid thread ID" }, 400); const thread = getThreadWithCandidates(db, id); if (!thread) return c.json({ error: "Thread not found" }, 404); return c.json(thread); @@ -53,7 +55,8 @@ app.get("/:id", (c) => { app.put("/:id", zValidator("json", updateThreadSchema), (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid thread ID" }, 400); const data = c.req.valid("json"); const thread = updateThread(db, id, data); if (!thread) return c.json({ error: "Thread not found" }, 404); @@ -62,7 +65,8 @@ app.put("/:id", zValidator("json", updateThreadSchema), (c) => { app.delete("/:id", async (c) => { const db = c.get("db"); - const id = Number(c.req.param("id")); + const id = parseId(c.req.param("id")); + if (!id) return c.json({ error: "Invalid thread ID" }, 400); const deleted = deleteThread(db, id); if (!deleted) return c.json({ error: "Thread not found" }, 404); @@ -82,7 +86,8 @@ app.delete("/:id", async (c) => { app.post("/:id/candidates", zValidator("json", createCandidateSchema), (c) => { const db = c.get("db"); - const threadId = Number(c.req.param("id")); + const threadId = parseId(c.req.param("id")); + if (!threadId) return c.json({ error: "Invalid thread ID" }, 400); // Verify thread exists const thread = getThreadWithCandidates(db, threadId); @@ -98,7 +103,8 @@ app.put( zValidator("json", updateCandidateSchema), (c) => { const db = c.get("db"); - const candidateId = Number(c.req.param("candidateId")); + const candidateId = parseId(c.req.param("candidateId")); + if (!candidateId) return c.json({ error: "Invalid candidate ID" }, 400); const data = c.req.valid("json"); const candidate = updateCandidate(db, candidateId, data); if (!candidate) return c.json({ error: "Candidate not found" }, 404); @@ -108,7 +114,8 @@ app.put( app.delete("/:threadId/candidates/:candidateId", async (c) => { const db = c.get("db"); - const candidateId = Number(c.req.param("candidateId")); + const candidateId = parseId(c.req.param("candidateId")); + if (!candidateId) return c.json({ error: "Invalid candidate ID" }, 400); const deleted = deleteCandidate(db, candidateId); if (!deleted) return c.json({ error: "Candidate not found" }, 404); @@ -131,7 +138,8 @@ app.patch( zValidator("json", reorderCandidatesSchema), (c) => { const db = c.get("db"); - const threadId = Number(c.req.param("id")); + const threadId = parseId(c.req.param("id")); + if (!threadId) return c.json({ error: "Invalid thread ID" }, 400); const { orderedIds } = c.req.valid("json"); const result = reorderCandidates(db, threadId, orderedIds); if (!result.success) return c.json({ error: result.error }, 400); @@ -143,7 +151,8 @@ app.patch( app.post("/:id/resolve", zValidator("json", resolveThreadSchema), (c) => { const db = c.get("db"); - const threadId = Number(c.req.param("id")); + const threadId = parseId(c.req.param("id")); + if (!threadId) return c.json({ error: "Invalid thread ID" }, 400); const { candidateId } = c.req.valid("json"); const result = resolveThread(db, threadId, candidateId);