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 => { 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 => { 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;