fix: possible to have same slug for team and user in an organization 9985 cal 2122 (#10170)
* Added rule to validate username vs team slugs in team considering orgs too * Validate valid username vs team slugs * Add or clause to find org team and child teams
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user