Files
tool-evaluator/artifacts/api-server/src/routes/users.ts
T
cheffe01 c5ca3ca992 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
2026-05-25 14:11:02 +00:00

132 lines
3.5 KiB
TypeScript

import { Router, type IRouter } from "express";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { db, usersTable } from "@workspace/db";
import { requireAdmin } from "../middleware/auth";
import { writeAuditLog } from "../lib/audit";
import { z } from "zod/v4";
const router: IRouter = Router();
const UserCreateSchema = z.object({
username: z.string().min(2),
password: z.string().min(6),
email: z.string().optional(),
role: z.enum(["admin", "user"]).optional().default("user"),
});
const UserRoleUpdateSchema = z.object({
role: z.enum(["admin", "user"]),
});
router.get("/users", requireAdmin, async (req, res): Promise<void> => {
const users = await db
.select({
id: usersTable.id,
username: usersTable.username,
email: usersTable.email,
role: usersTable.role,
createdAt: usersTable.createdAt,
})
.from(usersTable)
.orderBy(usersTable.createdAt);
res.json(users);
});
router.post("/users", requireAdmin, async (req, res): Promise<void> => {
const parsed = UserCreateSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const existing = await db
.select({ id: usersTable.id })
.from(usersTable)
.where(eq(usersTable.username, parsed.data.username))
.limit(1);
if (existing.length > 0) {
res.status(409).json({ error: "Username already exists" });
return;
}
const passwordHash = await bcrypt.hash(parsed.data.password, 10);
const [user] = await db
.insert(usersTable)
.values({
username: parsed.data.username,
passwordHash,
email: parsed.data.email ?? null,
role: parsed.data.role ?? "user",
})
.returning({
id: usersTable.id,
username: usersTable.username,
email: usersTable.email,
role: usersTable.role,
createdAt: usersTable.createdAt,
});
await writeAuditLog(req, "user", user.id, "create", { username: user.username, role: user.role });
res.status(201).json(user);
});
router.patch("/users/:id", requireAdmin, async (req, res): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: "Invalid user id" });
return;
}
const parsed = UserRoleUpdateSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const [user] = await db
.update(usersTable)
.set({ role: parsed.data.role })
.where(eq(usersTable.id, id))
.returning({
id: usersTable.id,
username: usersTable.username,
email: usersTable.email,
role: usersTable.role,
createdAt: usersTable.createdAt,
});
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
await writeAuditLog(req, "user", id, "update", { role: parsed.data.role });
res.json(user);
});
router.delete("/users/:id", requireAdmin, async (req, res): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: "Invalid user id" });
return;
}
if (req.session.user?.sub === String(id)) {
res.status(400).json({ error: "Cannot delete your own account" });
return;
}
const [user] = await db.delete(usersTable).where(eq(usersTable.id, id)).returning();
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
await writeAuditLog(req, "user", id, "delete", { username: user.username });
res.sendStatus(204);
});
export default router;