fix: Organization Migration - Prevent the redirected username from being claimed by others. (#13675)
* Check for migrated username * Add e2e for reserved username due to migration * Add username change test * Update username.ts * fixed type checks * Refactor to use isOrganization * Typing * Typing * Removed the isOrganization function --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
Keith Williams
parent
d8cee20d0c
commit
a677d0d2c4
@@ -1,6 +1,5 @@
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -48,7 +47,7 @@ export async function moveUserToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
if (!isOrganization({ team })) {
|
||||
if (!team.isOrganization) {
|
||||
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ export async function moveTeamToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
|
||||
|
||||
if (!isOrganization({ team: possibleOrg })) {
|
||||
if (!possibleOrg.isOrganization) {
|
||||
throw new Error(`${targetOrg.id} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -667,7 +666,7 @@ async function moveTeamsWithoutMembersToOrg({
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !isOrganization({ team }));
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
|
||||
|
||||
@@ -832,7 +831,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !isOrganization({ team }));
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { expect } from "@playwright/test";
|
||||
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { moveUserToOrg } from "@lib/orgMigration";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
@@ -110,4 +113,54 @@ test.describe("Change username on settings", () => {
|
||||
|
||||
await expect(page).toHaveURL(/.*checkout.stripe.com/);
|
||||
});
|
||||
|
||||
test("User can't take a username that has been migrated to a different username in an organization", async ({
|
||||
users,
|
||||
orgs,
|
||||
page,
|
||||
}) => {
|
||||
const existingUser =
|
||||
await test.step("Migrate user to a different username in an organization", async () => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
|
||||
const existingUser = await users.create({
|
||||
username: "john",
|
||||
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
name: "John Outside Organization",
|
||||
});
|
||||
|
||||
await moveUserToOrg({
|
||||
user: existingUser,
|
||||
targetOrg: {
|
||||
// Changed username. After this there is no user with username equal to {existingUser.username}
|
||||
username: `${existingUser.username}-org`,
|
||||
id: org.id,
|
||||
membership: {
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
shouldMoveTeams: false,
|
||||
});
|
||||
return existingUser;
|
||||
});
|
||||
|
||||
await test.step("Changing username for another user to the previous username of migrated user - shouldn't be allowed", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const previousUsername = existingUser.username!;
|
||||
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const usernameInput = page.locator("[data-testid=username-input]");
|
||||
|
||||
await usernameInput.fill(previousUsername);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator("[data-testid=update-username-btn]").nth(0)).toBeHidden();
|
||||
await expect(page.locator("[data-testid=update-username-btn]").nth(1)).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -604,15 +604,33 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
return membership;
|
||||
},
|
||||
getOrgMembership: async () => {
|
||||
return prisma.membership.findFirstOrThrow({
|
||||
const membership = await prisma.membership.findFirstOrThrow({
|
||||
where: {
|
||||
userId: user.id,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
include: { team: { include: { children: true } } },
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
children: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!membership) {
|
||||
return membership;
|
||||
}
|
||||
|
||||
return {
|
||||
...membership,
|
||||
team: {
|
||||
...membership.team,
|
||||
metadata: teamMetadataSchema.parse(membership.team.metadata),
|
||||
},
|
||||
};
|
||||
},
|
||||
getFirstEventAsOwner: async () =>
|
||||
prisma.eventType.findFirstOrThrow({
|
||||
@@ -670,6 +688,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
roleInOrganization?: MembershipRole;
|
||||
schedule?: Schedule;
|
||||
password?: string | null;
|
||||
emailDomain?: string;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
@@ -687,10 +706,11 @@ const createUser = (
|
||||
? opts.username
|
||||
: `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
|
||||
const emailDomain = opts?.emailDomain || "example.com";
|
||||
return {
|
||||
username: uname,
|
||||
name: opts?.name,
|
||||
email: opts?.email ?? `${uname}@example.com`,
|
||||
email: opts?.email ?? `${uname}@${emailDomain}`,
|
||||
password: {
|
||||
create: {
|
||||
hash: hashPassword(uname),
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { Browser, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { moveUserToOrg } from "@lib/orgMigration";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { getInviteLink } from "../lib/testUtils";
|
||||
@@ -16,7 +19,7 @@ test.afterEach(async ({ users, orgs }) => {
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test.describe("Email not matching orgAutoAcceptEmail", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
test("nonexisting user invited to an organization", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
@@ -83,7 +86,17 @@ test.describe("Organization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Team invitation", async ({ browser, page, users, emails }) => {
|
||||
// This test is already covered by booking.e2e.ts where existing user is invited and his booking links are tested.
|
||||
// We can re-test here when we want to test some more scenarios.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
test("existing user invited to an organization", () => {});
|
||||
|
||||
test("nonexisting user invited to a Team inside organization", async ({
|
||||
browser,
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true });
|
||||
await orgOwner.apiLogin();
|
||||
const { team } = await orgOwner.getFirstTeamMembership();
|
||||
@@ -183,7 +196,7 @@ test.describe("Organization", () => {
|
||||
});
|
||||
|
||||
test.describe("Email matching orgAutoAcceptEmail and a Verified Organization", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
test("nonexisting user is invited to Org", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
@@ -248,7 +261,52 @@ test.describe("Organization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Team Invitation", async ({ browser, page, users, emails }) => {
|
||||
// Such a user has user.username changed directly in addition to having the new username in the profile.username
|
||||
test("existing user migrated to an organization", async ({ users, page, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
const { existingUser } = await test.step("Invite an existing user to an organization", async () => {
|
||||
const existingUser = await users.create({
|
||||
username: "john",
|
||||
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
name: "John Outside Organization",
|
||||
});
|
||||
|
||||
await moveUserToOrg({
|
||||
user: existingUser,
|
||||
targetOrg: {
|
||||
username: `${existingUser.username}-org`,
|
||||
id: org.id,
|
||||
membership: {
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
shouldMoveTeams: false,
|
||||
});
|
||||
return { existingUser };
|
||||
});
|
||||
|
||||
await test.step("Signing up with the previous username of the migrated user - shouldn't be allowed", async () => {
|
||||
await page.goto("/signup");
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await page.locator('input[name="username"]').fill(existingUser.username!);
|
||||
await page
|
||||
.locator('input[name="email"]')
|
||||
.fill(`${existingUser.username}-differnet-email@example.com`);
|
||||
await page.locator('input[name="password"]').fill("Password99!");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('button[type="submit"]')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test("nonexisting user is invited to a team inside organization", async ({
|
||||
browser,
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/cr
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { usernameHandler, type RequestWithUsernameStatus } from "@calcom/lib/server/username";
|
||||
import { createWebUser as syncServicesCreateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
@@ -23,6 +24,8 @@ import {
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "../utils/token";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
|
||||
|
||||
async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
const {
|
||||
email: _email,
|
||||
@@ -35,6 +38,9 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
token: true,
|
||||
})
|
||||
.parse(req.body);
|
||||
|
||||
log.debug("handler", { email: _email });
|
||||
|
||||
let username: string | null = req.usernameStatus.requestedUserName;
|
||||
let checkoutSessionId: string | null = null;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { isUsernameReservedDueToMigration } from "@calcom/lib/server/username";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
|
||||
@@ -78,7 +79,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
organizationSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const isInviteForATeamInOrganization = !!team.parent;
|
||||
const isCheckingUsernameInGlobalNamespace = !team.isOrganization && !isInviteForATeamInOrganization;
|
||||
|
||||
if (isCheckingUsernameInGlobalNamespace) {
|
||||
const isUsernameAvailable = !(await isUsernameReservedDueToMigration(correctedUsername));
|
||||
if (!isUsernameAvailable) {
|
||||
res.status(409).json({ message: "A user exists with that username" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
@@ -123,6 +136,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const isUsernameAvailable = !(await isUsernameReservedDueToMigration(correctedUsername));
|
||||
if (!isUsernameAvailable) {
|
||||
res.status(409).json({ message: "A user exists with that username" });
|
||||
return;
|
||||
}
|
||||
if (IS_PREMIUM_USERNAME_ENABLED) {
|
||||
const checkUsername = await checkPremiumUsername(correctedUsername);
|
||||
if (checkUsername.premium) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSetti
|
||||
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -77,7 +76,7 @@ export default function TeamListItem(props: Props) {
|
||||
utils.viewer.teams.listInvites.invalidate();
|
||||
const userOrganizationId = user?.profile?.organization?.id;
|
||||
const isSubTeamOfDifferentOrg = team.parentId ? team.parentId != userOrganizationId : false;
|
||||
const isDifferentOrg = isOrganization({ team }) && team.id !== userOrganizationId;
|
||||
const isDifferentOrg = team.isOrganization && team.id !== userOrganizationId;
|
||||
// If the user team being accepted is a sub-team of different organization or the different organization itself then page must be reloaded to let the session change reflect reliably everywhere.
|
||||
if (variables.accept && (isSubTeamOfDifferentOrg || isDifferentOrg)) {
|
||||
window.location.reload();
|
||||
@@ -101,7 +100,7 @@ export default function TeamListItem(props: Props) {
|
||||
const { hideDropdown, setHideDropdown } = props;
|
||||
|
||||
if (!team) return <></>;
|
||||
const teamUrl = isOrganization({ team })
|
||||
const teamUrl = team.isOrganization
|
||||
? getTeamUrlSync({ orgSlug: team.slug, teamSlug: null })
|
||||
: getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug });
|
||||
const teamInfo = (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -46,14 +45,11 @@ export function TeamsListing() {
|
||||
},
|
||||
});
|
||||
|
||||
const teams = useMemo(() => data?.filter((m) => m.accepted && !isOrganization({ team: m })) || [], [data]);
|
||||
const teams = useMemo(() => data?.filter((m) => m.accepted && !m.isOrganization) || [], [data]);
|
||||
|
||||
const teamInvites = useMemo(
|
||||
() => data?.filter((m) => !m.accepted && !isOrganization({ team: m })) || [],
|
||||
[data]
|
||||
);
|
||||
const teamInvites = useMemo(() => data?.filter((m) => !m.accepted && !m.isOrganization) || [], [data]);
|
||||
|
||||
const organizationInvites = (data?.filter((m) => !m.accepted && isOrganization({ team: m })) || []).filter(
|
||||
const organizationInvites = (data?.filter((m) => !m.accepted && m.isOrganization) || []).filter(
|
||||
(orgInvite) => {
|
||||
const isThereASubTeamOfTheOrganizationInInvites = teamInvites.find(
|
||||
(teamInvite) => teamInvite.parentId === orgInvite.id
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/l
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ORGANIZATION_MIN_SEATS } from "@calcom/lib/constants";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -157,7 +156,7 @@ export const updateQuantitySubscriptionFromStripe = async (teamId: number) => {
|
||||
)?.quantity;
|
||||
if (!subscriptionQuantity) throw new Error("Subscription not found");
|
||||
|
||||
if (!!isOrganization({ team }) && membershipCount < ORGANIZATION_MIN_SEATS) {
|
||||
if (team.isOrganization && membershipCount < ORGANIZATION_MIN_SEATS) {
|
||||
console.info(
|
||||
`Org ${teamId} has less members than the min ${ORGANIZATION_MIN_SEATS}, skipping updating subscription.`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Membership, Team } from "@calcom/prisma/client";
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
export const enum ENTITY_PERMISSION_LEVEL {
|
||||
@@ -19,10 +19,6 @@ export function canEditEntity(
|
||||
);
|
||||
}
|
||||
|
||||
export function isOrganization({ team }: { team: Pick<Team, "isOrganization"> }) {
|
||||
return team.isOrganization;
|
||||
}
|
||||
|
||||
export function getEntityPermissionLevel(
|
||||
entity: {
|
||||
userId: number | null;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
import { ProfileRepository } from "./repository/profile";
|
||||
import { isUsernameReservedDueToMigration } from "./username";
|
||||
|
||||
export async function checkRegularUsername(_username: string, currentOrgDomain?: string | null) {
|
||||
const isCheckingUsernameInGlobalNamespace = !currentOrgDomain;
|
||||
const username = slugify(_username);
|
||||
|
||||
const premium = !!process.env.NEXT_PUBLIC_IS_E2E && username.length < 5;
|
||||
@@ -23,8 +25,13 @@ export async function checkRegularUsername(_username: string, currentOrgDomain?:
|
||||
message: "A user exists with that username",
|
||||
};
|
||||
}
|
||||
|
||||
const isUsernameAvailable = isCheckingUsernameInGlobalNamespace
|
||||
? !(await isUsernameReservedDueToMigration(username))
|
||||
: true;
|
||||
|
||||
return {
|
||||
available: true as const,
|
||||
available: isUsernameAvailable,
|
||||
premium,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,4 +3,6 @@ import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||
import { checkRegularUsername } from "./checkRegularUsername";
|
||||
import { usernameCheck as checkPremiumUsername } from "./username";
|
||||
|
||||
// TODO: Replace `lib/checkPremiumUsername` with `usernameCheck` and then import checkPremiumUsername directly here.
|
||||
// We want to remove dependency on website for signup stuff as signup is now part of app.
|
||||
export const checkUsername = !IS_PREMIUM_USERNAME_ENABLED ? checkRegularUsername : checkPremiumUsername;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Prisma } from "@calcom/prisma/client";
|
||||
import type { User as UserType } from "@calcom/prisma/client";
|
||||
import type { UpId, UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { isOrganization } from "../../entityPermissionUtils";
|
||||
import logger from "../../logger";
|
||||
import { safeStringify } from "../../safeStringify";
|
||||
import { ProfileRepository } from "./profile";
|
||||
@@ -92,8 +91,8 @@ export class UserRepository {
|
||||
userId,
|
||||
});
|
||||
|
||||
const acceptedOrgMemberships = acceptedTeamMemberships.filter((membership) =>
|
||||
isOrganization({ team: membership.team })
|
||||
const acceptedOrgMemberships = acceptedTeamMemberships.filter(
|
||||
(membership) => membership.team.isOrganization
|
||||
);
|
||||
|
||||
const organizations = acceptedOrgMemberships.map((membership) => membership.team);
|
||||
|
||||
@@ -2,10 +2,13 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/enums";
|
||||
|
||||
import { IS_PREMIUM_USERNAME_ENABLED } from "../constants";
|
||||
import logger from "../logger";
|
||||
import notEmpty from "../notEmpty";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["server/username"] });
|
||||
const cachedData: Set<string> = new Set();
|
||||
|
||||
export type RequestWithUsernameStatus = NextApiRequest & {
|
||||
@@ -106,7 +109,9 @@ const usernameHandler =
|
||||
return handler(req, res);
|
||||
};
|
||||
|
||||
const usernameCheck = async (usernameRaw: string) => {
|
||||
const usernameCheck = async (usernameRaw: string, currentOrgDomain?: string | null) => {
|
||||
log.debug("usernameCheck", { usernameRaw, currentOrgDomain });
|
||||
const isCheckingUsernameInGlobalNamespace = !currentOrgDomain;
|
||||
const response = {
|
||||
available: true,
|
||||
premium: false,
|
||||
@@ -129,6 +134,10 @@ const usernameCheck = async (usernameRaw: string) => {
|
||||
|
||||
if (user) {
|
||||
response.available = false;
|
||||
} else {
|
||||
response.available = isCheckingUsernameInGlobalNamespace
|
||||
? !(await isUsernameReservedDueToMigration(username))
|
||||
: true;
|
||||
}
|
||||
|
||||
if (await isPremiumUserName(username)) {
|
||||
@@ -158,6 +167,20 @@ const usernameCheck = async (usernameRaw: string) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Should be used when in global namespace(i.e. outside of an organization)
|
||||
*/
|
||||
export const isUsernameReservedDueToMigration = async (username: string) =>
|
||||
!!(await prisma.tempOrgRedirect.findUnique({
|
||||
where: {
|
||||
from_type_fromOrgId: {
|
||||
type: RedirectType.User,
|
||||
from: username,
|
||||
fromOrgId: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* It is a bit different from usernameCheck because it also check if the user signing up is the same user that has a pending invitation to organization
|
||||
* So, it uses email to uniquely identify the user and then also checks if the username requested by that user is available for taking or not.
|
||||
@@ -207,12 +230,13 @@ const usernameCheckForSignup = async ({
|
||||
const isClaimingAlreadySetUsername = user.username === username;
|
||||
const isClaimingUnsetUsername = !user.username;
|
||||
response.available = isClaimingUnsetUsername || isClaimingAlreadySetUsername;
|
||||
// There are no premium users outside an organization only
|
||||
// There are premium users outside an organization only
|
||||
response.premium = await isPremiumUserName(username);
|
||||
}
|
||||
// If user isn't found, it's a direct signup and that can't be of an organization
|
||||
} else {
|
||||
// If user isn't found, it's a new signup and that can't be of an organization
|
||||
response.premium = await isPremiumUserName(username);
|
||||
response.available = !(await isUsernameReservedDueToMigration(username));
|
||||
}
|
||||
|
||||
// get list of similar usernames in the db
|
||||
|
||||
@@ -3,7 +3,6 @@ import { orderBy } from "lodash";
|
||||
|
||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { getOrgAvatarUrl, getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
|
||||
@@ -214,7 +213,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
||||
await Promise.all(
|
||||
memberships
|
||||
.filter((mmship) => {
|
||||
if (isOrganization({ team: mmship.team })) {
|
||||
if (mmship.team.isOrganization) {
|
||||
return false;
|
||||
} else {
|
||||
if (!input?.filters || !hasFilter(input?.filters)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -45,9 +44,8 @@ export const acceptOrLeaveHandler = async ({ ctx, input }: AcceptOrLeaveOptions)
|
||||
});
|
||||
}
|
||||
|
||||
const isAnOrganization = isOrganization({ team });
|
||||
const isASubteam = team.parentId !== null;
|
||||
const idOfOrganizationInContext = isAnOrganization ? team.id : isASubteam ? team.parentId : null;
|
||||
const idOfOrganizationInContext = team.isOrganization ? team.id : isASubteam ? team.parentId : null;
|
||||
const needProfileUpdate = !!idOfOrganizationInContext;
|
||||
if (needProfileUpdate) {
|
||||
await createAProfileForAnExistingUser({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
@@ -174,7 +173,7 @@ async function handleExistingUsersInvites({
|
||||
}
|
||||
|
||||
const translation = await getTranslation(input.language ?? "en", "common");
|
||||
if (!isOrganization({ team })) {
|
||||
if (!team.isOrganization) {
|
||||
const [autoJoinUsers, regularUsers] = groupUsersByJoinability({
|
||||
existingUsersWithMembersips,
|
||||
team,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { TFunction } from "next-i18next";
|
||||
|
||||
import { sendTeamInviteEmail } from "@calcom/emails";
|
||||
import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries";
|
||||
@@ -452,7 +451,7 @@ export function getAutoJoinStatus({
|
||||
invitee: UserWithMembership;
|
||||
connectionInfoMap: Record<string, ReturnType<typeof getOrgConnectionInfo>>;
|
||||
}) {
|
||||
const isRegularTeam = !isOrganization({ team }) && !team.parentId;
|
||||
const isRegularTeam = !team.isOrganization && !team.parentId;
|
||||
|
||||
if (isRegularTeam) {
|
||||
// There are no-auto join in regular teams ever
|
||||
|
||||
Reference in New Issue
Block a user