c5ca3ca992
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
132 lines
3.5 KiB
TypeScript
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;
|