import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { isValidObjectId } from "mongoose"; import { connectDb } from "@/lib/server/db"; import { User } from "@/lib/server/models/User"; import { ACTIVE_BRANCH, Branch } from "@/lib/server/models/Branch"; import { Business } from "@/lib/server/models/Business"; import { UserSession } from "@/lib/server/models/UserSession"; import { authorize, MANAGERS } from "@/lib/server/auth"; import { objectId } from "@/lib/server/validation"; export const runtime = "nodejs"; const updateSchema = z .object({ isActive: z.boolean().optional(), branchId: objectId.optional(), // owner only: move staff to another branch }) .refine((v) => v.isActive !== undefined || v.branchId !== undefined, { message: "No fields to update" }); /** * PATCH /api/users/:id — deactivate/reactivate a staff account, or (owner * only) move it to another branch. This is how an owner "removes" a user: * the account and its sales history stay, but it can no longer sign in. * * SUPER_ADMIN any user in the business except themselves and the owner of record * BRANCH_MANAGER only DELIVERY staff in their own branch, and only isActive */ export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) { await connectDb(); const gate = authorize(req, MANAGERS); if (!gate.ok) return gate.response; const auth = gate.auth; if (!isValidObjectId(params.id)) return NextResponse.json({ error: "User not found" }, { status: 404 }); const json = await req.json().catch(() => null); const parsed = updateSchema.safeParse(json); if (!parsed.success) { return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 }); } const { isActive, branchId } = parsed.data; const target = await User.findOne({ _id: params.id, businessId: auth.businessId }); if (!target) return NextResponse.json({ error: "User not found" }, { status: 404 }); if (target._id.toString() === auth.sub) { return NextResponse.json({ error: "You cannot change your own account here" }, { status: 403 }); } if (auth.role === "BRANCH_MANAGER") { const ownStaff = target.role === "DELIVERY" && target.branchId?.toString() === auth.branchId; if (!ownStaff) { return NextResponse.json({ error: "Branch managers may only manage delivery staff in their own branch" }, { status: 403 }); } if (branchId !== undefined) { return NextResponse.json({ error: "Only the owner can move staff between branches" }, { status: 403 }); } } const business = await Business.findById(auth.businessId).select("ownerId").lean(); if (business && business.ownerId.toString() === target._id.toString()) { return NextResponse.json({ error: "The business owner's account cannot be changed" }, { status: 403 }); } if (branchId !== undefined) { if (target.role === "SUPER_ADMIN") { return NextResponse.json({ error: "Owner accounts are not tied to a branch" }, { status: 400 }); } const branch = await Branch.findOne({ _id: branchId, businessId: auth.businessId, ...ACTIVE_BRANCH }).lean(); if (!branch) return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 }); target.branchId = branch._id; } if (isActive !== undefined) target.isActive = isActive; await target.save(); // Revoke every device session so a deactivated (or moved) user cannot // mint new access tokens; any current access token expires within 15m. if (isActive === false || branchId !== undefined) { await UserSession.deleteMany({ userId: target._id }); } const obj = target.toObject() as unknown as Record; delete obj.passwordHash; delete obj.inviteTokenHash; return NextResponse.json({ user: obj }); }