From 8241fa3eaa7b5c9e5aef0279d7fd54d072164c7b Mon Sep 17 00:00:00 2001 From: Akhil Meka Date: Mon, 1 Dec 2025 17:26:35 +0530 Subject: [PATCH] feat: allow users to have access to multiple oorgs --- src/alert/alert.service.ts | 15 +++++++++++--- src/auth.ts | 4 ++-- src/auth/auth.service.ts | 2 +- src/ctask/ctask.schema.ts | 2 ++ src/ctask/ctask.service.ts | 26 ++++++++++++++++++------ src/events/events.service.ts | 2 +- src/notification/notification.service.ts | 6 +++++- src/organization/organization.service.ts | 12 +++++++++-- src/payments/payments.service.ts | 11 ++++++++-- src/permit/permit.service.ts | 18 +++++++++++++--- src/processed/processed.service.ts | 12 +++++++++-- src/realtime/realtime.route.ts | 2 +- src/rts/rts.service.ts | 8 ++++++-- src/unique.ts | 2 +- src/user/user.controller.ts | 5 ++++- src/user/user.schema.ts | 20 ++++++++++-------- src/utils/errors.ts | 14 ++++++------- 17 files changed, 117 insertions(+), 44 deletions(-) diff --git a/src/alert/alert.service.ts b/src/alert/alert.service.ts index 6965a0a..9226d49 100644 --- a/src/alert/alert.service.ts +++ b/src/alert/alert.service.ts @@ -53,7 +53,10 @@ export async function getUserAlerts( ]; if (user.role == "client") - filters.push({ recipientType: "team", recipientId: user.orgId }); + filters.push({ + recipientType: "team", + recipientId: { $in: [...user.orgId] }, + }); else filters.push({ recipientType: "team" }); const alerts = await alertsModel @@ -99,7 +102,10 @@ export async function markAllRead(user: AuthenticatedUser) { ]; if (user.role == "client") - filters.push({ recipientType: "team", recipientId: user.orgId }); + filters.push({ + recipientType: "team", + recipientId: { $in: [...user.orgId] }, + }); else filters.push({ recipientType: "team" }); const updatedResult = await alertsModel.updateMany( @@ -116,7 +122,10 @@ export async function getUnreadCount(user: AuthenticatedUser) { ]; if (user.role == "client") - filters.push({ recipientType: "team", recipientId: user.orgId }); + filters.push({ + recipientType: "team", + recipientId: { $in: [...user.orgId] }, + }); else filters.push({ recipientType: "team" }); const alerts = await alertsModel.find({ diff --git a/src/auth.ts b/src/auth.ts index e8dd586..6f0255f 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,4 +1,4 @@ -import bcrypt from 'bcryptjs'; +import bcrypt from "bcryptjs"; import { FastifyReply, FastifyRequest } from "fastify"; import { getToken } from "./tokens/token.service"; import { Claim } from "./utils/claims"; @@ -15,7 +15,7 @@ export type AuthenticatedUser = { sid?: string; type: string; userId?: string; - orgId?: string; + orgId?: Array; role?: string; tenantId: string; claims: Array; diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d375416..248edaa 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -35,7 +35,7 @@ export async function getSession( sid: session.id, type: "user", userId: user.id, - orgId: user.orgId ? user.orgId.toString() : null, + orgId: user.orgId ? user.orgId.map((item) => item.toString()) : [], role: user.role, tenantId: user.tenantId, claims: rules[user.role].claims ?? [], diff --git a/src/ctask/ctask.schema.ts b/src/ctask/ctask.schema.ts index c03dcc9..52b0e94 100644 --- a/src/ctask/ctask.schema.ts +++ b/src/ctask/ctask.schema.ts @@ -58,6 +58,7 @@ const createTaskInput = z.object({ }) .optional(), note: z.string().optional(), + orgId: z.string().optional(), }); const updateTaskInput = z.object({ @@ -80,6 +81,7 @@ const updateTaskInput = z.object({ currentStage: z.number(), }) .optional(), + orgId: z.string().optional(), }); const uploadTaskInput = z.object({ diff --git a/src/ctask/ctask.service.ts b/src/ctask/ctask.service.ts index 3f8d90b..9703cd0 100644 --- a/src/ctask/ctask.service.ts +++ b/src/ctask/ctask.service.ts @@ -10,6 +10,8 @@ import { UpdateTaskInput, } from "./ctask.schema"; +const InvalidOrg = new Error("invalid orgId"); + export async function createTask( input: CreateTaskInput, user: AuthenticatedUser @@ -21,10 +23,14 @@ export async function createTask( }; } + if (input.orgId && !user.orgId.includes(input.orgId)) { + throw InvalidOrg; + } + const task = await taskModel.create({ ...input, tenantId: user.tenantId, - orgId: user.orgId, + orgId: input.orgId ?? user.orgId[0], pid: generateId(), createdAt: new Date(), createdBy: user.userId ?? null, @@ -52,9 +58,13 @@ export async function updateTask( input: UpdateTaskInput, user: AuthenticatedUser ) { + if (input.orgId && !user.orgId.includes(input.orgId)) { + throw InvalidOrg; + } + const updatedTask = await taskModel .findOneAndUpdate( - { tenantId: user.tenantId, orgId: user.orgId, pid: taskId }, + { tenantId: user.tenantId, orgId: { $in: user.orgId }, pid: taskId }, input, { new: true } ) @@ -66,7 +76,11 @@ export async function updateTask( export async function getTask(taskId: string, user: AuthenticatedUser) { return await taskModel - .findOne({ tenantId: user.tenantId, orgId: user.orgId, pid: taskId }) + .findOne({ + tenantId: user.tenantId, + orgId: { $in: user.orgId }, + pid: taskId, + }) .populate({ path: "createdBy", select: "pid name avatar" }) .populate({ path: "assignedTo", select: "pid name avatar" }); } @@ -118,7 +132,7 @@ export async function listTasks( $match: { $and: [ { tenantId: user.tenantId }, - { orgId: user.orgId }, + { orgId: { $in: user.orgId } }, ...filterObj, ], }, @@ -204,7 +218,7 @@ export async function listTasks( export async function deleteTask(taskId: string, user: AuthenticatedUser) { return await taskModel.deleteOne({ pid: taskId, - orgId: user.orgId, + orgId: { $in: user.orgId }, tenantId: user.tenantId, }); } @@ -225,7 +239,7 @@ export async function searchTasks( $match: { $and: [ { tenantId: user.tenantId }, - { orgId: user.orgId }, + { orgId: { $in: user.orgId } }, ...filterObj, ], }, diff --git a/src/events/events.service.ts b/src/events/events.service.ts index 87a2f3e..8c9caa4 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -19,7 +19,7 @@ export async function createEvent(event: ChangeEvent) { export async function getEvents(user: AuthenticatedUser, from?: Date) { const filter: any[] = [{ tenantId: user.tenantId }]; - if (user.role == "client") filter.push({ orgId: user.orgId }); + if (user.role == "client") filter.push({ orgId: { $in: user.orgId } }); if (from) filter.push({ createdAt: { $gt: from } }); return await eventsModel.find({ $and: filter }).sort({ createdAt: -1 }); diff --git a/src/notification/notification.service.ts b/src/notification/notification.service.ts index a96c4b5..965fde4 100644 --- a/src/notification/notification.service.ts +++ b/src/notification/notification.service.ts @@ -152,7 +152,11 @@ export async function listNotifications( }); if (user.role == "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter( diff --git a/src/organization/organization.service.ts b/src/organization/organization.service.ts index 55ea186..8098534 100644 --- a/src/organization/organization.service.ts +++ b/src/organization/organization.service.ts @@ -51,7 +51,11 @@ export async function listOrgs( filterObj.push({ $or: [ { type: "county" }, - { _id: new mongoose.Types.ObjectId(user.orgId) }, + { + _id: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }, ], }); } @@ -148,7 +152,11 @@ export async function searchOrgs( filterObj.push({ $or: [ { type: "county" }, - { _id: new mongoose.Types.ObjectId(user.orgId) }, + { + _id: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }, ], }); } diff --git a/src/payments/payments.service.ts b/src/payments/payments.service.ts index 3c1eae3..f58cac4 100644 --- a/src/payments/payments.service.ts +++ b/src/payments/payments.service.ts @@ -15,7 +15,10 @@ export async function getPayment(paymentId: string, user: AuthenticatedUser) { pid: paymentId, }); - if (user.role === "client" && paymentObj.client.toString() !== user.orgId) + if ( + user.role === "client" && + !user.orgId.includes(paymentObj.client.toString()) + ) return null; return paymentObj; @@ -31,7 +34,11 @@ export async function listPayments( const filterObj = getFilterObject(params) || []; if (user.role == "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } const pipeline: Array = [ diff --git a/src/permit/permit.service.ts b/src/permit/permit.service.ts index 14b2486..e91d130 100644 --- a/src/permit/permit.service.ts +++ b/src/permit/permit.service.ts @@ -108,7 +108,11 @@ export async function getPermit(permitId: string, user: AuthenticatedUser) { .populate({ path: "assignedTo", select: "pid name avatar" }) .populate({ path: "createdBy", select: "pid name avatar" }); - if (permit && user.role == "client" && user.orgId != permit.client.toString()) + if ( + permit && + user.role == "client" && + !user.orgId.includes(permit.client.toString()) + ) return null; return permit; @@ -124,7 +128,11 @@ export async function listPermits( let filterObj = getFilterObject(params) || []; if (user.role == "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter( @@ -395,7 +403,11 @@ export async function searchPermit( const filterObj = getFilterObject(params) || []; if (user.role == "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } if (!params.searchToken) diff --git a/src/processed/processed.service.ts b/src/processed/processed.service.ts index 58fab04..c679f15 100644 --- a/src/processed/processed.service.ts +++ b/src/processed/processed.service.ts @@ -28,7 +28,11 @@ export async function getProcessedPermit( .populate({ path: "assignedTo", select: "pid name avatar" }) .populate({ path: "createdBy", select: "pid name avatar" }); - if (permit && user.role == "client" && user.orgId != permit.client.toString()) + if ( + permit && + user.role == "client" && + !user.orgId.includes(permit.client.toString()) + ) return null; return permit; @@ -138,7 +142,11 @@ export async function listProcessedPermits( let filterObj = getFilterObject(params) || []; if (user.role == "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter( diff --git a/src/realtime/realtime.route.ts b/src/realtime/realtime.route.ts index bc0d963..ad69360 100644 --- a/src/realtime/realtime.route.ts +++ b/src/realtime/realtime.route.ts @@ -35,7 +35,7 @@ export async function realTimeRoutes(fastify: FastifyInstance) { if ( req.user.role == "client" && - event.document.recipientId == req.user.orgId + req.user.orgId.includes(event.document.recipientId) ) alertFlag = true; } diff --git a/src/rts/rts.service.ts b/src/rts/rts.service.ts index ad8abc2..69fe3eb 100644 --- a/src/rts/rts.service.ts +++ b/src/rts/rts.service.ts @@ -27,7 +27,7 @@ export async function createRts( let defaultClient = input.client; const userInDb = await getUserWithoutPopulate(user.userId); if (userInDb && userInDb.orgId) { - defaultClient = userInDb.orgId.toString(); + defaultClient = userInDb.orgId[0].toString(); } if (!input.stage) { @@ -83,7 +83,11 @@ export async function listRts( let filterObj = getFilterObject(params) || []; if (user.role === "client") { - filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); + filterObj.push({ + client: { + $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), + }, + }); } let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter( diff --git a/src/unique.ts b/src/unique.ts index a5a1571..155bb86 100644 --- a/src/unique.ts +++ b/src/unique.ts @@ -53,7 +53,7 @@ export async function getUniqueFields( matchedValues = await orgModel.find().where("_id").in(values).exec(); } else if (field === "client") { if (user.role == "client") { - values = [user.orgId]; + values = user.orgId; } matchedValues = await orgModel.find().where("_id").in(values).exec(); diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 9c512d6..6a49b03 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -76,7 +76,10 @@ export async function getUserHandler(req: FastifyRequest, res: FastifyReply) { if (user == null) return res.code(404).send({ error: "resource not found" }); - if (req.user.role == "client" && user.orgId.toString() != req.user.orgId) + if ( + req.user.role == "client" && + !req.user.orgId.includes(user.orgId.toString()) + ) return res.code(404).send({ error: "resource not found" }); return res.code(200).send(user); diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index 0fd0f50..e5fcf69 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -1,5 +1,5 @@ import { buildJsonSchemas } from "fastify-zod"; -import mongoose, { InferSchemaType } from "mongoose"; +import mongoose, { InferSchemaType, Schema } from "mongoose"; import { z } from "zod"; import { roles } from "../utils/roles"; @@ -13,7 +13,7 @@ const userSchema = new mongoose.Schema({ unique: true, required: true, }, - orgId: { type: mongoose.Types.ObjectId, ref: "organization" }, + orgId: { type: [Schema.Types.ObjectId], ref: "organization" }, firstName: String, lastName: String, name: String, @@ -73,7 +73,7 @@ const userCore = { .email(), avatar: z.string().optional(), role: z.enum(roles), - orgId: z.string().optional(), + orgId: z.array(z.string()).optional(), password: z.string().optional(), }; @@ -98,18 +98,20 @@ const updateUserInput = z.object({ .optional(), avatar: z.string().url().optional(), role: z.enum(roles).optional(), - orgId: z.string().optional(), + orgId: z.array(z.string()).optional(), }); const userResponse = z.object({ _id: z.string(), pid: z.string(), orgId: z - .object({ - _id: z.string().optional(), - pid: z.string().optional(), - name: z.string().optional(), - }) + .array( + z.object({ + _id: z.string().optional(), + pid: z.string().optional(), + name: z.string().optional(), + }) + ) .optional(), firstName: z.string().optional(), lastName: z.string().optional(), diff --git a/src/utils/errors.ts b/src/utils/errors.ts index a381cfd..e9863f3 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,5 +1,5 @@ -import { FastifyReply, FastifyRequest } from 'fastify'; -import mongoose from 'mongoose'; +import { FastifyReply, FastifyRequest } from "fastify"; +import mongoose from "mongoose"; export function errorHandler( error: any, @@ -12,7 +12,7 @@ export function errorHandler( if (error.validation) { const errMsg = { - type: 'validation_error', + type: "validation_error", path: error.validation[0].instancePath, context: error.validationContext, msg: error.validation[0].message, @@ -25,13 +25,13 @@ export function errorHandler( if (error instanceof mongoose.mongo.MongoServerError) { if (error.code === 11000) { return res.code(400).send({ - type: 'duplicate_key', - context: 'body', - msg: 'value already exists', + type: "duplicate_key", + context: "body", + msg: "value already exists", params: error.keyValue, }); } } - return res.code(500).send(); + return res.code(500).send(error.message); }