diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index 0ccaef0223..1358e14bbb 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -6,6 +6,7 @@ import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import slugify from "@calcom/lib/slugify"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; +import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -70,6 +71,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(409).json({ message }); } + let foundToken: { id: number; teamId: number | null; expires: Date } | null = null; + if (token) { + foundToken = await prisma.verificationToken.findFirst({ + where: { + token, + }, + select: { + id: true, + expires: true, + teamId: true, + }, + }); + + if (!foundToken) { + return res.status(401).json({ message: "Invalid Token" }); + } + + if (dayjs(foundToken?.expires).isBefore(dayjs())) { + 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 hashedPassword = await hashPassword(password); const user = await prisma.user.upsert({ @@ -88,91 +118,75 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - if (token) { - const foundToken = await prisma.verificationToken.findFirst({ + if (foundToken && foundToken?.teamId) { + const team = await prisma.team.findUnique({ where: { - token, + id: foundToken.teamId, }, }); - if (!foundToken) { - return res.status(401).json({ message: "Invalid Token" }); - } + if (team) { + const teamMetadata = teamMetadataSchema.parse(team?.metadata); + if (teamMetadata?.isOrganization) { + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + organizationId: team.id, + }, + }); + } - if (dayjs(foundToken?.expires).isBefore(dayjs())) { - return res.status(401).json({ message: "Token expired" }); - } - - if (foundToken.teamId) { - const team = await prisma.team.findUnique({ + const membership = await prisma.membership.update({ where: { - id: foundToken.teamId, + userId_teamId: { userId: user.id, teamId: team.id }, + }, + data: { + accepted: true, }, }); + closeComUpsertTeamUser(team, user, membership.role); - if (team) { - const teamMetadata = teamMetadataSchema.parse(team?.metadata); - if (teamMetadata?.isOrganization) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - organizationId: team.id, - }, - }); - } - - const membership = await prisma.membership.update({ + // Accept any child team invites for orgs. + if (team.parentId) { + // Join ORG + await prisma.user.update({ where: { - userId_teamId: { userId: user.id, teamId: team.id }, + id: user.id, + }, + data: { + organizationId: team.parentId, + }, + }); + + /** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */ + await prisma.membership.updateMany({ + where: { + userId: user.id, + team: { + id: team.parentId, + }, + accepted: false, }, data: { accepted: true, }, }); - closeComUpsertTeamUser(team, user, membership.role); - // Accept any child team invites for orgs. - if (team.parentId) { - // Join ORG - await prisma.user.update({ - where: { - id: user.id, + // Join any other invites + await prisma.membership.updateMany({ + where: { + userId: user.id, + team: { + parentId: team.parentId, }, - data: { - organizationId: team.parentId, - }, - }); - - /** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */ - await prisma.membership.updateMany({ - where: { - userId: user.id, - team: { - id: team.parentId, - }, - accepted: false, - }, - data: { - accepted: true, - }, - }); - - // Join any other invites - await prisma.membership.updateMany({ - where: { - userId: user.id, - team: { - parentId: team.parentId, - }, - accepted: false, - }, - data: { - accepted: true, - }, - }); - } + accepted: false, + }, + data: { + accepted: true, + }, + }); } } diff --git a/packages/lib/validateUsernameInOrg.ts b/packages/lib/validateUsernameInOrg.ts new file mode 100644 index 0000000000..d628a194a2 --- /dev/null +++ b/packages/lib/validateUsernameInOrg.ts @@ -0,0 +1,47 @@ +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 => { + try { + let takenSlugs = []; + const teamsFound = await prisma.team.findMany({ + where: { + OR: [{ id: teamId }, { parentId: teamId }], + }, + select: { + slug: true, + parentId: true, + }, + }); + + // 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 = childTeams.map((team) => team.slug); + } else { + takenSlugs = teamsFound.map((team) => team.slug); + } + + return !takenSlugs.includes(slugify(usernameSlug)); + } catch (error) { + console.error(error); + return false; + } +}; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index ff260836ed..8bdc906715 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -86,7 +86,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) = isCalcomMember: true, }; /** - * Here we want to redirect to a differnt place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a differnt email template. + * Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template. * This only changes if the user is a CAL user and has not completed onboarding and has no password */ if (!invitee.completedOnboarding && !invitee.password && invitee.identityProvider === "CAL") {