Add multi-tenant SaaS: business signup, owner/branch-manager/staff hierarchy, full tenant isolation

This commit is contained in:
EOH Developer
2026-10-04 01:22:42 -07:00
parent 3b22ddf6fb
commit 04e3051eb6
38 changed files with 640 additions and 67 deletions
+5
View File
@@ -4,6 +4,7 @@ import { connectDb } from "@/lib/server/db";
import { User } from "@/lib/server/models/User";
import { UserSession } from "@/lib/server/models/UserSession";
import { Branch } from "@/lib/server/models/Branch";
import { Business } from "@/lib/server/models/Business";
import { signAccessToken, generateRefreshToken, hashRefreshToken, refreshTtlMs } from "@/lib/server/jwt";
import { ACCESS_COOKIE, REFRESH_COOKIE, accessCookieOptions, refreshCookieOptions } from "@/lib/server/auth";
import { getEnv } from "@/lib/server/env";
@@ -93,18 +94,22 @@ export async function POST(req: NextRequest) {
const accessToken = signAccessToken({
sub: user._id.toString(),
businessId: user.businessId.toString(),
role: user.role,
branchId: user.branchId ? user.branchId.toString() : null,
deviceId,
});
const branch = user.branchId ? await Branch.findById(user.branchId).lean() : null;
const business = await Business.findById(user.businessId).lean();
const res = NextResponse.json({
user: {
id: user._id,
name: user.name,
role: user.role,
businessId: user.businessId,
businessName: business?.name ?? null,
branchId: user.branchId,
branchName: branch?.branchName ?? null,
phoneNumber: user.phoneNumber,
+4
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { connectDb } from "@/lib/server/db";
import { User } from "@/lib/server/models/User";
import { Branch } from "@/lib/server/models/Branch";
import { Business } from "@/lib/server/models/Business";
import { getAuth } from "@/lib/server/auth";
export const runtime = "nodejs";
@@ -17,11 +18,14 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
const branch = user.branchId ? await Branch.findById(user.branchId).lean() : null;
const business = await Business.findById(user.businessId).lean();
return NextResponse.json({
user: {
id: user._id,
name: user.name,
role: user.role,
businessId: user.businessId,
businessName: business?.name ?? null,
branchId: user.branchId,
branchName: branch?.branchName ?? null,
phoneNumber: user.phoneNumber,
+1
View File
@@ -37,6 +37,7 @@ export async function POST(req: NextRequest) {
const accessToken = signAccessToken({
sub: user._id.toString(),
businessId: user.businessId.toString(),
role: user.role,
branchId: user.branchId ? user.branchId.toString() : null,
deviceId,
+152
View File
@@ -0,0 +1,152 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import bcrypt from "bcryptjs";
import mongoose from "mongoose";
import { connectDb } from "@/lib/server/db";
import { User } from "@/lib/server/models/User";
import { Business } from "@/lib/server/models/Business";
import { Product } from "@/lib/server/models/Product";
import { UserSession } from "@/lib/server/models/UserSession";
import { signAccessToken, generateRefreshToken, hashRefreshToken, refreshTtlMs } from "@/lib/server/jwt";
import { ACCESS_COOKIE, REFRESH_COOKIE, accessCookieOptions, refreshCookieOptions } from "@/lib/server/auth";
import { normalizeKenyanPhone } from "@/lib/server/phone";
export const runtime = "nodejs";
/**
* Owner self-signup — the ONLY way a new tenant (Business) enters the
* system. It atomically:
* 1. creates the Business,
* 2. creates the owner as a SUPER_ADMIN (branchId = null) bound to it,
* 3. seeds a default water + eggs product catalog scoped to the business,
* 4. logs the owner straight in (session + cookies), same as /login.
*
* Branches and branch managers are created LATER by the owner from the
* admin UI; delivery staff are then added by branch managers. Signup only
* bootstraps the tenant and its first user.
*/
const signupSchema = z.object({
businessName: z.string().min(2).max(120),
ownerName: z.string().min(2).max(120),
email: z.string().email(),
phoneNumber: z.string().min(1),
password: z.string().min(8, "Password must be at least 8 characters"),
deviceId: z.string().uuid(),
deviceLabel: z.string().max(120).optional(),
});
const DEFAULT_CATALOG = [
{ label: "5L Jerrycan", sizeLiters: 5, unit: "L", category: "Water", unitPrice: 60, sortOrder: 1 },
{ label: "10L Jerrycan", sizeLiters: 10, unit: "L", category: "Water", unitPrice: 110, sortOrder: 2 },
{ label: "20L Jerrycan", sizeLiters: 20, unit: "L", category: "Water", unitPrice: 200, sortOrder: 3 },
{ label: "Tray of Eggs (30)", sizeLiters: 30, unit: "pcs", category: "Eggs", unitPrice: 400, sortOrder: 4 },
{ label: "Half Tray (15)", sizeLiters: 15, unit: "pcs", category: "Eggs", unitPrice: 210, sortOrder: 5 },
{ label: "Single Egg", sizeLiters: 1, unit: "pcs", category: "Eggs", unitPrice: 15, sortOrder: 6 },
];
export async function POST(req: NextRequest) {
await connectDb();
const json = await req.json().catch(() => null);
const parsed = signupSchema.safeParse(json);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 });
}
const { businessName, ownerName, email, phoneNumber, password, deviceId, deviceLabel } = parsed.data;
let normalizedPhone: string;
try {
normalizedPhone = normalizeKenyanPhone(phoneNumber);
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 400 });
}
const lowerEmail = email.toLowerCase();
// Email + phone are globally unique across all tenants (login is by
// email alone, with no tenant selector), so reject collisions early
// with a friendly message rather than a raw duplicate-key error.
const emailTaken = await User.findOne({ email: lowerEmail }).lean();
if (emailTaken) {
return NextResponse.json({ error: "An account with this email already exists." }, { status: 409 });
}
const phoneTaken = await User.findOne({ phoneNumber: normalizedPhone }).lean();
if (phoneTaken) {
return NextResponse.json({ error: "An account with this phone number already exists." }, { status: 409 });
}
const passwordHash = await bcrypt.hash(password, 12);
// Create owner first (with a temporary businessId), then the business,
// then point the owner at the real business id and persist. This avoids
// a chicken-and-egg between User.businessId (required) and
// Business.ownerId (required).
const ownerId = new mongoose.Types.ObjectId();
const businessId = new mongoose.Types.ObjectId();
const business = new Business({ _id: businessId, name: businessName.trim(), ownerId });
const owner = new User({
_id: ownerId,
businessId,
name: ownerName.trim(),
role: "SUPER_ADMIN",
branchId: null,
phoneNumber: normalizedPhone,
email: lowerEmail,
passwordHash,
});
try {
await business.save();
await owner.save();
} catch (e) {
// Roll back a partial create so a failed signup never leaves an
// orphaned business or user behind.
await Business.deleteOne({ _id: businessId }).catch(() => {});
await User.deleteOne({ _id: ownerId }).catch(() => {});
const msg = (e as Error).message || "";
if (msg.includes("duplicate key")) {
return NextResponse.json({ error: "An account with this email or phone already exists." }, { status: 409 });
}
return NextResponse.json({ error: "Could not create your business. Please try again." }, { status: 500 });
}
// Seed the default catalog for the new tenant.
await Product.insertMany(DEFAULT_CATALOG.map((p) => ({ ...p, businessId })));
// Auto-login: create the session + issue cookies, exactly like /login.
const refreshToken = generateRefreshToken();
const refreshTokenHash = hashRefreshToken(refreshToken);
const expiresAt = new Date(Date.now() + refreshTtlMs());
await UserSession.findOneAndUpdate(
{ userId: owner._id, deviceId },
{ refreshTokenHash, expiresAt, deviceLabel: deviceLabel ?? null, lastUsedAt: new Date() },
{ upsert: true, new: true }
);
const accessToken = signAccessToken({
sub: owner._id.toString(),
businessId: businessId.toString(),
role: owner.role,
branchId: null,
deviceId,
});
const res = NextResponse.json({
user: {
id: owner._id,
name: owner.name,
role: owner.role,
businessId,
businessName: business.name,
branchId: null,
branchName: null,
phoneNumber: owner.phoneNumber,
email: owner.email,
},
redirectTo: "/admin/dashboard",
});
res.cookies.set(ACCESS_COOKIE, accessToken, accessCookieOptions());
res.cookies.set(REFRESH_COOKIE, refreshToken, refreshCookieOptions(Math.floor(refreshTtlMs() / 1000)));
return res;
}
+5 -3
View File
@@ -13,11 +13,13 @@ export async function GET(req: NextRequest) {
if (!auth) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
if (auth.role === "SUPER_ADMIN") {
const branches = await Branch.find().sort({ branchName: 1 }).lean();
const branches = await Branch.find({ businessId: auth.businessId }).sort({ branchName: 1 }).lean();
return NextResponse.json({ branches });
}
if (!auth.branchId) return NextResponse.json({ branches: [] });
const branch = await Branch.findById(auth.branchId).lean();
// Branch-scoped users still get filtered by businessId as a backstop,
// so a stale/forged branchId from another tenant can never resolve.
const branch = await Branch.findOne({ _id: auth.branchId, businessId: auth.businessId }).lean();
return NextResponse.json({ branches: branch ? [branch] : [] });
}
@@ -38,6 +40,6 @@ export async function POST(req: NextRequest) {
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 });
}
const branch = await Branch.create(parsed.data);
const branch = await Branch.create({ ...parsed.data, businessId: auth!.businessId });
return NextResponse.json({ branch }, { status: 201 });
}
@@ -16,7 +16,7 @@ export async function GET(req: NextRequest) {
const branchId = resolveBranchScope(auth, req.nextUrl.searchParams.get("branchId"));
const date = req.nextUrl.searchParams.get("date") || new Date().toISOString().slice(0, 10);
const invMatch: Record<string, unknown> = { date };
const invMatch: Record<string, unknown> = { date, businessId: new mongoose.Types.ObjectId(auth.businessId) };
if (branchId) invMatch.branchId = new mongoose.Types.ObjectId(branchId);
const inventoryRows = await DailyInventory.aggregate([
@@ -44,6 +44,7 @@ export async function GET(req: NextRequest) {
dayEnd.setUTCDate(dayEnd.getUTCDate() + 1);
const soldMatch: Record<string, unknown> = {
businessId: new mongoose.Types.ObjectId(auth.businessId),
createdAt: { $gte: dayStart, $lt: dayEnd },
paymentStatus: "SUCCESS",
};
+5 -2
View File
@@ -23,7 +23,10 @@ export async function GET(req: NextRequest) {
const branchId = resolveBranchScope(auth, req.nextUrl.searchParams.get("branchId"));
const { start, end } = todayRange();
const matchStage: Record<string, unknown> = { createdAt: { $gte: start, $lt: end } };
const matchStage: Record<string, unknown> = {
businessId: new mongoose.Types.ObjectId(auth.businessId),
createdAt: { $gte: start, $lt: end },
};
if (branchId) matchStage.branchId = new mongoose.Types.ObjectId(branchId);
const [totals] = await Transaction.aggregate([
@@ -66,7 +69,7 @@ export async function GET(req: NextRequest) {
let branchRanking: any[] = [];
if (!branchId) {
branchRanking = await Transaction.aggregate([
{ $match: { createdAt: { $gte: start, $lt: end }, paymentStatus: "SUCCESS" } },
{ $match: { businessId: new mongoose.Types.ObjectId(auth.businessId), createdAt: { $gte: start, $lt: end }, paymentStatus: "SUCCESS" } },
{
$group: {
_id: "$branchId",
+9 -1
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { connectDb } from "@/lib/server/db";
import { Expense, EXPENSE_CATEGORIES } from "@/lib/server/models/Expense";
import { Branch } from "@/lib/server/models/Branch";
import { getAuth, resolveBranchScope } from "@/lib/server/auth";
export const runtime = "nodejs";
@@ -18,7 +19,7 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const scopeBranch = resolveBranchScope(auth, url.searchParams.get("branchId"));
const filter: Record<string, unknown> = {};
const filter: Record<string, unknown> = { businessId: auth.businessId };
if (scopeBranch) filter.branchId = scopeBranch;
const from = url.searchParams.get("from");
@@ -80,7 +81,14 @@ export async function POST(req: NextRequest) {
}
}
// The target branch MUST belong to this tenant.
const branch = await Branch.findOne({ _id: branchId, businessId: auth.businessId }).lean();
if (!branch) {
return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 });
}
const expense = await Expense.create({
businessId: auth.businessId,
branchId,
category: parsed.data.category,
amount: parsed.data.amount,
+8 -2
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { connectDb } from "@/lib/server/db";
import { DailyInventory } from "@/lib/server/models/DailyInventory";
import { Branch } from "@/lib/server/models/Branch";
import { getAuth, requireRole, resolveBranchScope } from "@/lib/server/auth";
export const runtime = "nodejs";
@@ -33,11 +34,16 @@ export async function POST(req: NextRequest) {
const branchId = resolveBranchScope(auth!, parsed.data.branchId);
if (!branchId) return NextResponse.json({ error: "branchId is required" }, { status: 400 });
// The branch MUST belong to this tenant — blocks a crafted branchId from
// writing inventory into another business.
const branch = await Branch.findOne({ _id: branchId, businessId: auth!.businessId }).lean();
if (!branch) return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 });
const date = parsed.data.date ?? todayLocal();
const row = await DailyInventory.findOneAndUpdate(
{ branchId, productId: parsed.data.productId, date },
{ morningDispatched: parsed.data.morningDispatched, recordedBy: auth!.sub },
{ upsert: true, new: true }
{ morningDispatched: parsed.data.morningDispatched, recordedBy: auth!.sub, businessId: auth!.businessId },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
return NextResponse.json({ inventory: row });
}
+7 -2
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { connectDb } from "@/lib/server/db";
import { DailyInventory } from "@/lib/server/models/DailyInventory";
import { Branch } from "@/lib/server/models/Branch";
import { getAuth, requireRole, resolveBranchScope } from "@/lib/server/auth";
export const runtime = "nodejs";
@@ -33,11 +34,15 @@ export async function POST(req: NextRequest) {
const branchId = resolveBranchScope(auth!, parsed.data.branchId);
if (!branchId) return NextResponse.json({ error: "branchId is required" }, { status: 400 });
// The branch MUST belong to this tenant.
const branch = await Branch.findOne({ _id: branchId, businessId: auth!.businessId }).lean();
if (!branch) return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 });
const date = parsed.data.date ?? todayLocal();
const row = await DailyInventory.findOneAndUpdate(
{ branchId, productId: parsed.data.productId, date },
{ eveningPhysicalCount: parsed.data.eveningPhysicalCount, recordedBy: auth!.sub },
{ upsert: true, new: true }
{ eveningPhysicalCount: parsed.data.eveningPhysicalCount, recordedBy: auth!.sub, businessId: auth!.businessId },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
return NextResponse.json({ inventory: row });
}
@@ -12,7 +12,7 @@ export async function GET(req: NextRequest, { params }: { params: { transactionI
const auth = getAuth(req);
if (!auth) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
const txn = await Transaction.findById(params.transactionId);
const txn = await Transaction.findOne({ _id: params.transactionId, businessId: auth.businessId });
if (!txn) return NextResponse.json({ error: "Transaction not found" }, { status: 404 });
const ageMs = Date.now() - txn.createdAt.getTime();
+4 -3
View File
@@ -64,20 +64,21 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: e.message }, { status: 400 });
}
const product = await Product.findById(productId).lean();
const product = await Product.findOne({ _id: productId, businessId: auth!.businessId }).lean();
if (!product || !product.isActive) {
return NextResponse.json({ error: "Selected product is not available" }, { status: 400 });
}
const override = await BranchPricing.findOne({ branchId, productId }).lean();
const override = await BranchPricing.findOne({ branchId, productId, businessId: auth!.businessId }).lean();
const unitPrice = override?.unitPrice ?? product.unitPrice;
const amount = Math.round(quantity * unitPrice * 100) / 100;
if (amount <= 0) {
return NextResponse.json({ error: "Invalid sale amount" }, { status: 400 });
}
const branch = await Branch.findById(branchId).lean();
const branch = await Branch.findOne({ _id: branchId, businessId: auth!.businessId }).lean();
const txn = await Transaction.create({
businessId: auth!.businessId,
branchId,
staffId: auth!.sub,
customerName: customerName ?? null,
+5 -1
View File
@@ -30,7 +30,11 @@ export async function PATCH(req: NextRequest, { params }: { params: { id: string
return NextResponse.json({ error: "No fields to update" }, { status: 400 });
}
const product = await Product.findByIdAndUpdate(params.id, parsed.data, { new: true });
const product = await Product.findOneAndUpdate(
{ _id: params.id, businessId: auth!.businessId },
parsed.data,
{ new: true }
);
if (!product) return NextResponse.json({ error: "Product not found" }, { status: 404 });
return NextResponse.json({ product });
}
+2 -2
View File
@@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
const auth = getAuth(req);
if (!auth) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
const products = await Product.find({ isActive: true }).sort({ sortOrder: 1 }).lean();
const products = await Product.find({ businessId: auth.businessId, isActive: true }).sort({ sortOrder: 1 }).lean();
return NextResponse.json({ products });
}
@@ -37,6 +37,6 @@ export async function POST(req: NextRequest) {
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 });
}
const product = await Product.create(parsed.data);
const product = await Product.create({ ...parsed.data, businessId: auth!.businessId });
return NextResponse.json({ product }, { status: 201 });
}
+10 -2
View File
@@ -3,6 +3,7 @@ import { z } from "zod";
import { connectDb } from "@/lib/server/db";
import { Purchase } from "@/lib/server/models/Purchase";
import { Product } from "@/lib/server/models/Product";
import { Branch } from "@/lib/server/models/Branch";
import { getAuth, resolveBranchScope } from "@/lib/server/auth";
export const runtime = "nodejs";
@@ -17,7 +18,7 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const scopeBranch = resolveBranchScope(auth, url.searchParams.get("branchId"));
const filter: Record<string, unknown> = {};
const filter: Record<string, unknown> = { businessId: auth.businessId };
if (scopeBranch) filter.branchId = scopeBranch;
const from = url.searchParams.get("from");
@@ -82,12 +83,18 @@ export async function POST(req: NextRequest) {
}
}
// The target branch MUST belong to this tenant.
const branchDoc = await Branch.findOne({ _id: branchId, businessId: auth.businessId }).lean();
if (!branchDoc) {
return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 });
}
// Resolve a human-readable product label snapshot. Prefer an explicit
// productId lookup; fall back to a free-text label for ad-hoc items.
let productLabel = parsed.data.productLabel ?? null;
let productId: string | null = parsed.data.productId ?? null;
if (productId) {
const product = await Product.findById(productId).lean();
const product = await Product.findOne({ _id: productId, businessId: auth.businessId }).lean();
if (!product) {
return NextResponse.json({ error: "Product not found" }, { status: 400 });
}
@@ -100,6 +107,7 @@ export async function POST(req: NextRequest) {
const totalCost = parsed.data.quantity * parsed.data.unitCost;
const purchase = await Purchase.create({
businessId: auth.businessId,
branchId,
supplierName: parsed.data.supplierName,
productId,
+3 -2
View File
@@ -30,12 +30,13 @@ export async function GET(req: NextRequest) {
const to = url.searchParams.get("to") ? new Date(url.searchParams.get("to")!) : now;
const branchObjId = branchId ? new mongoose.Types.ObjectId(branchId) : null;
const businessObjId = new mongoose.Types.ObjectId(auth.businessId);
const dateMatch = { createdAt: { $gte: from, $lte: to } };
const salesMatch: Record<string, unknown> = { ...dateMatch, paymentStatus: "SUCCESS" };
const salesMatch: Record<string, unknown> = { ...dateMatch, businessId: businessObjId, paymentStatus: "SUCCESS" };
if (branchObjId) salesMatch.branchId = branchObjId;
const costMatch: Record<string, unknown> = { ...dateMatch };
const costMatch: Record<string, unknown> = { ...dateMatch, businessId: businessObjId };
if (branchObjId) costMatch.branchId = branchObjId;
const [salesAgg, purchaseAgg, expenseAgg] = await Promise.all([
+3 -2
View File
@@ -39,8 +39,8 @@ export async function POST(req: NextRequest) {
const { sales } = parsed.data;
const productIds = Array.from(new Set(sales.map((s) => s.productId)));
const products = await Product.find({ _id: { $in: productIds } }).lean();
const overrides = await BranchPricing.find({ branchId, productId: { $in: productIds } }).lean();
const products = await Product.find({ _id: { $in: productIds }, businessId: auth!.businessId }).lean();
const overrides = await BranchPricing.find({ branchId, productId: { $in: productIds }, businessId: auth!.businessId }).lean();
const productMap = new Map(products.map((p) => [p._id.toString(), p]));
const overrideMap = new Map(overrides.map((o) => [o.productId.toString(), o.unitPrice]));
@@ -64,6 +64,7 @@ export async function POST(req: NextRequest) {
try {
const created = await Transaction.create({
businessId: auth!.businessId,
branchId,
staffId: auth!.sub,
customerName: sale.customerName ?? null,
+1 -1
View File
@@ -14,7 +14,7 @@ export async function GET(req: NextRequest) {
const branchId = resolveBranchScope(auth, req.nextUrl.searchParams.get("branchId"));
const limit = Math.min(Number(req.nextUrl.searchParams.get("limit")) || 50, 200);
const filter: Record<string, unknown> = {};
const filter: Record<string, unknown> = { businessId: auth.businessId };
if (branchId) filter.branchId = branchId;
const startOfDay = new Date();
+18 -2
View File
@@ -18,7 +18,9 @@ export async function GET(req: NextRequest) {
}
const branchId = resolveBranchScope(auth!, req.nextUrl.searchParams.get("branchId"));
const filter: Record<string, unknown> = branchId ? { branchId } : {};
// businessId is the hard tenant boundary; branchId narrows within it.
const filter: Record<string, unknown> = { businessId: auth!.businessId };
if (branchId) filter.branchId = branchId;
const users = await User.find(filter).select("-passwordHash").sort({ name: 1 }).lean();
return NextResponse.json({ users });
}
@@ -60,6 +62,16 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "branchId is required for this role" }, { status: 400 });
}
// Any branch supplied (by a SUPER_ADMIN creating a manager/staff for a
// specific branch) MUST belong to this business — otherwise a crafted
// branchId from another tenant could attach a user cross-tenant.
if (branchId) {
const branch = await Branch.findOne({ _id: branchId, businessId: auth!.businessId }).lean();
if (!branch) {
return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 });
}
}
let phone: string;
try {
phone = normalizeKenyanPhone(data.phoneNumber);
@@ -71,6 +83,7 @@ export async function POST(req: NextRequest) {
try {
const user = await User.create({
businessId: auth!.businessId,
name: data.name,
role: data.role,
branchId: branchId ?? null,
@@ -80,7 +93,10 @@ export async function POST(req: NextRequest) {
});
if (data.role === "BRANCH_MANAGER" && branchId) {
await Branch.findByIdAndUpdate(branchId, { managerId: user._id });
await Branch.findOneAndUpdate(
{ _id: branchId, businessId: auth!.businessId },
{ managerId: user._id }
);
}
const obj: any = user.toObject();