parent
e8e0d03265
commit
8995dcc82a
|
@ -51,7 +51,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const { data: session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
const {
|
||||
currentUsername,
|
||||
setCurrentUsername = noop,
|
||||
|
|
|
@ -25,7 +25,7 @@ interface ICustomUsernameProps {
|
|||
|
||||
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
|
||||
const { t } = useLocale();
|
||||
const { data: session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
|
||||
const {
|
||||
currentUsername,
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import { PremiumTextfield } from "./PremiumTextfield";
|
||||
import { UsernameTextfield } from "./UsernameTextfield";
|
||||
|
||||
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
||||
|
||||
interface UsernameAvailabilityFieldProps {
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
}
|
||||
|
||||
export const getUsernameAvailabilityComponent = (isPremium: boolean) => {
|
||||
if (isPremium)
|
||||
return dynamic(() => import("./PremiumTextfield").then((m) => m.PremiumTextfield), { ssr: false });
|
||||
return dynamic(() => import("./UsernameTextfield").then((m) => m.UsernameTextfield), { ssr: false });
|
||||
};
|
||||
|
||||
export const UsernameAvailabilityField = ({
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
|
@ -39,6 +40,7 @@ export const UsernameAvailabilityField = ({
|
|||
},
|
||||
});
|
||||
|
||||
const UsernameAvailability = getUsernameAvailabilityComponent(!IS_SELF_HOSTED && !user.organization?.id);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const usernamePrefix = orgBranding
|
||||
|
@ -59,6 +61,7 @@ export const UsernameAvailabilityField = ({
|
|||
setInputUsernameValue={onChange}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
disabled={!!user.organization?.id}
|
||||
addOnLeading={`${usernamePrefix}/`}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
|||
import { IS_CALCOM } from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg";
|
||||
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
@ -39,6 +39,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const username = slugify(data.username);
|
||||
const userEmail = email.toLowerCase();
|
||||
|
||||
const validationResponse = (
|
||||
incomingEmail: string,
|
||||
validation: { isValid: boolean; email: string | undefined }
|
||||
) => {
|
||||
const { isValid, email } = validation;
|
||||
if (!isValid) {
|
||||
const message: string =
|
||||
email !== incomingEmail ? "Username already taken" : "Email address is already registered";
|
||||
|
||||
return res.status(409).json({ message });
|
||||
}
|
||||
};
|
||||
|
||||
if (!username) {
|
||||
res.status(422).json({ message: "Invalid username" });
|
||||
return;
|
||||
|
@ -65,42 +78,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return res.status(401).json({ message: "Token expired" });
|
||||
}
|
||||
if (foundToken?.teamId) {
|
||||
const isValidUsername = await validateUsernameInOrg(username, foundToken?.teamId);
|
||||
|
||||
if (!isValidUsername) {
|
||||
return res.status(409).json({ message: "Username already taken" });
|
||||
}
|
||||
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
|
||||
return validationResponse(userEmail, teamUserValidation);
|
||||
}
|
||||
} else {
|
||||
// There is an existingUser if the username matches
|
||||
// OR if the email matches AND either the email is verified
|
||||
// or both username and password are set
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ username },
|
||||
{
|
||||
AND: [
|
||||
{ email: userEmail },
|
||||
{
|
||||
OR: [
|
||||
{ emailVerified: { not: null } },
|
||||
{
|
||||
AND: [{ password: { not: null } }, { username: { not: null } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
const message: string =
|
||||
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
|
||||
|
||||
return res.status(409).json({ message });
|
||||
}
|
||||
const userValidation = await validateUsername(username, userEmail);
|
||||
return validationResponse(userEmail, userValidation);
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
|
|
@ -14,6 +14,7 @@ import { useFlagMap } from "@calcom/features/flags/context/provider";
|
|||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
|
@ -130,10 +131,11 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
|||
<TextField
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? getOrgFullDomain(orgSlug, { protocol: false })
|
||||
? getOrgFullDomain(orgSlug, { protocol: true })
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
||||
}
|
||||
{...register("username")}
|
||||
disabled={!!orgSlug}
|
||||
required
|
||||
/>
|
||||
<EmailField
|
||||
|
@ -192,8 +194,6 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
};
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || flags["disable-signup"]) {
|
||||
console.log({ flag: flags["disable-signup"] });
|
||||
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
|
@ -210,6 +210,20 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
metadata: true,
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!verificationToken || verificationToken.expires < new Date()) {
|
||||
|
@ -249,41 +263,35 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
|
||||
let username = guessUsernameFromEmail(verificationToken.identifier);
|
||||
|
||||
const orgInfo = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: verificationToken?.identifier,
|
||||
},
|
||||
select: {
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const tokenTeam = {
|
||||
...verificationToken?.team,
|
||||
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
|
||||
};
|
||||
|
||||
const userOrgMetadata = teamMetadataSchema.parse(orgInfo?.organization?.metadata ?? {});
|
||||
// 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;
|
||||
// If we are dealing with an org, the slug may come from the team itself or its parent
|
||||
const orgSlug = isOrganization
|
||||
? tokenTeam.slug || tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug
|
||||
: null;
|
||||
|
||||
if (!IS_SELF_HOSTED) {
|
||||
// Org context shouldn't check if a username is premium
|
||||
if (!IS_SELF_HOSTED && !isOrganization) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Transform all + to - in username
|
||||
username = username.replace(/\+/g, "-");
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
token,
|
||||
prepopulateFormValues: {
|
||||
email: verificationToken.identifier,
|
||||
username,
|
||||
username: slugify(username),
|
||||
},
|
||||
orgSlug: (orgInfo?.organization?.slug || userOrgMetadata?.requestedSlug) ?? null,
|
||||
orgSlug,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ export function subdomainSuffix() {
|
|||
}
|
||||
|
||||
export function getOrgFullDomain(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
|
||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}/`;
|
||||
}
|
||||
|
||||
export function getSlugOrRequestedSlug(slug: string) {
|
||||
|
|
|
@ -7,7 +7,8 @@ export const slugify = (str: string) => {
|
|||
.replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash
|
||||
.replace(/[\s_]+/g, "-") // Replace whitespace and underscores with a single dash
|
||||
.replace(/^-+/, "") // Remove dashes from start
|
||||
.replace(/-+$/, ""); // Remove dashes from end
|
||||
.replace(/-+$/, "") // Remove dashes from end
|
||||
.replace(/\+/g, "-"); // Transform all + to -
|
||||
};
|
||||
|
||||
export default slugify;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const validateUsername = async (username: string, email: string, organizationId?: number) => {
|
||||
// There is an existingUser if, within an org context or not, the username matches
|
||||
// OR if the email matches AND either the email is verified
|
||||
// or both username and password are set
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
...(organizationId ? { organizationId } : {}),
|
||||
OR: [
|
||||
{ username },
|
||||
{
|
||||
AND: [
|
||||
{ email },
|
||||
{
|
||||
OR: [
|
||||
{ emailVerified: { not: null } },
|
||||
{
|
||||
AND: [{ password: { not: null } }, { username: { not: null } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
return { isValid: !existingUser, email: existingUser?.email };
|
||||
};
|
||||
|
||||
export const validateUsernameInTeam = async (username: string, email: string, teamId: number) => {
|
||||
try {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
select: {
|
||||
metadata: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamData = { ...team, metadata: teamMetadataSchema.parse(team?.metadata) };
|
||||
|
||||
if (teamData.metadata?.isOrganization || teamData.parentId) {
|
||||
// Organization context -> org-context username check
|
||||
const orgId = teamData.parentId || teamId;
|
||||
return validateUsername(username, email, orgId);
|
||||
} else {
|
||||
// Regular team context -> regular username check
|
||||
return validateUsername(username, email);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { isValid: false, email: undefined };
|
||||
}
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
|
||||
import slugify from "./slugify";
|
||||
|
||||
/** Scenarios:
|
||||
* 1 org 1 child team:
|
||||
* 1 org 2+ child teams:
|
||||
* 1 org 1 child team and 1 child team of first child team: Is this supported?
|
||||
*/
|
||||
|
||||
export const validateUsernameInOrg = async (usernameSlug: string, teamId: number): Promise<boolean> => {
|
||||
try {
|
||||
let takenSlugs = [];
|
||||
const teamsFound = await prisma.team.findMany({
|
||||
where: {
|
||||
OR: [{ id: teamId }, { parentId: teamId }],
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const usersFound = await prisma.user.findMany({
|
||||
where: {
|
||||
organizationId: teamId,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
takenSlugs = usersFound.map((user) => user.username);
|
||||
|
||||
// If only one team is found and it has a parent, then it's an child team
|
||||
// and we can use the parent id to find all the teams that belong to this org
|
||||
if (teamsFound && teamsFound.length === 1 && teamsFound[0].parentId) {
|
||||
// Let's find all the teams that belong to this org
|
||||
const childTeams = await prisma.team.findMany({
|
||||
where: {
|
||||
// With this we include org team slug and child teams slugs
|
||||
OR: [{ id: teamsFound[0].parentId }, { parentId: teamsFound[0].parentId }],
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
takenSlugs = takenSlugs.concat(childTeams.map((team) => team.slug));
|
||||
} else {
|
||||
takenSlugs = takenSlugs.concat(teamsFound.map((team) => team.slug));
|
||||
}
|
||||
|
||||
return !takenSlugs.includes(slugify(usernameSlug));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue