import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { UpdateUserSchema } from "@/lib/schemas"; import { hashPassword, hashPin } from "@/lib/password"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; export async function PATCH(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const body = await parseJson(req, UpdateUserSchema); const user = await prisma.user.findUnique({ where: { id } }); if (!user) throw new ApiError(404, "not_found", "User not found"); const data: Record = {}; if (body.name !== undefined) data.name = body.name; if (body.active !== undefined) data.active = body.active; if (user.role === "admin") { if (body.email) { const dup = await prisma.user.findFirst({ where: { email: body.email, id: { not: id } }, }); if (dup) throw new ApiError(409, "duplicate", "Email already in use"); data.email = body.email; } if (body.password) data.passwordHash = await hashPassword(body.password); if (body.pin) { throw new ApiError(400, "invalid_field", "Admins don't have PINs"); } } else { if (body.email || body.password) { throw new ApiError(400, "invalid_field", "Operators use PIN, not email/password"); } if (body.pin) { data.pinHash = await hashPin(body.pin); data.failedAttempts = 0; data.lockedUntil = null; } if (body.name && body.name !== user.name) { const dup = await prisma.user.findFirst({ where: { role: "operator", name: body.name, id: { not: id } }, }); if (dup) throw new ApiError(409, "duplicate", "Operator name already in use"); } } const updated = await prisma.user.update({ where: { id }, data, select: { id: true, role: true, name: true, email: true, active: true }, }); await audit({ actorId: actor.id, action: "update", entity: "User", entityId: id, after: { changed: Object.keys(data).filter((k) => k !== "passwordHash" && k !== "pinHash"), secretsChanged: "passwordHash" in data || "pinHash" in data, }, ipAddress: clientIp(req), }); return ok({ user: updated }); } catch (err) { return errorResponse(err); } } export async function DELETE(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; if (id === actor.id) { throw new ApiError(400, "self_delete", "You cannot delete your own account"); } const user = await prisma.user.findUnique({ where: { id } }); if (!user) throw new ApiError(404, "not_found", "User not found"); if (user.role === "admin") { const otherAdmins = await prisma.user.count({ where: { role: "admin", active: true, id: { not: id } }, }); if (otherAdmins === 0) { throw new ApiError(400, "last_admin", "Cannot remove the last active admin"); } } // soft-delete: deactivate and remove all sessions await prisma.$transaction([ prisma.session.deleteMany({ where: { userId: id } }), prisma.user.update({ where: { id }, data: { active: false } }), ]); await audit({ actorId: actor.id, action: "deactivate", entity: "User", entityId: id, ipAddress: clientIp(req), }); return ok({ ok: true }); } catch (err) { return errorResponse(err); } }