mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +00:00
* 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]>
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
import { UserPermissionRole } from "@calcom/kysely/types";
|
|
import { ORGANIZATION_SELF_SERVE_MIN_SEATS, ORGANIZATION_SELF_SERVE_PRICE } from "@calcom/lib/constants";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
|
import { prisma } from "@calcom/prisma";
|
|
import { MembershipRole } from "@calcom/prisma/enums";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import type { OnboardingUser } from "./service/onboarding/types";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["ee", "organizations", "OrganizationPermissionService"] });
|
|
type SeatsPrice = {
|
|
seats?: number | null;
|
|
pricePerSeat?: number | null;
|
|
};
|
|
|
|
export interface validatePermissionsIOrganizationPermissionService {
|
|
hasPermissionToCreateForEmail(targetEmail: string): Promise<boolean>;
|
|
hasPendingOrganizations(email: string, slug?: string): Promise<boolean>;
|
|
hasPermissionToModifyDefaultPayment(): boolean;
|
|
hasPermissionToMigrateTeams(teamIds: number[]): Promise<boolean>;
|
|
hasModifiedDefaultPayment(
|
|
input: {
|
|
billingPeriod?: string;
|
|
} & SeatsPrice
|
|
): boolean;
|
|
}
|
|
|
|
export class OrganizationPermissionService {
|
|
constructor(private readonly user: OnboardingUser) {}
|
|
|
|
async hasPermissionToCreateForEmail(targetEmail: string): Promise<boolean> {
|
|
return this.user.email === targetEmail || this.user.role === "ADMIN";
|
|
}
|
|
|
|
/**
|
|
* If an onboarding is complete then it also means that org is created already.
|
|
*/
|
|
async hasConflictingOrganization({ slug }: { slug: string }): Promise<boolean> {
|
|
return !!(await OrganizationRepository.findBySlug({ slug }));
|
|
}
|
|
|
|
async hasCompletedOnboarding(email: string): Promise<boolean> {
|
|
const orgOnboarding = await prisma.organizationOnboarding.findUnique({
|
|
where: {
|
|
orgOwnerEmail: email,
|
|
},
|
|
});
|
|
|
|
return !!(orgOnboarding && orgOnboarding.isComplete);
|
|
}
|
|
|
|
hasPermissionToModifyDefaultPayment(): boolean {
|
|
return this.user.role === UserPermissionRole.ADMIN;
|
|
}
|
|
|
|
hasModifiedDefaultPayment(data: SeatsPrice & { billingPeriod?: string }): boolean {
|
|
const isBillingPeriodModified =
|
|
data.billingPeriod !== undefined && data.billingPeriod !== null && data.billingPeriod !== "MONTHLY";
|
|
|
|
const isSeatsModified =
|
|
data.seats !== undefined && data.seats !== null && data.seats !== ORGANIZATION_SELF_SERVE_MIN_SEATS;
|
|
|
|
const isPricePerSeatModified =
|
|
data.pricePerSeat !== undefined &&
|
|
data.pricePerSeat !== null &&
|
|
data.pricePerSeat !== ORGANIZATION_SELF_SERVE_PRICE;
|
|
|
|
log.debug(
|
|
"hasModifiedDefaultPayment",
|
|
safeStringify({ isBillingPeriodModified, isSeatsModified, isPricePerSeatModified })
|
|
);
|
|
return isBillingPeriodModified || isSeatsModified || isPricePerSeatModified;
|
|
}
|
|
|
|
async hasPermissionToMigrateTeams(teamIds: number[]): Promise<boolean> {
|
|
if (teamIds.length === 0) return true;
|
|
const teamMemberships = await prisma.membership.findMany({
|
|
where: {
|
|
userId: this.user.id,
|
|
teamId: {
|
|
in: teamIds,
|
|
},
|
|
role: {
|
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
|
},
|
|
},
|
|
});
|
|
|
|
return teamMemberships.length === teamIds.length;
|
|
}
|
|
|
|
/**
|
|
* Doesn't validate custom price permission as that is validated before onboarding is created and this method also runs when checkout is to be started
|
|
*/
|
|
async validatePermissions(
|
|
input: {
|
|
orgOwnerEmail: string;
|
|
teams?: { id: number; isBeingMigrated: boolean }[];
|
|
billingPeriod?: string;
|
|
slug: string;
|
|
} & SeatsPrice
|
|
): Promise<boolean> {
|
|
if (!(await this.hasPermissionToCreateForEmail(input.orgOwnerEmail))) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "you_do_not_have_permission_to_create_an_organization_for_this_email",
|
|
});
|
|
}
|
|
|
|
if (await this.hasConflictingOrganization({ slug: input.slug })) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "organization_already_exists_with_this_slug",
|
|
});
|
|
}
|
|
|
|
if (await this.hasCompletedOnboarding(input.orgOwnerEmail)) {
|
|
// TODO: Consider redirecting to success page
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "organization_onboarding_already_completed",
|
|
});
|
|
}
|
|
|
|
const teamsToMigrate = input.teams
|
|
?.filter((team) => team.id > 0 && team.isBeingMigrated)
|
|
.map((team) => team.id);
|
|
|
|
if (teamsToMigrate && !(await this.hasPermissionToMigrateTeams(teamsToMigrate))) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "you_do_not_have_permission_to_migrate_one_or_more_of_the_teams",
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|