fix: Correctly prefill username in case of non-org email invite in an org (#12854)

* fix: Correctly prefill username in case of non-org email invite in an org

* Update test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Hariom Balhara 2023-12-19 08:14:48 +05:30 committed by GitHub
parent f77e480d02
commit 4b16860d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 73 additions and 15 deletions

View File

@ -74,7 +74,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const debouncedApiCall = useMemo(
() =>
debounce(async (username: string) => {
const { data } = await fetchUsername(username);
// TODO: Support orgSlug
const { data } = await fetchUsername(username, null);
setMarkAsError(!data.available && !!currentUsername && username !== currentUsername);
setIsInputUsernamePremium(data.premium);
setUsernameIsAvailable(data.available);

View File

@ -44,7 +44,8 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
const debouncedApiCall = useMemo(
() =>
debounce(async (username) => {
const { data } = await fetchUsername(username);
// TODO: Support orgSlug
const { data } = await fetchUsername(username, null);
setMarkAsError(!data.available);
setUsernameIsAvailable(data.available);
}, 150),

View File

@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { checkUsername } from "@calcom/lib/server/checkUsername";
@ -8,8 +9,14 @@ type Response = {
premium: boolean;
};
const bodySchema = z.object({
username: z.string(),
orgSlug: z.string().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
const { currentOrgDomain } = orgDomainConfig(req);
const result = await checkUsername(req.body.username, currentOrgDomain);
const { username, orgSlug } = bodySchema.parse(req.body);
const result = await checkUsername(username, currentOrgDomain || orgSlug);
return res.status(200).json(result);
}

View File

@ -73,6 +73,7 @@ function UsernameField({
setPremium,
premium,
setUsernameTaken,
orgSlug,
usernameTaken,
...props
}: React.ComponentProps<typeof TextField> & {
@ -80,6 +81,7 @@ function UsernameField({
setPremium: (value: boolean) => void;
premium: boolean;
usernameTaken: boolean;
orgSlug?: string;
setUsernameTaken: (value: boolean) => void;
}) {
const { t } = useLocale();
@ -95,7 +97,7 @@ function UsernameField({
setUsernameTaken(false);
return;
}
fetchUsername(debouncedUsername).then(({ data }) => {
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
setPremium(data.premium);
setUsernameTaken(!data.available);
});
@ -276,6 +278,7 @@ export default function Signup({
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
@ -610,15 +613,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
};
const isATeamInOrganization = tokenTeam?.parentId !== null;
const isOrganization = tokenTeam.metadata?.isOrganization;
// Detect if the team is an org by either the metadata flag or if it has a parent team
const isOrganization = tokenTeam.metadata?.isOrganization || tokenTeam?.parentId !== null;
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
// If we are dealing with an org, the slug may come from the team itself or its parent
const orgSlug = isOrganization
const orgSlug = isOrganizationOrATeamInOrganization
? tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug || tokenTeam.slug
: null;
// Org context shouldn't check if a username is premium
if (!IS_SELF_HOSTED && !isOrganization) {
if (!IS_SELF_HOSTED && !isOrganizationOrATeamInOrganization) {
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
const { available, suggestion } = await checkPremiumUsername(username);
@ -626,7 +631,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
}
const isValidEmail = checkValidEmail(verificationToken.identifier);
const isOrgInviteByLink = isOrganization && !isValidEmail;
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
: null;
@ -638,7 +643,14 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
prepopulateFormValues: !isOrgInviteByLink
? {
email: verificationToken.identifier,
username: slugify(username),
username: isOrganizationOrATeamInOrganization
? getOrgUsernameFromEmail(
verificationToken.identifier,
(isOrganization
? tokenTeam.metadata?.orgAutoAcceptEmail
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
)
: slugify(username),
}
: null,
orgSlug,

View File

@ -45,7 +45,12 @@ test.describe.serial("Organization", () => {
});
assertInviteLink(inviteLink);
await signupFromEmailInviteLink(browser, inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
@ -118,7 +123,12 @@ test.describe.serial("Organization", () => {
assertInviteLink(inviteLink);
await signupFromEmailInviteLink(browser, inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
@ -200,7 +210,12 @@ test.describe.serial("Organization", () => {
});
assertInviteLink(inviteLink);
await signupFromEmailInviteLink(browser, inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
@ -276,7 +291,12 @@ test.describe.serial("Organization", () => {
assertInviteLink(inviteLink);
await signupFromEmailInviteLink(browser, inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
@ -356,14 +376,30 @@ async function signupFromInviteLink({
return { email };
}
async function signupFromEmailInviteLink(browser: Browser, inviteLink: string) {
async function signupFromEmailInviteLink({
browser,
inviteLink,
expectedUsername,
expectedEmail,
}: {
browser: Browser;
inviteLink: string;
expectedUsername: string;
expectedEmail: string;
}) {
// Follow invite link in new window
const context = await browser.newContext();
const signupPage = await context.newPage();
signupPage.goto(inviteLink);
await signupPage.waitForLoadState("networkidle");
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toBeDisabled();
expect(await signupPage.locator(`[data-testid="signup-usernamefield"]`).inputValue()).toBe(
expectedUsername
);
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toBeDisabled();
expect(await signupPage.locator(`[data-testid="signup-emailfield"]`).inputValue()).toBe(expectedEmail);
await signupPage.waitForLoadState("networkidle");
// Check required fields
await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`);

View File

@ -5,12 +5,13 @@ type ResponseUsernameApi = {
suggestion?: string;
};
export async function fetchUsername(username: string) {
export async function fetchUsername(username: string, orgSlug: string | null) {
const response = await fetch("/api/username", {
credentials: "include",
method: "POST",
body: JSON.stringify({
username: username.trim(),
orgSlug: orgSlug ?? undefined,
}),
headers: {
"Content-Type": "application/json",