From 831eb6325c6ec5e9df2686f546032066a1a7ab56 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Feb 2026 17:31:41 -0800 Subject: [PATCH] Centralize user functions --- server/lib/userOrg.ts | 142 +++++++++++++++++++++++++++ server/routers/user/createOrgUser.ts | 52 ++-------- server/routers/user/removeUserOrg.ts | 88 +---------------- 3 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 server/lib/userOrg.ts diff --git a/server/lib/userOrg.ts b/server/lib/userOrg.ts new file mode 100644 index 00000000..6ed10039 --- /dev/null +++ b/server/lib/userOrg.ts @@ -0,0 +1,142 @@ +import { + db, + Org, + orgs, + resources, + siteResources, + sites, + Transaction, + UserOrg, + userOrgs, + userResources, + userSiteResources, + userSites +} from "@server/db"; +import { eq, and, inArray, ne, exists } from "drizzle-orm"; +import { usageService } from "@server/lib/billing/usageService"; +import { FeatureId } from "@server/lib/billing"; + +export async function assignUserToOrg( + org: Org, + values: typeof userOrgs.$inferInsert, + trx: Transaction | typeof db = db +) { + const [userOrg] = await trx.insert(userOrgs).values(values).returning(); + + // calculate if the user is in any other of the orgs before we count it as an add to the billing org + if (org.billingOrgId) { + const otherBillingOrgs = await trx + .select() + .from(orgs) + .where( + and( + eq(orgs.billingOrgId, org.billingOrgId), + ne(orgs.orgId, org.orgId) + ) + ); + + const billingOrgIds = otherBillingOrgs.map((o) => o.orgId); + + const orgsInBillingDomainThatTheUserIsStillIn = await trx + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userOrg.userId), + inArray(userOrgs.orgId, billingOrgIds) + ) + ); + + if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { + await usageService.add(org.orgId, FeatureId.USERS, 1, trx); + } + } +} + +export async function removeUserFromOrg( + org: Org, + userId: string, + trx: Transaction | typeof db = db +) { + await trx + .delete(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId))); + + await trx.delete(userResources).where( + and( + eq(userResources.userId, userId), + exists( + trx + .select() + .from(resources) + .where( + and( + eq(resources.resourceId, userResources.resourceId), + eq(resources.orgId, org.orgId) + ) + ) + ) + ) + ); + + await trx.delete(userSiteResources).where( + and( + eq(userSiteResources.userId, userId), + exists( + trx + .select() + .from(siteResources) + .where( + and( + eq( + siteResources.siteResourceId, + userSiteResources.siteResourceId + ), + eq(siteResources.orgId, org.orgId) + ) + ) + ) + ) + ); + + await trx.delete(userSites).where( + and( + eq(userSites.userId, userId), + exists( + db + .select() + .from(sites) + .where( + and( + eq(sites.siteId, userSites.siteId), + eq(sites.orgId, org.orgId) + ) + ) + ) + ) + ); + + // calculate if the user is in any other of the orgs before we count it as an remove to the billing org + if (org.billingOrgId) { + const billingOrgs = await trx + .select() + .from(orgs) + .where(eq(orgs.billingOrgId, org.billingOrgId)); + + const billingOrgIds = billingOrgs.map((o) => o.orgId); + + const orgsInBillingDomainThatTheUserIsStillIn = await trx + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + inArray(userOrgs.orgId, billingOrgIds) + ) + ); + + if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { + await usageService.add(org.orgId, FeatureId.USERS, -1, trx); + } + } +} diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index f9cab25e..b39ea22e 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -16,6 +16,7 @@ import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { assignUserToOrg } from "@server/lib/userOrg"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -220,15 +221,12 @@ export async function createOrgUser( ); } - await trx - .insert(userOrgs) - .values({ - orgId, - userId: existingUser.userId, - roleId: role.roleId, - autoProvisioned: false - }) - .returning(); + await assignUserToOrg(org, { + orgId, + userId: existingUser.userId, + roleId: role.roleId, + autoProvisioned: false + }, trx); } else { userId = generateId(15); @@ -246,47 +244,15 @@ export async function createOrgUser( }) .returning(); - await trx - .insert(userOrgs) - .values({ + await assignUserToOrg(org, { orgId, userId: newUser.userId, roleId: role.roleId, autoProvisioned: false - }) - .returning(); + }, trx); } await calculateUserClientsForOrgs(userId, trx); - - // calculate if the user is in any other of the orgs before we count it as an add to the billing org - if (org.billingOrgId) { - const otherBillingOrgs = await trx - .select() - .from(orgs) - .where( - and( - eq(orgs.billingOrgId, org.billingOrgId), - ne(orgs.orgId, orgId) - ) - ); - - const billingOrgIds = otherBillingOrgs.map((o) => o.orgId); - - const orgsInBillingDomainThatTheUserIsStillIn = await trx - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - inArray(userOrgs.orgId, billingOrgIds) - ) - ); - - if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { - await usageService.add(orgId, FeatureId.USERS, 1, trx); - } - } }); } else { return next( diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index d90d78c0..4c321ad3 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -22,6 +22,7 @@ import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { UserType } from "@server/types/UserTypes"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; +import { removeUserFromOrg } from "@server/lib/userOrg"; const removeUserSchema = z.strictObject({ userId: z.string(), @@ -89,68 +90,7 @@ export async function removeUserOrg( } await db.transaction(async (trx) => { - await trx - .delete(userOrgs) - .where( - and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) - ); - - await db.delete(userResources).where( - and( - eq(userResources.userId, userId), - exists( - db - .select() - .from(resources) - .where( - and( - eq( - resources.resourceId, - userResources.resourceId - ), - eq(resources.orgId, orgId) - ) - ) - ) - ) - ); - - await db.delete(userSiteResources).where( - and( - eq(userSiteResources.userId, userId), - exists( - db - .select() - .from(siteResources) - .where( - and( - eq( - siteResources.siteResourceId, - userSiteResources.siteResourceId - ), - eq(siteResources.orgId, orgId) - ) - ) - ) - ) - ); - - await db.delete(userSites).where( - and( - eq(userSites.userId, userId), - exists( - db - .select() - .from(sites) - .where( - and( - eq(sites.siteId, userSites.siteId), - eq(sites.orgId, orgId) - ) - ) - ) - ) - ); + await removeUserFromOrg(org, userId, trx); // if (build === "saas") { // const [rootUser] = await trx @@ -170,30 +110,6 @@ export async function removeUserOrg( // } await calculateUserClientsForOrgs(userId, trx); - - // calculate if the user is in any other of the orgs before we count it as an remove to the billing org - if (org.billingOrgId) { - const billingOrgs = await trx - .select() - .from(orgs) - .where(eq(orgs.billingOrgId, org.billingOrgId)); - - const billingOrgIds = billingOrgs.map((o) => o.orgId); - - const orgsInBillingDomainThatTheUserIsStillIn = await trx - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - inArray(userOrgs.orgId, billingOrgIds) - ) - ); - - if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { - await usageService.add(orgId, FeatureId.USERS, -1, trx); - } - } }); return response(res, {