feat: auth foundation, similar tools, costs, redundancy, anonymous voting
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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 { requireFeature } from "../middleware/feature";
|
||||
import { writeAuditLog } from "../lib/audit";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/tools/:id/costs", async (req, res): Promise<void> => {
|
||||
const toolId = Number(req.params.id);
|
||||
if (isNaN(toolId)) { res.status(400).json({ error: "Invalid id" }); return; }
|
||||
|
||||
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, toolId));
|
||||
if (!tool) { res.status(404).json({ error: "Tool not found" }); return; }
|
||||
|
||||
const costs = await db
|
||||
.select()
|
||||
.from(toolCostsTable)
|
||||
.where(eq(toolCostsTable.toolId, toolId))
|
||||
.orderBy(toolCostsTable.createdAt);
|
||||
|
||||
res.json(costs);
|
||||
});
|
||||
|
||||
router.post("/tools/:id/costs", requireAuth, requireFeature("costs"), async (req, res): Promise<void> => {
|
||||
const toolId = Number(req.params.id);
|
||||
if (isNaN(toolId)) { res.status(400).json({ error: "Invalid id" }); return; }
|
||||
|
||||
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, toolId));
|
||||
if (!tool) { res.status(404).json({ error: "Tool not found" }); return; }
|
||||
|
||||
const { licenseType, cost, currency, renewalDate, notes } = req.body;
|
||||
|
||||
const [entry] = await db.insert(toolCostsTable).values({
|
||||
toolId,
|
||||
licenseType: licenseType ?? "free",
|
||||
cost: cost ?? null,
|
||||
currency: currency ?? "EUR",
|
||||
renewalDate: renewalDate ? new Date(renewalDate) : null,
|
||||
notes: notes ?? null,
|
||||
createdBy: Number(req.session.user!.sub),
|
||||
}).returning();
|
||||
|
||||
await writeAuditLog(req, "tool_cost", entry.id, "create", { toolId, licenseType, cost });
|
||||
res.status(201).json(entry);
|
||||
});
|
||||
|
||||
router.patch("/costs/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) { res.status(400).json({ error: "Invalid id" }); return; }
|
||||
|
||||
const [existing] = await db.select().from(toolCostsTable).where(eq(toolCostsTable.id, id));
|
||||
if (!existing) { res.status(404).json({ error: "Cost entry not found" }); return; }
|
||||
|
||||
const { licenseType, cost, currency, renewalDate, notes } = req.body;
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (licenseType !== undefined) updateData.licenseType = licenseType;
|
||||
if (cost !== undefined) updateData.cost = cost;
|
||||
if (currency !== undefined) updateData.currency = currency;
|
||||
if (renewalDate !== undefined) updateData.renewalDate = renewalDate ? new Date(renewalDate) : null;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const [updated] = await db.update(toolCostsTable).set(updateData).where(eq(toolCostsTable.id, id)).returning();
|
||||
await writeAuditLog(req, "tool_cost", id, "update", { toolId: existing.toolId, ...updateData });
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.delete("/costs/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) { res.status(400).json({ error: "Invalid id" }); return; }
|
||||
|
||||
const [existing] = await db.select().from(toolCostsTable).where(eq(toolCostsTable.id, id));
|
||||
if (!existing) { res.status(404).json({ error: "Cost entry not found" }); return; }
|
||||
|
||||
await writeAuditLog(req, "tool_cost", id, "delete", { toolId: existing.toolId });
|
||||
await db.delete(toolCostsTable).where(eq(toolCostsTable.id, id));
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user