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:
parent
f77e480d02
commit
4b16860d07
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!`);
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue
Block a user