Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should read the orgSlug here from the correct place and pass that on.

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 @@
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,13 +97,13 @@
setUsernameTaken(false);
return;
}
fetchUsername(debouncedUsername).then(({ data }) => {
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
setPremium(data.premium);
setUsernameTaken(!data.available);
});
}
checkUsername();
}, [debouncedUsername, setPremium, setUsernameTaken, formState.isSubmitting, formState.isSubmitSuccessful]);

Check warning on line 106 in apps/web/pages/signup.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/pages/signup.tsx#L106

[react-hooks/exhaustive-deps] React Hook useEffect has a missing dependency: 'orgSlug'. Either include it or remove the dependency array.

return (
<div>
Expand Down Expand Up @@ -270,6 +272,7 @@
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
Expand Down Expand Up @@ -604,23 +607,25 @@
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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better naming

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 @@ -632,7 +637,14 @@
prepopulateFormValues: !isOrgInviteByLink
? {
email: verificationToken.identifier,
username: slugify(username),
username: isOrganizationOrATeamInOrganization
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fixes the prefilling issue.

? 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
@@ -1,4 +1,4 @@
import type { Browser, Page } from "@playwright/test";

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Test timeout of 60000ms exceeded.

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Pending operations: - locator.click at apps/web/playwright/organization/organization-invitation.e2e.ts:406:51

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Test timeout of 60000ms exceeded.

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Pending operations: - locator.click at apps/web/playwright/organization/organization-invitation.e2e.ts:406:51

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Test timeout of 60000ms exceeded.

Check failure on line 1 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Pending operations: - locator.click at apps/web/playwright/organization/organization-invitation.e2e.ts:406:51
import { expect } from "@playwright/test";

import prisma from "@calcom/prisma";
Expand Down Expand Up @@ -45,7 +45,12 @@
});

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 @@

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 @@
});

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 @@

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,18 +376,34 @@
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!`);
await signupPage.locator("button[type=submit]").click();

Check failure on line 406 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation locator.click: Target closed =========================== logs =========================== waiting for locator('button[type=submit]') locator resolved to <button disabled type="submit" class="whitespace-nowrap …>Create Account</button> attempting click action waiting for element to be visible, enabled and stable element is not enabled - waiting... ============================================================ 404 | // Check required fields 405 | await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`); > 406 | await signupPage.locator("button[type=submit]").click(); | ^ 407 | await signupPage.waitForURL("/getting-started?from=signup"); 408 | await context.close(); 409 | await signupPage.close(); at signupFromEmailInviteLink (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:406:51) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:213:9 at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:192:7

Check failure on line 406 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── locator.click: Target closed =========================== logs =========================== waiting for locator('button[type=submit]') locator resolved to <button disabled type="submit" class="whitespace-nowrap …>Create Account</button> attempting click action waiting for element to be visible, enabled and stable element is not enabled - waiting... ============================================================ 404 | // Check required fields 405 | await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`); > 406 | await signupPage.locator("button[type=submit]").click(); | ^ 407 | await signupPage.waitForURL("/getting-started?from=signup"); 408 | await context.close(); 409 | await signupPage.close(); at signupFromEmailInviteLink (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:406:51) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:213:9 at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:192:7

Check failure on line 406 in apps/web/playwright/organization/organization-invitation.e2e.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (7/10)

[@calcom/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation

1) [@***com/web] › apps/web/playwright/organization/organization-invitation.e2e.ts:185:9 › Organization › Email matching orgAutoAcceptEmail and a Verified Organization › Org Invitation Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── locator.click: Target closed =========================== logs =========================== waiting for locator('button[type=submit]') locator resolved to <button disabled type="submit" class="whitespace-nowrap …>Create Account</button> attempting click action waiting for element to be visible, enabled and stable element is not enabled - waiting... ============================================================ 404 | // Check required fields 405 | await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`); > 406 | await signupPage.locator("button[type=submit]").click(); | ^ 407 | await signupPage.waitForURL("/getting-started?from=signup"); 408 | await context.close(); 409 | await signupPage.close(); at signupFromEmailInviteLink (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:406:51) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:213:9 at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-invitation.e2e.ts:192:7
await signupPage.waitForURL("/getting-started?from=signup");
await context.close();
await signupPage.close();
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
Loading