diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index f0ef6fd9d0..e8f13bf166 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -2,7 +2,7 @@ import type { NextApiResponse } from "next"; import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHandler"; import selfhostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler"; -import type { RequestWithUsernameStatus } from "@calcom/features/auth/signup/username"; +import { type RequestWithUsernameStatus } from "@calcom/features/auth/signup/username"; import { IS_CALCOM } from "@calcom/lib/constants"; import { HttpError } from "@calcom/lib/http-error"; diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 53a960179f..14b83ecc7f 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -56,6 +56,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup if (err.checkoutSessionId) { const stripe = await getStripe(); if (stripe) { + console.log("Redirecting to stripe checkout"); const { error } = await stripe.redirectToCheckout({ sessionId: err.checkoutSessionId, }); diff --git a/packages/features/auth/lib/signup/calcomhandler.ts b/packages/features/auth/lib/signup/calcomhandler.ts deleted file mode 100644 index ae7079ac39..0000000000 --- a/packages/features/auth/lib/signup/calcomhandler.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { NextApiResponse } from "next"; - -import stripe from "@calcom/app-store/stripepayment/lib/server"; -import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; -import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest"; -import type { RequestWithUsernameStatus } from "@calcom/lib/server/username"; -import { usernameHandler } from "@calcom/lib/server/username"; -import { createWebUser as syncServicesCreateWebUser } from "@calcom/lib/sync/SyncServiceManager"; -import prisma from "@calcom/prisma"; - -async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { - if (req.method === "POST") { - const { email: _email, password } = req.body; - let username: string | null = req.usernameStatus.requestedUserName; - let checkoutSessionId: string | null = null; - - // Check for premium username - if (req.usernameStatus.statusCode === 418) { - return res.status(req.usernameStatus.statusCode).json(req.usernameStatus.json); - } - - // Validate the user - if (!username) { - res.status(422).json({ message: "Invalid username" }); - return; - } - - if (typeof _email !== "string" || !_email.includes("@")) { - res.status(422).json({ message: "Invalid email" }); - return; - } - - const email = _email.toLowerCase(); - - if (!password || password.trim().length < 7) { - res.status(422).json({ - message: "Invalid input - password should be at least 7 characters long.", - }); - return; - } - - const existingUser = await prisma.user.findFirst({ - where: { - OR: [ - { - username, - }, - { - email, - }, - ], - }, - }); - - if (existingUser) { - res.status(422).json({ message: "A user exists with that email address" }); - return; - } - - // Create the customer in Stripe - const customer = await stripe.customers.create({ - email, - metadata: { - email /* Stripe customer email can be changed, so we add this to keep track of which email was used to signup */, - username, - }, - }); - - const returnUrl = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=/auth/verify?sessionId={CHECKOUT_SESSION_ID}`; - - // Pro username, must be purchased - if (req.usernameStatus.statusCode === 402) { - const checkoutSession = await stripe.checkout.sessions.create({ - mode: "subscription", - payment_method_types: ["card"], - customer: customer.id, - line_items: [ - { - price: getPremiumMonthlyPlanPriceId(), - quantity: 1, - }, - ], - success_url: returnUrl, - cancel_url: returnUrl, - allow_promotion_codes: true, - }); - - /** We create a username-less user until he pays */ - checkoutSessionId = checkoutSession.id; - username = null; - } - - // Hash the password - const hashedPassword = await hashPassword(password); - - // Create the user - const user = await prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - metadata: { - stripeCustomerId: customer.id, - checkoutSessionId, - }, - }, - }); - - sendEmailVerification({ - email, - language: await getLocaleFromRequest(req), - username: username || "", - }); - - // Sync Services - await syncServicesCreateWebUser(user); - - if (checkoutSessionId) { - res.status(402).json({ - message: "Created user but missing payment", - checkoutSessionId, - }); - return; - } - - res.status(201).json({ message: "Created user", stripeCustomerId: customer.id }); - } else { - res.status(405).end(); - } -} - -export default usernameHandler(handler); diff --git a/packages/features/auth/lib/signup/selfhostedHandler.ts b/packages/features/auth/lib/signup/selfhostedHandler.ts deleted file mode 100644 index 11affd1738..0000000000 --- a/packages/features/auth/lib/signup/selfhostedHandler.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { NextApiResponse } from "next"; - -import dayjs from "@calcom/dayjs"; -import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; -import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; -import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; -import { IS_CALCOM } from "@calcom/lib/constants"; -import type { RequestWithUsernameStatus } from "@calcom/lib/server/username"; -import slugify from "@calcom/lib/slugify"; -import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; -import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername"; -import prisma from "@calcom/prisma"; -import { IdentityProvider } from "@calcom/prisma/enums"; -import { signupSchema } from "@calcom/prisma/zod-utils"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -export default async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { - if (req.method !== "POST") { - return res.status(405).end(); - } - - if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") { - res.status(403).json({ message: "Signup is disabled" }); - return; - } - - const data = req.body; - const { email, password, language, token } = signupSchema.parse(data); - - const username = slugify(data.username); - const userEmail = email.toLowerCase(); - - if (!username) { - res.status(422).json({ message: "Invalid username" }); - return; - } - - 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 teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId); - if (!teamUserValidation.isValid) { - return res.status(409).json({ message: "Username or email is already taken" }); - } - } - } else { - const userValidation = await validateUsername(username, userEmail); - if (!userValidation.isValid) { - return res.status(409).json({ message: "Username or email is already taken" }); - } - } - - const hashedPassword = await hashPassword(password); - - if (foundToken && foundToken?.teamId) { - const team = await prisma.team.findUnique({ - where: { - id: foundToken.teamId, - }, - }); - if (team) { - const teamMetadata = teamMetadataSchema.parse(team?.metadata); - - if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) { - const checkUsername = await checkPremiumUsername(username); - if (checkUsername.premium) { - // This signup page is ONLY meant for team invites and local setup. Not for every day users. - // In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two. - return res.status(422).json({ - message: "Sign up from https://cal.com/signup to claim your premium username", - }); - } - } - - const user = await prisma.user.upsert({ - where: { email: userEmail }, - update: { - username, - password: hashedPassword, - emailVerified: new Date(Date.now()), - identityProvider: IdentityProvider.CAL, - }, - create: { - username, - email: userEmail, - password: hashedPassword, - identityProvider: IdentityProvider.CAL, - }, - }); - - if (teamMetadata?.isOrganization) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - organizationId: team.id, - }, - }); - } - - const membership = await prisma.membership.update({ - where: { - userId_teamId: { userId: user.id, teamId: team.id }, - }, - 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, - }, - 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, - }, - }); - } - } - - // Cleanup token after use - await prisma.verificationToken.delete({ - where: { - id: foundToken.id, - }, - }); - } else { - if (IS_CALCOM) { - const checkUsername = await checkPremiumUsername(username); - if (checkUsername.premium) { - res.status(422).json({ - message: "Sign up from https://cal.com/signup to claim your premium username", - }); - return; - } - } - await prisma.user.upsert({ - where: { email: userEmail }, - update: { - username, - password: hashedPassword, - emailVerified: new Date(Date.now()), - identityProvider: IdentityProvider.CAL, - }, - create: { - username, - email: userEmail, - password: hashedPassword, - identityProvider: IdentityProvider.CAL, - }, - }); - await sendEmailVerification({ - email: userEmail, - username, - language, - }); - } - - res.status(201).json({ message: "Created user" }); -} diff --git a/packages/features/auth/lib/signup/signupUtils.ts b/packages/features/auth/lib/signup/signupUtils.ts deleted file mode 100644 index c8eeefddc2..0000000000 --- a/packages/features/auth/lib/signup/signupUtils.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type Stripe from "stripe"; - -import { PREMIUM_MONTHLY_PLAN_PRICE } from "@calcom/app-store/stripepayment/lib"; -import stripe from "@calcom/app-store/stripepayment/lib/server"; -import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; -import { IS_CALCOM, IS_STRIPE_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; -import { HttpError } from "@calcom/lib/http-error"; -import type { RequestWithUsernameStatus } from "@calcom/lib/server/username"; -import slugify from "@calcom/lib/slugify"; -import { createWebUser as syncServicesCreateWebUser } from "@calcom/lib/sync/SyncServiceManager"; -import { signupSchema } from "@calcom/prisma/zod-utils"; - -import { sendEmailVerification } from "../verifyEmail"; - -export async function findExistingUser(username: string, email: string) { - const user = await prisma.user.findFirst({ - where: { - OR: [ - { - username, - }, - { - email, - }, - ], - }, - }); - if (user) { - throw new HttpError({ - statusCode: 442, - message: "A user exists with that email address", - }); - } -} - -export function ensurePostMethod(req: RequestWithUsernameStatus) { - if (req.method !== "POST") { - throw new HttpError({ - statusCode: 405, - message: "Method not allowed", - }); - } -} - -export function throwIfSignupIsDisabled() { - if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") { - throw new HttpError({ - statusCode: 403, - message: "Signup is disabled", - }); - } -} - -export function parseSignupData(data: unknown) { - const parsedSchema = signupSchema.safeParse(data); - if (!parsedSchema.success) { - throw new HttpError({ - statusCode: 422, - message: "Invalid input", - }); - } - return { - ...parsedSchema.data, - email: parsedSchema.data.email.toLowerCase(), - username: slugify(parsedSchema.data.username), - }; -} -export async function createStripeCustomer({ email, username }: { email: string; username: string }) { - // Create the customer in Stripe - if (!IS_STRIPE_ENABLED) return; - console.log("Creating Stripe customer"); - const customer = await stripe.customers.create({ - email, - metadata: { - email /* Stripe customer email can be changed, so we add this to keep track of which email was used to signup */, - username, - }, - }); - return customer; -} - -export async function handlePremiumUsernameFlow({ - customer, - premiumUsernameStatusCode, -}: { - premiumUsernameStatusCode: number; - customer?: Stripe.Customer; -}) { - if (!IS_STRIPE_ENABLED || !PREMIUM_MONTHLY_PLAN_PRICE || !IS_CALCOM) return; - - if (!customer) { - throw new HttpError({ - statusCode: 500, - message: "Missing customer", - }); - } - - const metadata: { - stripeCustomerId?: string; - checkoutSessionId?: string; - } = {}; - - const returnUrl = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=/auth/verify?sessionId={CHECKOUT_SESSION_ID}`; - - if (premiumUsernameStatusCode === 402) { - const checkoutSession = await stripe.checkout.sessions.create({ - mode: "subscription", - payment_method_types: ["card"], - customer: customer.id, - line_items: [ - { - price: getPremiumMonthlyPlanPriceId(), - quantity: 1, - }, - ], - success_url: returnUrl, - cancel_url: returnUrl, - allow_promotion_codes: true, - }); - - /** We create a username-less user until he pays */ - metadata["stripeCustomerId"] = customer.id; - metadata["checkoutSessionId"] = checkoutSession.id; - } - - return metadata; -} - -export function createUser({ - username, - email, - hashedPassword, - metadata, -}: { - username: string; - email: string; - hashedPassword: string; - metadata?: { - stripeCustomerId?: string; - checkoutSessionId?: string; - }; -}) { - return prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - metadata, - }, - }); -} - -export function syncServicesCreateUser(user: Awaited>) { - return IS_CALCOM && syncServicesCreateWebUser(user); -} - -export function sendVerificationEmail({ - email, - language, - username, -}: { - email: string; - language: string; - username: string; -}) { - return sendEmailVerification({ - email, - language, - username: username || "", - }); -} diff --git a/packages/features/auth/lib/signup/teamInviteUtils.ts b/packages/features/auth/lib/signup/teamInviteUtils.ts deleted file mode 100644 index 9198f81333..0000000000 --- a/packages/features/auth/lib/signup/teamInviteUtils.ts +++ /dev/null @@ -1,131 +0,0 @@ -import dayjs from "@calcom/dayjs"; -import { HttpError } from "@calcom/lib/http-error"; -import { IdentityProvider } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -export async function checkIfTokenExistsAndValid(token: string) { - const foundToken = await prisma.verificationToken.findFirst({ - where: { - token, - }, - select: { - id: true, - expires: true, - teamId: true, - }, - }); - if (!foundToken) { - throw new HttpError({ - statusCode: 401, - message: "Invalid Token", - }); - } - - if (dayjs(foundToken?.expires).isBefore(dayjs())) { - throw new HttpError({ - statusCode: 401, - message: "Token expired", - }); - } - - return foundToken; -} - -export async function findTeam(teamId: number) { - const team = await prisma.team.findUnique({ - where: { - id: teamId, - }, - }); - - if (!team) { - throw new HttpError({ - statusCode: 404, - message: "Team not found", - }); - } - - return { - ...team, - metadata: teamMetadataSchema.parse(team.metadata), - }; -} - -export async function upsertUsersPasswordAndVerify(email: string, username: string, hashedPassword: string) { - return await prisma.user.upsert({ - where: { email }, - update: { - username, - password: hashedPassword, - emailVerified: new Date(Date.now()), - identityProvider: IdentityProvider.CAL, - }, - create: { - username, - email: email, - password: hashedPassword, - identityProvider: IdentityProvider.CAL, - }, - }); -} - -export async function acceptAllInvitesWithTeamId(userId: number, teamId: number) { - const membership = await prisma.membership.update({ - where: { - userId_teamId: { userId: userId, teamId: teamId }, - }, - data: { - accepted: true, - }, - }); - - return membership; -} - -export async function joinOrgAndAcceptChildInvites(userId: number, orgId: number) { - // Join ORG - await prisma.user.update({ - where: { - id: userId, - }, - data: { - organizationId: orgId, - }, - }); - - /** 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: userId, - team: { - id: orgId, - }, - accepted: false, - }, - data: { - accepted: true, - }, - }); - - // Join any other invites - await prisma.membership.updateMany({ - where: { - userId: userId, - team: { - parentId: orgId, - }, - accepted: false, - }, - data: { - accepted: true, - }, - }); -} - -export async function cleanUpInviteToken(tokenId: number) { - await prisma.verificationToken.delete({ - where: { - id: tokenId, - }, - }); -} diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index c59020c8d3..2257136928 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -7,12 +7,28 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest"; import { createWebUser as syncServicesCreateWebUser } from "@calcom/lib/sync/SyncServiceManager"; +import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; +import { validateUsername } from "@calcom/lib/validateUsername"; import prisma from "@calcom/prisma"; +import { IdentityProvider } from "@calcom/prisma/enums"; +import { signupSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { usernameHandler, type RequestWithUsernameStatus } from "../username"; +import { joinOrganization, joinAnyChildTeamOnOrgInvite } from "../utils/organization"; +import { findTokenByToken, throwIfTokenExpired, validateUsernameForTeam } from "../utils/token"; async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { - const { email: _email, password } = req.body; + const { + email: _email, + password, + token, + } = signupSchema + .pick({ + email: true, + password: true, + token: true, + }) + .parse(req.body); let username: string | null = req.usernameStatus.requestedUserName; let checkoutSessionId: string | null = null; @@ -27,36 +43,18 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { return; } - if (typeof _email !== "string" || !_email.includes("@")) { - res.status(422).json({ message: "Invalid email" }); - return; - } - const email = _email.toLowerCase(); - if (!password || password.trim().length < 7) { - res.status(422).json({ - message: "Invalid input - password should be at least 7 characters long.", - }); - return; - } - - const existingUser = await prisma.user.findFirst({ - where: { - OR: [ - { - username, - }, - { - email, - }, - ], - }, - }); - - if (existingUser) { - res.status(422).json({ message: "A user exists with that email address" }); - return; + let foundToken: { id: number; teamId: number | null; expires: Date } | null = null; + if (token) { + foundToken = await findTokenByToken({ token }); + throwIfTokenExpired(foundToken?.expires); + validateUsernameForTeam({ username, email, teamId: foundToken?.teamId }); + } else { + const userValidation = await validateUsername(username, email); + if (!userValidation.isValid) { + return res.status(409).json({ message: "Username or email is already taken" }); + } } // Create the customer in Stripe @@ -95,37 +93,94 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { // Hash the password const hashedPassword = await hashPassword(password); - // Create the user - const user = await prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - metadata: { - stripeCustomerId: customer.id, - checkoutSessionId, + if (foundToken && foundToken?.teamId) { + const team = await prisma.team.findUnique({ + where: { + id: foundToken.teamId, }, - }, - }); + }); + if (team) { + const teamMetadata = teamMetadataSchema.parse(team?.metadata); - sendEmailVerification({ - email, - language: await getLocaleFromRequest(req), - username: username || "", - }); + const user = await prisma.user.upsert({ + where: { email }, + update: { + username, + password: hashedPassword, + emailVerified: new Date(Date.now()), + identityProvider: IdentityProvider.CAL, + }, + create: { + username, + email, + password: hashedPassword, + identityProvider: IdentityProvider.CAL, + }, + }); - // Sync Services - await syncServicesCreateWebUser(user); + if (teamMetadata?.isOrganization) { + await joinOrganization({ + organizationId: team.id, + userId: user.id, + }); + } + + const membership = await prisma.membership.update({ + where: { + userId_teamId: { userId: user.id, teamId: team.id }, + }, + data: { + accepted: true, + }, + }); + closeComUpsertTeamUser(team, user, membership.role); + + // Accept any child team invites for orgs. + if (team.parentId) { + await joinAnyChildTeamOnOrgInvite({ + userId: user.id, + orgId: team.parentId, + }); + } + } + + // Cleanup token after use + await prisma.verificationToken.delete({ + where: { + id: foundToken.id, + }, + }); + } else { + // Create the user + const user = await prisma.user.create({ + data: { + username, + email, + password: hashedPassword, + metadata: { + stripeCustomerId: customer.id, + checkoutSessionId, + }, + }, + }); + + sendEmailVerification({ + email, + language: await getLocaleFromRequest(req), + username: username || "", + }); + // Sync Services + await syncServicesCreateWebUser(user); + } if (checkoutSessionId) { - res.status(402).json({ + return res.status(402).json({ message: "Created user but missing payment", checkoutSessionId, }); - return; } - res.status(201).json({ message: "Created user", stripeCustomerId: customer.id }); + return res.status(201).json({ message: "Created user", stripeCustomerId: customer.id }); } export default usernameHandler(handler); diff --git a/packages/features/auth/signup/handlers/selfHostedHandler.ts b/packages/features/auth/signup/handlers/selfHostedHandler.ts index 5292804b6b..6e56f2bd01 100644 --- a/packages/features/auth/signup/handlers/selfHostedHandler.ts +++ b/packages/features/auth/signup/handlers/selfHostedHandler.ts @@ -1,18 +1,20 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import dayjs from "@calcom/dayjs"; import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; 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 { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername"; +import { validateUsername } from "@calcom/lib/validateUsername"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; import { signupSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { joinAnyChildTeamOnOrgInvite, joinOrganization } from "../utils/organization"; +import { findTokenByToken, throwIfTokenExpired, validateUsernameForTeam } from "../utils/token"; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const data = req.body; const { email, password, language, token } = signupSchema.parse(data); @@ -27,30 +29,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 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 teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId); - if (!teamUserValidation.isValid) { - return res.status(409).json({ message: "Username or email is already taken" }); - } - } + foundToken = await findTokenByToken({ token }); + throwIfTokenExpired(foundToken?.expires); + validateUsernameForTeam({ username, email: userEmail, teamId: foundToken?.teamId }); } else { const userValidation = await validateUsername(username, userEmail); if (!userValidation.isValid) { @@ -69,16 +50,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (team) { const teamMetadata = teamMetadataSchema.parse(team?.metadata); - if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) { - const checkUsername = await checkPremiumUsername(username); - if (checkUsername.premium) { - // This signup page is ONLY meant for team invites and local setup. Not for every day users. - // In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two. - return res.status(422).json({ - message: "Sign up from https://cal.com/signup to claim your premium username", - }); - } - } + // if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) { + // const checkUsername = await checkPremiumUsername(username); + // if (checkUsername.premium) { + // // This signup page is ONLY meant for team invites and local setup. Not for every day users. + // // In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two. + // return res.status(422).json({ + // message: "Sign up from https://cal.com/signup to claim your premium username", + // }); + // } + // } const user = await prisma.user.upsert({ where: { email: userEmail }, @@ -97,13 +78,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (teamMetadata?.isOrganization) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - organizationId: team.id, - }, + await joinOrganization({ + organizationId: team.id, + userId: user.id, }); } @@ -119,42 +96,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Accept any child team invites for orgs. if (team.parentId) { - // Join ORG - await prisma.user.update({ - where: { - 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, - }, - }); - - // Join any other invites - await prisma.membership.updateMany({ - where: { - userId: user.id, - team: { - parentId: team.parentId, - }, - accepted: false, - }, - data: { - accepted: true, - }, + await joinAnyChildTeamOnOrgInvite({ + userId: user.id, + orgId: team.parentId, }); } } diff --git a/packages/features/auth/signup/username.ts b/packages/features/auth/signup/username.ts index cd162a9ff9..9a98155aa1 100644 --- a/packages/features/auth/signup/username.ts +++ b/packages/features/auth/signup/username.ts @@ -1,4 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; import notEmpty from "@calcom/lib/notEmpty"; import slugify from "@calcom/lib/slugify"; @@ -25,6 +26,16 @@ export type RequestWithUsernameStatus = NextApiRequest & { }; }; }; +export const usernameStatusSchema = z.object({ + statusCode: z.union([z.literal(200), z.literal(402), z.literal(418)]), + requestedUserName: z.string(), + json: z.object({ + available: z.boolean(), + premium: z.boolean(), + message: z.string().optional(), + suggestion: z.string().optional(), + }), +}); type CustomNextApiHandler = ( req: RequestWithUsernameStatus, diff --git a/packages/features/auth/signup/utils/organization.ts b/packages/features/auth/signup/utils/organization.ts new file mode 100644 index 0000000000..3b95e4f1ec --- /dev/null +++ b/packages/features/auth/signup/utils/organization.ts @@ -0,0 +1,56 @@ +export async function joinOrganization({ + organizationId, + userId, +}: { + userId: number; + organizationId: number; +}) { + return await prisma.user.update({ + where: { + id: userId, + }, + data: { + organizationId: organizationId, + }, + }); +} + +export async function joinAnyChildTeamOnOrgInvite({ userId, orgId }: { userId: number; orgId: number }) { + // Join ORG + await prisma.user.update({ + where: { + id: userId, + }, + data: { + organizationId: orgId, + }, + }); + + /** 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, + team: { + id: orgId, + }, + accepted: false, + }, + data: { + accepted: true, + }, + }); + + // Join any other invites + await prisma.membership.updateMany({ + where: { + userId, + team: { + parentId: orgId, + }, + accepted: false, + }, + data: { + accepted: true, + }, + }); +} diff --git a/packages/features/auth/signup/utils/token.ts b/packages/features/auth/signup/utils/token.ts new file mode 100644 index 0000000000..bff5676a50 --- /dev/null +++ b/packages/features/auth/signup/utils/token.ts @@ -0,0 +1,53 @@ +import dayjs from "@calcom/dayjs"; +import { HttpError } from "@calcom/lib/http-error"; +import { validateUsernameInTeam } from "@calcom/lib/validateUsername"; + +export async function findTokenByToken({ token }: { token: string }) { + const foundToken = await prisma.verificationToken.findFirst({ + where: { + token, + }, + select: { + id: true, + expires: true, + teamId: true, + }, + }); + + if (!foundToken) { + throw new HttpError({ + statusCode: 401, + message: "Invalid Token", + }); + } + + return foundToken; +} + +export function throwIfTokenExpired(expires: Date) { + if (dayjs(expires).isBefore(dayjs())) { + throw new HttpError({ + statusCode: 401, + message: "Token expired", + }); + } +} + +export async function validateUsernameForTeam({ + username, + email, + teamId, +}: { + username: string; + email: string; + teamId: number | null; +}) { + if (!teamId) return; + const teamUserValidation = await validateUsernameInTeam(username, email, teamId); + if (!teamUserValidation.isValid) { + throw new HttpError({ + statusCode: 409, + message: "Username or email is already taken", + }); + } +}