import type { User as AuthJsUser } from "next-auth"; import { prisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { getSeats, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; import { isServiceError } from "@/lib/utils"; import { orgNotFound, ServiceError, userNotFound } from "@/lib/serviceError"; import { createLogger } from "@sourcebot/logger"; import { getAuditService } from "@/ee/features/audit/factory"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "./errorCodes"; import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { incrementOrgSeatCount } from "@/ee/features/billing/serverUtils"; const logger = createLogger('web-auth-utils'); const auditService = getAuditService(); export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { if (!user.id) { logger.error("User ID is undefined on user creation"); await auditService.createAudit({ action: "user.creation_failed", actor: { id: "undefined", type: "user" }, target: { id: "undefined", type: "user" }, orgId: SINGLE_TENANT_ORG_ID, metadata: { message: "User ID is undefined on user creation" } }); throw new Error("User ID is undefined on user creation"); } const defaultOrg = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, }, include: { members: { where: { role: { not: OrgRole.GUEST, } } }, } }); // We expect the default org to have been created on app initialization if (defaultOrg === null) { await auditService.createAudit({ action: "user.creation_failed", actor: { id: user.id, type: "user" }, target: { id: user.id, type: "user" }, orgId: SINGLE_TENANT_ORG_ID, metadata: { message: "Default org not found on single tenant user creation" } }); throw new Error("Default org not found on single tenant user creation"); } // If this is the first user to sign up, we make them the owner of the default org. const isFirstUser = defaultOrg.members.length === 0; if (isFirstUser) { await prisma.$transaction(async (tx) => { await tx.org.update({ where: { id: SINGLE_TENANT_ORG_ID, }, data: { members: { create: { role: OrgRole.OWNER, user: { connect: { id: user.id, } } } } } }); }); await auditService.createAudit({ action: "user.owner_created", actor: { id: user.id, type: "user" }, orgId: SINGLE_TENANT_ORG_ID, target: { id: SINGLE_TENANT_ORG_ID.toString(), type: "org" } }); } else if (!defaultOrg.memberApprovalRequired) { const hasAvailability = await orgHasAvailability(defaultOrg.domain); if (!hasAvailability) { logger.warn(`onCreateUser: org ${SINGLE_TENANT_ORG_ID} has reached max capacity. User ${user.id} was not added to the org.`); return; } await prisma.userToOrg.create({ data: { userId: user.id, orgId: SINGLE_TENANT_ORG_ID, role: OrgRole.MEMBER, } }); } }; export const orgHasAvailability = async (domain: string): Promise => { const org = await prisma.org.findUnique({ where: { domain, }, }); if (!org) { logger.error(`orgHasAvailability: org not found for domain ${domain}`); return false; } const members = await prisma.userToOrg.findMany({ where: { orgId: org.id, role: { not: OrgRole.GUEST, }, }, }); const maxSeats = getSeats(); const memberCount = members.length; if (maxSeats !== SOURCEBOT_UNLIMITED_SEATS && memberCount >= maxSeats) { logger.error(`orgHasAvailability: org ${org.id} has reached max capacity`); return false; } return true; } export const addUserToOrganization = async (userId: string, orgId: number): Promise<{ success: boolean } | ServiceError> => { const user = await prisma.user.findUnique({ where: { id: userId, }, }); if (!user) { logger.error(`addUserToOrganization: user not found for id ${userId}`); return userNotFound(); } const org = await prisma.org.findUnique({ where: { id: orgId, }, }); if (!org) { logger.error(`addUserToOrganization: org not found for id ${orgId}`); return orgNotFound(); } const hasAvailability = await orgHasAvailability(org.domain); if (!hasAvailability) { return { statusCode: StatusCodes.BAD_REQUEST, errorCode: ErrorCode.ORG_SEAT_COUNT_REACHED, message: "Organization is at max capacity", } satisfies ServiceError; } const res = await prisma.$transaction(async (tx) => { await tx.userToOrg.create({ data: { userId: user.id, orgId: org.id, role: OrgRole.MEMBER, } }); if (IS_BILLING_ENABLED) { const result = await incrementOrgSeatCount(orgId, tx); if (isServiceError(result)) { throw result; } } // Delete the account request if it exists since we've added the user to the org const accountRequest = await tx.accountRequest.findUnique({ where: { requestedById_orgId: { requestedById: user.id, orgId: orgId, } }, }); if (accountRequest) { logger.info(`Deleting account request ${accountRequest.id} for user ${user.id} since they've been added to the org`); await tx.accountRequest.delete({ where: { id: accountRequest.id, } }); } // Delete any invites that may exist for this user since we've added them to the org const invites = await tx.invite.findMany({ where: { recipientEmail: user.email!, orgId: org.id, }, }) for (const invite of invites) { logger.info(`Deleting invite ${invite.id} for ${user.email} since they've been added to the org`); await tx.invite.delete({ where: { id: invite.id, }, }); } }); if (isServiceError(res)) { logger.error(`addUserToOrganization: failed to add user ${userId} to org ${orgId}: ${res.message}`); return res; } return { success: true, } };