Skip to content

Commit

Permalink
fix: Correctly prefill username in case of non-org email invite in an…
Browse files Browse the repository at this point in the history
… org (calcom#12854)

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

* Update test

---------

Co-authored-by: Udit Takkar <[email protected]>
  • Loading branch information
hariombalhara and Udit-takkar authored Dec 19, 2023
1 parent f77e480 commit 4b16860
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
9 changes: 8 additions & 1 deletion apps/web/pages/api/username.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
}
24 changes: 18 additions & 6 deletions apps/web/pages/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ function UsernameField({
setPremium,
premium,
setUsernameTaken,
orgSlug,
usernameTaken,
...props
}: React.ComponentProps<typeof TextField> & {
username: string;
setPremium: (value: boolean) => void;
premium: boolean;
usernameTaken: boolean;
orgSlug?: string;
setUsernameTaken: (value: boolean) => void;
}) {
const { t } = useLocale();
Expand All @@ -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);
});
Expand Down Expand Up @@ -276,6 +278,7 @@ export default function Signup({
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
Expand Down Expand Up @@ -610,23 +613,25 @@ 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);

username = available ? username : suggestion || username;
}

const isValidEmail = checkValidEmail(verificationToken.identifier);
const isOrgInviteByLink = isOrganization && !isValidEmail;
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
: null;
Expand All @@ -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,
Expand Down
46 changes: 41 additions & 5 deletions apps/web/playwright/organization/organization-invitation.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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!`);
Expand Down
3 changes: 2 additions & 1 deletion packages/lib/fetchUsername.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 4b16860

Please sign in to comment.