import { Router, type IRouter } from "express"; import { eq, sql } from "drizzle-orm"; import bcrypt from "bcryptjs"; import { db, usersTable } from "@workspace/db"; import { requireAdmin } from "../middleware/auth"; import { writeAuditLog } from "../lib/audit"; import { passwordRateLimit } from "../lib/rate-limit"; import { z } from "zod"; const router: IRouter = Router(); const Tier = z.enum(["free", "premium", "enterprise"]); 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"), tier: Tier.optional().default("free"), }); const UserUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), tier: Tier.optional(), }); const SetPasswordSchema = z.object({ password: z.string().min(6), }); router.patch("/users/:id/password", requireAdmin, passwordRateLimit, async (req, res): Promise => { const id = parseInt(String(req.params.id), 10); if (isNaN(id)) { res.status(400).json({ error: "Invalid user id" }); return; } const parsed = SetPasswordSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: parsed.error.message }); return; } const [target] = await db .select({ id: usersTable.id, authProvider: usersTable.authProvider, username: usersTable.username }) .from(usersTable) .where(eq(usersTable.id, id)) .limit(1); if (!target) { res.status(404).json({ error: "User not found" }); return; } if (target.authProvider !== "local") { res.status(422).json({ error: "oidc" }); return; } const passwordHash = await bcrypt.hash(parsed.data.password, 12); await db.update(usersTable).set({ passwordHash }).where(eq(usersTable.id, id)); await writeAuditLog(req, "user", id, "set_password", { username: target.username }); res.sendStatus(204); }); router.get("/users", requireAdmin, async (req, res): Promise => { const users = await db .select({ id: usersTable.id, username: usersTable.username, email: usersTable.email, role: usersTable.role, tier: usersTable.tier, authProvider: usersTable.authProvider, createdAt: usersTable.createdAt, }) .from(usersTable) .orderBy(usersTable.createdAt); res.json(users); }); router.post("/users", requireAdmin, async (req, res): Promise => { 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, 12); const [user] = await db .insert(usersTable) .values({ username: parsed.data.username, passwordHash, email: parsed.data.email ?? null, role: parsed.data.role ?? "user", tier: parsed.data.tier ?? "free", }) .returning({ id: usersTable.id, username: usersTable.username, email: usersTable.email, role: usersTable.role, tier: usersTable.tier, createdAt: usersTable.createdAt, }); await writeAuditLog(req, "user", user.id, "create", { username: user.username, role: user.role, tier: user.tier }); res.status(201).json(user); }); router.patch("/users/:id", requireAdmin, async (req, res): Promise => { const id = parseInt(String(req.params.id), 10); if (isNaN(id)) { res.status(400).json({ error: "Invalid user id" }); return; } const parsed = UserUpdateSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: parsed.error.message }); return; } const [target] = await db .select({ id: usersTable.id, role: usersTable.role }) .from(usersTable) .where(eq(usersTable.id, id)) .limit(1); if (!target) { res.status(404).json({ error: "User not found" }); return; } if (target.role === "admin" && parsed.data.role === "user") { if (req.session.user?.sub === String(id)) { res.status(400).json({ error: "Cannot demote your own account" }); return; } const [{ count }] = await db .select({ count: sql`count(*)::int` }) .from(usersTable) .where(eq(usersTable.role, "admin")); if (count <= 1) { res.status(400).json({ error: "Cannot demote the last admin" }); return; } } const updateData: Record = {}; if (parsed.data.role !== undefined) updateData.role = parsed.data.role; if (parsed.data.tier !== undefined) updateData.tier = parsed.data.tier; const [user] = await db .update(usersTable) .set(updateData) .where(eq(usersTable.id, id)) .returning({ id: usersTable.id, username: usersTable.username, email: usersTable.email, role: usersTable.role, tier: usersTable.tier, createdAt: usersTable.createdAt, }); await writeAuditLog(req, "user", id, "update", updateData); res.json(user); }); router.delete("/users/:id", requireAdmin, async (req, res): Promise => { const id = parseInt(String(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;