diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index 97079e6c88..ad32497a2f 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -1,6 +1,5 @@ import type { NextApiResponse } from "next"; -import dayjs from "@calcom/dayjs"; import { createUser, findExistingUser, @@ -10,7 +9,16 @@ import { sendVerificationEmail, syncServicesCreateUser, throwIfSignupIsDisabled, + createStripeCustomer, } from "@calcom/feature-auth/lib/signup/signupUtils"; +import { + checkIfTokenExistsAndValid, + acceptAllInvitesWithTeamId, + findTeam, + upsertUsersPasswordAndVerify, + joinOrgAndAcceptChildInivtes, + cleanUpInviteToken, +} from "@calcom/feature-auth/lib/signup/teamInviteUtils"; import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { IS_CALCOM } from "@calcom/lib/constants"; import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest"; @@ -18,8 +26,6 @@ import { HttpError } from "@calcom/lib/http-error"; import type { RequestWithUsernameStatus } from "@calcom/lib/server/username"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { validateUsernameInTeam } from "@calcom/lib/validateUsername"; -import { IdentityProvider } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; export default async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { try { @@ -28,9 +34,14 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA const { email, password, language, token, username } = parseSignupData(req.body); await findExistingUser(username, email); const hashedPassword = await hashPassword(password); - const premiumUsernameMetadata = await handlePremiumUsernameFlow({ + + const customer = await createStripeCustomer({ email, username, + }); + + const premiumUsernameMetadata = await handlePremiumUsernameFlow({ + customer, premiumUsernameStatusCode: req.usernameStatus.statusCode, }); @@ -48,56 +59,19 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA username: username || "", }); await syncServicesCreateUser(user); - } - { - const 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" }); - } + } else { + const foundToken = await checkIfTokenExistsAndValid(token); if (foundToken?.teamId) { const teamUserValidation = await validateUsernameInTeam(username, email, foundToken?.teamId); if (!teamUserValidation.isValid) { return res.status(409).json({ message: "Username or email is already taken" }); } - const team = await prisma.team.findUnique({ - where: { - id: foundToken.teamId, - }, - }); + const team = await findTeam(foundToken.teamId); if (team) { - const teamMetadata = teamMetadataSchema.parse(team?.metadata); - - const user = 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, - }, - }); - + const teamMetadata = team.metadata; + const user = await upsertUsersPasswordAndVerify(email, username, hashedPassword); if (teamMetadata?.isOrganization) { await prisma.user.update({ where: { @@ -108,64 +82,12 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA }, }); } - - const membership = await prisma.membership.update({ - where: { - userId_teamId: { userId: user.id, teamId: team.id }, - }, - data: { - accepted: true, - }, - }); + const membership = await acceptAllInvitesWithTeamId(user.id, team.id); 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, - }, - }); + await joinOrgAndAcceptChildInivtes(user.id, team.parentId); } - - // Cleanup token after use - await prisma.verificationToken.delete({ - where: { - id: foundToken.id, - }, - }); + await cleanUpInviteToken(foundToken.id); } } } @@ -184,9 +106,11 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA return res.status(201).json({ message: "Created user" }); } catch (e) { + console.log(e); if (e instanceof HttpError) { return res.status(e.statusCode).json({ message: e.message }); } + return res.status(500).json({ message: "Internal server error" }); } diff --git a/packages/features/auth/lib/signup/signupUtils.ts b/packages/features/auth/lib/signup/signupUtils.ts index 00fe587caf..f0d38b9303 100644 --- a/packages/features/auth/lib/signup/signupUtils.ts +++ b/packages/features/auth/lib/signup/signupUtils.ts @@ -1,4 +1,4 @@ -import { sendEmailVerification } from "auth/lib/verifyEmail"; +import type Stripe from "stripe"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; @@ -9,6 +9,8 @@ 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: { @@ -62,22 +64,7 @@ export function parseSignupData(data: unknown) { username: slugify(parsedSchema.data.username), }; } - -export async function handlePremiumUsernameFlow({ - email, - username, - premiumUsernameStatusCode, -}: { - email: string; - username: string; - premiumUsernameStatusCode: number; -}) { - if (!IS_CALCOM) return; - const metadata: { - stripeCustomerId?: string; - checkoutSessionId?: string; - } = {}; - +export async function createStripeCustomer({ email, username }: { email: string; username: string }) { // Create the customer in Stripe const customer = await stripe.customers.create({ email, @@ -87,6 +74,25 @@ export async function handlePremiumUsernameFlow({ }, }); + return customer; +} + +export async function handlePremiumUsernameFlow({ + customer, + premiumUsernameStatusCode, +}: { + premiumUsernameStatusCode: number; + customer?: Stripe.Customer; +}) { + if (!IS_CALCOM) return; + + if (!customer) { + throw new HttpError({ + statusCode: 500, + message: "Missing customer", + }); + } + const returnUrl = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=/auth/verify?sessionId={CHECKOUT_SESSION_ID}`; if (premiumUsernameStatusCode === 402) { @@ -150,7 +156,6 @@ export function sendVerificationEmail({ language: string; username: string; }) { - if (!IS_CALCOM) return; return sendEmailVerification({ email, language, diff --git a/packages/features/auth/lib/signup/teamInviteUtils.ts b/packages/features/auth/lib/signup/teamInviteUtils.ts new file mode 100644 index 0000000000..9edf582ae7 --- /dev/null +++ b/packages/features/auth/lib/signup/teamInviteUtils.ts @@ -0,0 +1,131 @@ +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 joinOrgAndAcceptChildInivtes(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, + }, + }); +}