import { Router, Request, Response, NextFunction } from "express"; import { z } from "zod"; import bcrypt from "bcryptjs"; import { prisma } from "../lib/prisma.js"; import { requireAuth, requireRole } from "../middleware/auth.js"; import { AppError } from "../middleware/errorHandler.js"; import { parsePage, paginatedResponse } from "../lib/pagination.js"; import { AuthenticatedRequest } from "../types/index.js"; const router = Router(); const auth = requireAuth as unknown as (r: Request, s: Response, n: NextFunction) => void; const vendorUp = requireRole("admin", "vendor") as unknown as (r: Request, s: Response, n: NextFunction) => void; // Strip passwordHash from any user object before sending function safe(u: T): Omit { const { passwordHash: _, ...rest } = u; return rest; } const CreateUserSchema = z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(1).max(100), roleId: z.string().min(1), vendorId: z.string().min(1).optional(), // admin can assign to any vendor }); const UpdateUserSchema = z.object({ name: z.string().min(1).max(100).optional(), roleId: z.string().min(1).optional(), password: z.string().min(8).optional(), }); // GET /api/v1/users — admin sees all users; vendor sees their own vendor router.get("/", auth, vendorUp, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const { page, limit, skip } = parsePage(req.query as Record); const isAdmin = authReq.auth.roleName === "admin"; const where = isAdmin ? {} : { vendorId: authReq.auth.vendorId }; const [users, total] = await Promise.all([ prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: "desc" }, include: { role: true, vendor: { select: { id: true, name: true } } }, }), prisma.user.count({ where }), ]); res.json(paginatedResponse(users.map(safe), total, { page, limit, skip })); } catch (err) { next(err); } }); // GET /api/v1/users/roles/list — must be before /:id router.get("/roles/list", auth, async (_req: Request, res: Response, next: NextFunction) => { try { const roles = await prisma.role.findMany({ orderBy: { name: "asc" } }); res.json(roles); } catch (err) { next(err); } }); // GET /api/v1/users/:id router.get("/:id", auth, vendorUp, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const isAdmin = authReq.auth.roleName === "admin"; const user = await prisma.user.findFirst({ where: { id: req.params.id, ...(isAdmin ? {} : { vendorId: authReq.auth.vendorId }) }, include: { role: true, vendor: { select: { id: true, name: true } } }, }); if (!user) throw new AppError(404, "NOT_FOUND", "User not found"); res.json(safe(user)); } catch (err) { next(err); } }); // POST /api/v1/users router.post("/", auth, vendorUp, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const isAdmin = authReq.auth.roleName === "admin"; const body = CreateUserSchema.parse(req.body); const existing = await prisma.user.findUnique({ where: { email: body.email } }); if (existing) throw new AppError(409, "CONFLICT", "Email already in use"); const role = await prisma.role.findUnique({ where: { id: body.roleId } }); if (!role) throw new AppError(400, "BAD_REQUEST", "Invalid role"); // Vendors cannot create admin accounts if (authReq.auth.roleName === "vendor" && role.name === "admin") { throw new AppError(403, "FORBIDDEN", "Vendors cannot create admin accounts"); } const targetVendorId = isAdmin && body.vendorId ? body.vendorId : authReq.auth.vendorId; const user = await prisma.user.create({ data: { email: body.email, passwordHash: await bcrypt.hash(body.password, 10), name: body.name, vendorId: targetVendorId, roleId: body.roleId, }, include: { role: true, vendor: { select: { id: true, name: true } } }, }); res.status(201).json(safe(user)); } catch (err) { next(err); } }); // PUT /api/v1/users/:id router.put("/:id", auth, vendorUp, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const isAdmin = authReq.auth.roleName === "admin"; const body = UpdateUserSchema.parse(req.body); const existing = await prisma.user.findFirst({ where: { id: req.params.id, ...(isAdmin ? {} : { vendorId: authReq.auth.vendorId }) }, include: { role: true }, }); if (!existing) throw new AppError(404, "NOT_FOUND", "User not found"); if (body.roleId) { const role = await prisma.role.findUnique({ where: { id: body.roleId } }); if (!role) throw new AppError(400, "BAD_REQUEST", "Invalid role"); if (authReq.auth.roleName === "vendor" && role.name === "admin") { throw new AppError(403, "FORBIDDEN", "Vendors cannot assign admin role"); } } const updateData: Record = {}; if (body.name) updateData.name = body.name; if (body.roleId) updateData.roleId = body.roleId; if (body.password) updateData.passwordHash = await bcrypt.hash(body.password, 10); const user = await prisma.user.update({ where: { id: req.params.id }, data: updateData, include: { role: true, vendor: { select: { id: true, name: true } } }, }); res.json(safe(user)); } catch (err) { next(err); } }); // DELETE /api/v1/users/:id router.delete("/:id", auth, vendorUp, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const isAdmin = authReq.auth.roleName === "admin"; if (req.params.id === authReq.auth.userId) { throw new AppError(400, "BAD_REQUEST", "Cannot delete your own account"); } const existing = await prisma.user.findFirst({ where: { id: req.params.id, ...(isAdmin ? {} : { vendorId: authReq.auth.vendorId }) }, }); if (!existing) throw new AppError(404, "NOT_FOUND", "User not found"); await prisma.user.delete({ where: { id: req.params.id } }); res.status(204).send(); } catch (err) { next(err); } }); export default router;