From 0c6a35e841eed68ad562b74e336cf848cc38df28 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 1 Aug 2026 15:25:06 +0200 Subject: [PATCH] fix: category cache refresh, API 404, redundancy mapping, cost/relation authz, voterToken exposure - Invalidate categories/features queries after creating/editing tools so new categories appear immediately in search, browse dropdown and tool form - Always refetch categories/features when the combobox/suggestion inputs mount - Return JSON 404 for unmatched /api routes instead of the SPA index.html - Read the manually confirmed 'better tool' from the recommendation notes instead of using the min tool id in the redundancy dashboard - Require admin for cost/relation update+delete endpoints - Stop exposing the voter token in the ratings list response - Fix parseInt type error on user id params (Express 5 params typing) --- artifacts/api-server/src/app.ts | 6 ++++++ artifacts/api-server/src/routes/admin.ts | 3 ++- artifacts/api-server/src/routes/costs.ts | 6 +++--- artifacts/api-server/src/routes/ratings.ts | 10 +++++++++- artifacts/api-server/src/routes/tools.ts | 4 ++-- artifacts/api-server/src/routes/users.ts | 4 ++-- .../toolrate/src/components/category-combobox.tsx | 10 ++++++++-- artifacts/toolrate/src/components/feature-input.tsx | 10 ++++++++-- artifacts/toolrate/src/pages/tool-edit.tsx | 4 ++++ artifacts/toolrate/src/pages/tool-new.tsx | 4 +++- 10 files changed, 47 insertions(+), 14 deletions(-) diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts index 37d53eb..4c79678 100644 --- a/artifacts/api-server/src/app.ts +++ b/artifacts/api-server/src/app.ts @@ -60,6 +60,12 @@ app.use( app.use("/api", router); +// Any unmatched /api route should return a JSON 404 instead of falling +// through to the SPA catch-all below. +app.use("/api", (_req, res) => { + res.status(404).json({ error: "Not found" }); +}); + const staticDir = process.env.STATIC_DIR; if (staticDir && existsSync(staticDir)) { app.use(express.static(staticDir)); diff --git a/artifacts/api-server/src/routes/admin.ts b/artifacts/api-server/src/routes/admin.ts index d0aa226..99ff6e1 100644 --- a/artifacts/api-server/src/routes/admin.ts +++ b/artifacts/api-server/src/routes/admin.ts @@ -95,7 +95,8 @@ router.get("/admin/redundancy", requireAuth, async (_req, res): Promise => const manualEvalMap = new Map(); for (const e of manualEvals) { const key = [Math.min(e.toolId, e.relatedToolId), Math.max(e.toolId, e.relatedToolId)].join(":"); - manualEvalMap.set(key, e.toolId); + const betterMatch = e.notes?.match(/Recommended:\s*(\d+)/); + manualEvalMap.set(key, betterMatch ? Number(betterMatch[1]) : e.toolId); } function buildStats(t: typeof toolsTable.$inferSelect) { diff --git a/artifacts/api-server/src/routes/costs.ts b/artifacts/api-server/src/routes/costs.ts index a26ee4c..b01d813 100644 --- a/artifacts/api-server/src/routes/costs.ts +++ b/artifacts/api-server/src/routes/costs.ts @@ -1,7 +1,7 @@ import { Router, type IRouter } from "express"; import { eq, and } from "drizzle-orm"; import { db, toolsTable, toolCostsTable } from "@workspace/db"; -import { requireAuth } from "../middleware/auth"; +import { requireAuth, requireAdmin } from "../middleware/auth"; import { requireFeature } from "../middleware/feature"; import { writeAuditLog } from "../lib/audit"; @@ -47,7 +47,7 @@ router.post("/tools/:id/costs", requireAuth, requireFeature("costs"), async (req res.status(201).json(entry); }); -router.patch("/costs/:id", requireAuth, async (req, res): Promise => { +router.patch("/costs/:id", requireAdmin, async (req, res): Promise => { const id = Number(req.params.id); if (isNaN(id)) { res.status(400).json({ error: "Invalid id" }); return; } @@ -68,7 +68,7 @@ router.patch("/costs/:id", requireAuth, async (req, res): Promise => { res.json(updated); }); -router.delete("/costs/:id", requireAuth, async (req, res): Promise => { +router.delete("/costs/:id", requireAdmin, async (req, res): Promise => { const id = Number(req.params.id); if (isNaN(id)) { res.status(400).json({ error: "Invalid id" }); return; } diff --git a/artifacts/api-server/src/routes/ratings.ts b/artifacts/api-server/src/routes/ratings.ts index c9731c4..ae36c9f 100644 --- a/artifacts/api-server/src/routes/ratings.ts +++ b/artifacts/api-server/src/routes/ratings.ts @@ -31,7 +31,15 @@ router.get("/tools/:id/ratings", async (req, res): Promise => { } const ratings = await db - .select() + .select({ + id: ratingsTable.id, + toolId: ratingsTable.toolId, + usefulness: ratingsTable.usefulness, + usability: ratingsTable.usability, + comment: ratingsTable.comment, + reviewerName: ratingsTable.reviewerName, + createdAt: ratingsTable.createdAt, + }) .from(ratingsTable) .where(eq(ratingsTable.toolId, params.data.id)) .orderBy(ratingsTable.createdAt); diff --git a/artifacts/api-server/src/routes/tools.ts b/artifacts/api-server/src/routes/tools.ts index 6c1c74b..b82dba6 100644 --- a/artifacts/api-server/src/routes/tools.ts +++ b/artifacts/api-server/src/routes/tools.ts @@ -9,7 +9,7 @@ import { UpdateToolBody, DeleteToolParams, } from "@workspace/api-zod"; -import { requireAuth } from "../middleware/auth"; +import { requireAuth, requireAdmin } from "../middleware/auth"; import { requireFeature } from "../middleware/feature"; import { writeAuditLog } from "../lib/audit"; @@ -349,7 +349,7 @@ router.post("/tools/:id/relations", requireAuth, requireFeature("similar-tools") res.status(201).json(relation); }); -router.delete("/tools/relations/:id", requireAuth, async (req, res): Promise => { +router.delete("/tools/relations/:id", requireAdmin, async (req, res): Promise => { const id = Number(req.params.id); if (isNaN(id)) { res.status(400).json({ error: "Invalid id" }); return; } diff --git a/artifacts/api-server/src/routes/users.ts b/artifacts/api-server/src/routes/users.ts index 74e23b5..9c8a5fd 100644 --- a/artifacts/api-server/src/routes/users.ts +++ b/artifacts/api-server/src/routes/users.ts @@ -73,7 +73,7 @@ router.post("/users", requireAdmin, async (req, res): Promise => { }); router.patch("/users/:id", requireAdmin, async (req, res): Promise => { - const id = parseInt(req.params.id, 10); + const id = parseInt(String(req.params.id), 10); if (isNaN(id)) { res.status(400).json({ error: "Invalid user id" }); return; @@ -107,7 +107,7 @@ router.patch("/users/:id", requireAdmin, async (req, res): Promise => { }); router.delete("/users/:id", requireAdmin, async (req, res): Promise => { - const id = parseInt(req.params.id, 10); + const id = parseInt(String(req.params.id), 10); if (isNaN(id)) { res.status(400).json({ error: "Invalid user id" }); return; diff --git a/artifacts/toolrate/src/components/category-combobox.tsx b/artifacts/toolrate/src/components/category-combobox.tsx index 5c46cfd..6c581ff 100644 --- a/artifacts/toolrate/src/components/category-combobox.tsx +++ b/artifacts/toolrate/src/components/category-combobox.tsx @@ -15,7 +15,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { useListCategories } from "@workspace/api-client-react"; +import { useListCategories, getListCategoriesQueryKey } from "@workspace/api-client-react"; interface CategoryComboboxProps { value: string; @@ -26,7 +26,13 @@ interface CategoryComboboxProps { export function CategoryCombobox({ value, onChange, placeholder = "Select or type a category..." }: CategoryComboboxProps) { const [open, setOpen] = useState(false); const [inputValue, setInputValue] = useState(value); - const categories = useListCategories(); + const categories = useListCategories({ + query: { + queryKey: getListCategoriesQueryKey(), + refetchOnMount: "always", + staleTime: 0, + }, + }); const known: string[] = categories.data ?? []; diff --git a/artifacts/toolrate/src/components/feature-input.tsx b/artifacts/toolrate/src/components/feature-input.tsx index 9897db6..575065d 100644 --- a/artifacts/toolrate/src/components/feature-input.tsx +++ b/artifacts/toolrate/src/components/feature-input.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from "react"; import { Input } from "@/components/ui/input"; -import { useListAllFeatures } from "@workspace/api-client-react"; +import { useListAllFeatures, getListAllFeaturesQueryKey } from "@workspace/api-client-react"; import { cn } from "@/lib/utils"; interface FeatureInputProps { @@ -13,7 +13,13 @@ interface FeatureInputProps { export function FeatureInput({ value, onChange, placeholder, "data-testid": testId }: FeatureInputProps) { const [open, setOpen] = useState(false); const containerRef = useRef(null); - const allFeatures = useListAllFeatures(); + const allFeatures = useListAllFeatures({ + query: { + queryKey: getListAllFeaturesQueryKey(), + refetchOnMount: "always", + staleTime: 0, + }, + }); const known: string[] = allFeatures.data ?? []; diff --git a/artifacts/toolrate/src/pages/tool-edit.tsx b/artifacts/toolrate/src/pages/tool-edit.tsx index 2f48b6c..d3936e5 100644 --- a/artifacts/toolrate/src/pages/tool-edit.tsx +++ b/artifacts/toolrate/src/pages/tool-edit.tsx @@ -8,6 +8,8 @@ import { useUpdateTool, getGetToolQueryKey, getListToolsQueryKey, + getListCategoriesQueryKey, + getListAllFeaturesQueryKey, } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; @@ -103,6 +105,8 @@ export default function ToolEdit() { toast({ title: "Tool updated", description: "Changes saved successfully." }); queryClient.invalidateQueries({ queryKey: getGetToolQueryKey(id) }); queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListCategoriesQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListAllFeaturesQueryKey() }); setLocation(`/tools/${id}`); }, onError: (err) => { diff --git a/artifacts/toolrate/src/pages/tool-new.tsx b/artifacts/toolrate/src/pages/tool-new.tsx index 9143cf3..34f0afc 100644 --- a/artifacts/toolrate/src/pages/tool-new.tsx +++ b/artifacts/toolrate/src/pages/tool-new.tsx @@ -2,7 +2,7 @@ import { useLocation } from "wouter"; import { useForm, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; -import { useCreateTool, getListToolsQueryKey } from "@workspace/api-client-react"; +import { useCreateTool, getListToolsQueryKey, getListCategoriesQueryKey, getListAllFeaturesQueryKey } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { Layout } from "@/components/layout"; @@ -75,6 +75,8 @@ export default function ToolNew() { onSuccess: (newTool) => { toast({ title: "Tool added successfully", description: "Your tool is now available for review." }); queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListCategoriesQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListAllFeaturesQueryKey() }); setLocation(`/tools/${newTool.id}`); }, onError: () => {