Files
tool-evaluator/artifacts/api-server/src/routes/ratings.ts
T
opencode 0c6a35e841
Build & Push Docker Image / build (push) Successful in 8m33s
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)
2026-08-01 15:25:06 +02:00

95 lines
2.7 KiB
TypeScript

import { Router, type IRouter } from "express";
import { eq, and } from "drizzle-orm";
import { db, toolsTable, ratingsTable } from "@workspace/db";
import {
ListToolRatingsParams,
CreateRatingParams,
CreateRatingBody,
} from "@workspace/api-zod";
import { requireAuth } from "../middleware/auth";
import crypto from "crypto";
const router: IRouter = Router();
const VOTER_SECRET = process.env.VOTER_SECRET || "dev-voter-secret-change-in-production";
function computeVoterToken(userId: string): string {
return crypto.createHmac("sha256", VOTER_SECRET).update(userId).digest("hex");
}
router.get("/tools/:id/ratings", async (req, res): Promise<void> => {
const params = ListToolRatingsParams.safeParse(req.params);
if (!params.success) {
res.status(400).json({ error: params.error.message });
return;
}
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id));
if (!tool) {
res.status(404).json({ error: "Tool not found" });
return;
}
const ratings = await db
.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);
res.json(ratings);
});
router.post("/tools/:id/ratings", requireAuth, async (req, res): Promise<void> => {
const params = CreateRatingParams.safeParse(req.params);
if (!params.success) {
res.status(400).json({ error: params.error.message });
return;
}
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id));
if (!tool) {
res.status(404).json({ error: "Tool not found" });
return;
}
const parsed = CreateRatingBody.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const token = computeVoterToken(req.session.user!.sub);
const [existing] = await db
.select()
.from(ratingsTable)
.where(and(eq(ratingsTable.toolId, params.data.id), eq(ratingsTable.voterToken, token)))
.limit(1);
if (existing) {
res.status(409).json({ error: "You have already reviewed this tool." });
return;
}
const [rating] = await db.insert(ratingsTable).values({
toolId: params.data.id,
usefulness: parsed.data.usefulness,
usability: parsed.data.usability,
comment: parsed.data.comment ?? null,
reviewerName: parsed.data.reviewerName ?? null,
voterToken: token,
}).returning();
res.status(201).json(rating);
});
export default router;