Files
calendar/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
sean-brydonGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Hariom Balhara
fa35cc5210 chore: organization onboarding refactor (#24381)
* feat: redirect to new onboarding flow

* Getting started

* Brand details

* Preview organization brands

* Orgs team pages

* Invite team steps

* Move to global zustand store

* Few darkmdoe fixes

* Wip onboarding + stripe flow

* Default plan state

Server Action for gettting slug satus of org

* Remove onboardingId

* Confirmation prompt

* Update old onboarding flow handlers to handle new fields

* update onboarding hook

* Filter out organization section for none -company emails

* Match placeholders to users domain

* Drop migration

* Wip new onboarding intent

* WIP flow for self-hosted. Same service call just split logic

* WIP

* Add TODO

* Use onboarding user type instead of trpc session

* WIP

* WIP

* pass role and team name from onboarding to save in schema

* Add test to ensure role + name + team are persisted into onboarding table

* migrate roles to enum values

* Update ENUM

* Fix type error

* Redirect if flag is disabled

* Remove web

* WIP

* WIP

* Fix migration

* Fix calls

* User onboarding User types instead of trpc session

* Fix factory tests

* Fix flow for self hoste

* Type error

* More type fixes

* Fix handler tests

* Fix enum return type being different

* Use consistant types across the oganization stuff

* Fix

* Use TEAM_BILLING for e2e test

* Refactor is not company email and add tests

* Fix

* Fix

* Refactor flow to submit after form complete

* Fix flow with billing disabled

* Fix tests

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Rename and move test files

* WIP

* Fix types

* Update repo paths + tests

* Move to service folder

* Fix tests

* Fix types

* Remove old test files

* Restore lock

* Fix path

* Fix tests with new paths and factory logic

* Fix updaetdAt

* WIP onboardingID isolation

* Fix e2e test

* verify test

* Code rabbit

* Rename SelfHostedOnboardongService -> SelfHostedOrganizationOnboardingService

* Fix stores

* Fix type error

* Fix types

* remove tsignore

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* NITS

* Add the logic to auto complete admin org when billing enabled

* Fix store being weird

* We need to return the parsed value

* fixes

* sync from db always

* Add onboardingSgtore tests

* fix test

* remove step and status

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <[email protected]>
2025-10-18 14:13:30 +00:00

117 lines
4.4 KiB
TypeScript

import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { OrganizationOnboardingFactory } from "@calcom/ee/organizations/lib/service/onboarding/OrganizationOnboardingFactory";
import {
assertCanCreateOrg,
findUserToBeOrgOwner,
} from "@calcom/features/ee/organizations/lib/server/orgCreationUtils";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
import { prisma } from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TIntentToCreateOrgInputSchema } from "./intentToCreateOrg.schema";
const log = logger.getSubLogger({ prefix: ["intentToCreateOrgHandler"] });
type CreateOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TIntentToCreateOrgInputSchema;
};
export const intentToCreateOrgHandler = async ({ input, ctx }: CreateOptions) => {
const { slug, name, orgOwnerEmail, isPlatform } = input;
log.debug(
"Starting organization creation intent",
safeStringify({ slug, name, orgOwnerEmail, isPlatform })
);
if (IS_SELF_HOSTED) {
const deploymentRepo = new DeploymentRepository(prisma);
const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo);
const hasValidLicense = await licenseKeyService.checkLicense();
if (!hasValidLicense) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "License is not valid",
});
}
}
const loggedInUser = ctx.user;
if (!loggedInUser) throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." });
const IS_USER_ADMIN = loggedInUser.role === UserPermissionRole.ADMIN;
log.debug("User authorization check", safeStringify({ userId: loggedInUser.id, isAdmin: IS_USER_ADMIN }));
if (!IS_USER_ADMIN && loggedInUser.email !== orgOwnerEmail && !isPlatform) {
log.warn(
"Unauthorized organization creation attempt",
safeStringify({ loggedInUserEmail: loggedInUser.email, orgOwnerEmail })
);
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only create organization where you are the owner",
});
}
const orgOwner = await findUserToBeOrgOwner(orgOwnerEmail);
if (!orgOwner) {
// The flow exists to create the user through the stripe webhook invoice.paid but there could be a possible security issue with the approach. So, we avoid it currently.
// Issue: As the onboarding link(which has onboardingId) could be used by unwanted person to pay and then invite some unwanted members to the organization.
throw new TRPCError({
code: "BAD_REQUEST",
message: `No user found with email ${orgOwnerEmail}`,
});
}
log.debug("Found organization owner", safeStringify({ orgOwnerId: orgOwner.id, email: orgOwner.email }));
const organizationOnboarding = await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
// If onboarding exists and is incomplete, this is a resume flow (e.g., admin handover)
// Allow proceeding with the existing onboarding record
if (organizationOnboarding) {
if (organizationOnboarding.isComplete) {
// Organization already created - shouldn't create another one
throw new Error("organization_onboarding_already_exists");
}
// Incomplete onboarding exists - this is expected for resume/handover flows
log.debug(
"Found incomplete onboarding record - proceeding with resume flow",
safeStringify({ onboardingId: organizationOnboarding.id, slug })
);
// Use existing onboarding ID for the resume flow
input.onboardingId = organizationOnboarding.id;
}
await assertCanCreateOrg({
slug,
isPlatform,
orgOwner,
restrictBasedOnMinimumPublishedTeams: !IS_USER_ADMIN,
});
const onboardingService = OrganizationOnboardingFactory.create({
id: ctx.user.id,
email: ctx.user.email,
role: ctx.user.role,
});
const result = await onboardingService.createOnboardingIntent(input);
log.debug("Organization creation intent successful", safeStringify({ slug, orgOwnerId: orgOwner.id }));
return result;
};
export default intentToCreateOrgHandler;