Add local user authentication and admin capabilities
Implement local user authentication with password hashing, add admin roles for user management and audit log viewing, and introduce audit logging for critical actions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 832a44ff-12ae-4096-8a0d-666ec083d536 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/1p7jhzu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
DeleteToolParams,
|
||||
} from "@workspace/api-zod";
|
||||
import { requireAuth } from "../middleware/auth";
|
||||
import { writeAuditLog } from "../lib/audit";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -27,6 +28,13 @@ function buildToolWithStats(tool: typeof toolsTable.$inferSelect, ratings: { use
|
||||
return { ...tool, ratingCount, avgUsefulness, avgUsability, avgCombined };
|
||||
}
|
||||
|
||||
function canEditTool(req: import("express").Request, tool: { createdBy: string | null }): boolean {
|
||||
const user = req.session.user;
|
||||
if (!user) return false;
|
||||
if (user.role === "admin") return true;
|
||||
return tool.createdBy === user.sub || tool.createdBy === user.preferred_username;
|
||||
}
|
||||
|
||||
router.get("/tools", async (req, res): Promise<void> => {
|
||||
const parsed = ListToolsQueryParams.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
@@ -79,16 +87,21 @@ router.post("/tools", requireAuth, async (req, res): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.session.user!;
|
||||
const createdBy = user.preferred_username || user.name || user.sub;
|
||||
|
||||
const [tool] = await db.insert(toolsTable).values({
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
category: parsed.data.category,
|
||||
websiteUrl: parsed.data.websiteUrl ?? null,
|
||||
iconUrl: parsed.data.iconUrl ?? null,
|
||||
createdBy,
|
||||
features: parsed.data.features ?? [],
|
||||
tags: parsed.data.tags ?? [],
|
||||
}).returning();
|
||||
|
||||
await writeAuditLog(req, "tool", tool.id, "create", { name: tool.name, category: tool.category });
|
||||
res.status(201).json(tool);
|
||||
});
|
||||
|
||||
@@ -120,6 +133,21 @@ router.patch("/tools/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(toolsTable)
|
||||
.where(eq(toolsTable.id, params.data.id));
|
||||
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Tool not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canEditTool(req, existing)) {
|
||||
res.status(403).json({ error: "Not allowed to edit this tool" });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = UpdateToolBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
@@ -127,13 +155,28 @@ router.patch("/tools/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (parsed.data.name !== undefined) updateData.name = parsed.data.name;
|
||||
if (parsed.data.description !== undefined) updateData.description = parsed.data.description;
|
||||
if (parsed.data.category !== undefined) updateData.category = parsed.data.category;
|
||||
if (parsed.data.websiteUrl !== undefined) updateData.websiteUrl = parsed.data.websiteUrl;
|
||||
if (parsed.data.iconUrl !== undefined) updateData.iconUrl = parsed.data.iconUrl;
|
||||
if (parsed.data.features !== undefined) updateData.features = parsed.data.features;
|
||||
if (parsed.data.tags !== undefined) updateData.tags = parsed.data.tags;
|
||||
const changes: Record<string, { from: unknown; to: unknown }> = {};
|
||||
|
||||
function track(key: keyof typeof existing, value: unknown) {
|
||||
if (value !== undefined && value !== existing[key]) {
|
||||
changes[key] = { from: existing[key], to: value };
|
||||
(updateData as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
track("name", parsed.data.name);
|
||||
track("description", parsed.data.description);
|
||||
track("category", parsed.data.category);
|
||||
track("websiteUrl", parsed.data.websiteUrl);
|
||||
track("iconUrl", parsed.data.iconUrl);
|
||||
if (parsed.data.features !== undefined) {
|
||||
changes["features"] = { from: existing.features, to: parsed.data.features };
|
||||
updateData.features = parsed.data.features;
|
||||
}
|
||||
if (parsed.data.tags !== undefined) {
|
||||
changes["tags"] = { from: existing.tags, to: parsed.data.tags };
|
||||
updateData.tags = parsed.data.tags;
|
||||
}
|
||||
|
||||
const [tool] = await db
|
||||
.update(toolsTable)
|
||||
@@ -146,6 +189,10 @@ router.patch("/tools/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length > 0) {
|
||||
await writeAuditLog(req, "tool", tool.id, "update", changes);
|
||||
}
|
||||
|
||||
res.json(tool);
|
||||
});
|
||||
|
||||
@@ -156,12 +203,23 @@ router.delete("/tools/:id", requireAuth, async (req, res): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const [tool] = await db.delete(toolsTable).where(eq(toolsTable.id, params.data.id)).returning();
|
||||
if (!tool) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(toolsTable)
|
||||
.where(eq(toolsTable.id, params.data.id));
|
||||
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Tool not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canEditTool(req, existing)) {
|
||||
res.status(403).json({ error: "Not allowed to delete this tool" });
|
||||
return;
|
||||
}
|
||||
|
||||
await writeAuditLog(req, "tool", existing.id, "delete", { name: existing.name });
|
||||
await db.delete(toolsTable).where(eq(toolsTable.id, params.data.id));
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user