import dayjs from "@calcom/dayjs"; import { clearSessionCache } from "@calcom/features/auth/lib/getServerSession"; import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing"; import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service"; import { IS_STRIPE_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { prisma } from "@calcom/prisma"; import { CreationSource, MembershipRole } from "@calcom/prisma/enums"; import { userMetadata } from "@calcom/prisma/zod-utils"; import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler"; import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; const verifySchema = z.object({ token: z.string(), makePrimary: z.string().optional(), redirectTo: z.string().optional(), }); const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email"; // TODO: To be unit tested export async function moveUserToMatchingOrg({ email }: { email: string }) { const organizationRepository = getOrganizationRepository(); const org = await organizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail({ email }); if (!org) { return; } await inviteMembersWithNoInviterPermissionCheck({ inviterName: null, teamId: org.id, language: "en", creationSource: CreationSource.WEBAPP, invitations: [ { usernameOrEmail: email, role: MembershipRole.MEMBER, }, ], orgSlug: org.slug || org.requestedSlug, }); } export async function handler(req: NextApiRequest, res: NextApiResponse) { const { token, makePrimary, redirectTo } = verifySchema.parse(req.query); const foundToken = await prisma.verificationToken.findFirst({ where: { token, }, }); if (!foundToken) { return res.status(401).json({ message: "No token found" }); } if (dayjs(foundToken?.expires).isBefore(dayjs())) { return res.status(401).json({ message: "Token expired" }); } // The user is verifying the secondary email if (foundToken?.secondaryEmailId) { await prisma.secondaryEmail.update({ where: { id: foundToken.secondaryEmailId, email: foundToken?.identifier, }, data: { emailVerified: new Date(), }, }); if (makePrimary === "true") { const secondaryEmail = await prisma.secondaryEmail.findUnique({ where: { id: foundToken.secondaryEmailId }, select: { userId: true, email: true }, }); if (secondaryEmail) { const primaryUser = await prisma.user.findUnique({ where: { id: secondaryEmail.userId }, select: { id: true, email: true, emailVerified: true }, }); if (primaryUser) { await prisma.$transaction([ prisma.user.update({ where: { id: primaryUser.id }, data: { email: secondaryEmail.email, emailVerified: new Date() }, }), prisma.secondaryEmail.update({ where: { id: foundToken.secondaryEmailId }, data: { email: primaryUser.email, emailVerified: primaryUser.emailVerified, }, }), ]); } } clearSessionCache(); } await cleanUpVerificationTokens(foundToken.id); const isSafePath = redirectTo?.startsWith("/") && !redirectTo.startsWith("//") && !redirectTo.startsWith("/\\"); let redirectPath = isSafePath && redirectTo ? redirectTo : "/settings/my-account/profile"; if (makePrimary === "true") { const separator = redirectPath.includes("?") ? "&" : "?"; redirectPath = `${redirectPath}${separator}sessionClear=1`; } return res.redirect(`${WEBAPP_URL}${redirectPath}`); } const user = await prisma.user.findFirst({ where: { email: foundToken?.identifier, }, }); if (!user) { return res.status(401).json({ message: "Cannot find a user attached to this token" }); } const userMetadataParsed = userMetadata.parse(user.metadata); // Attach the new email and verify if (userMetadataParsed?.emailChangeWaitingForVerification) { // Ensure this email isn't in use const existingUser = await prisma.user.findUnique({ where: { email: userMetadataParsed?.emailChangeWaitingForVerification }, select: { id: true, }, }); if (existingUser) { return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE }); } // Ensure this email isn't being added by another user as secondary email const existingSecondaryUser = await prisma.secondaryEmail.findUnique({ where: { email: userMetadataParsed?.emailChangeWaitingForVerification, }, select: { id: true, userId: true, }, }); if (existingSecondaryUser && existingSecondaryUser.userId !== user.id) { return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE }); } const oldEmail = user.email; const updatedEmail = userMetadataParsed.emailChangeWaitingForVerification; delete userMetadataParsed.emailChangeWaitingForVerification; // Update and re-verify await prisma.user.update({ where: { id: user.id, }, data: { email: updatedEmail, metadata: userMetadataParsed, }, }); if (IS_STRIPE_ENABLED && userMetadataParsed.stripeCustomerId) { const billingService = getBillingProviderService(); await billingService.updateCustomer({ customerId: userMetadataParsed.stripeCustomerId, email: updatedEmail, }); } // The user is trying to update the email to an already existing unverified secondary email of his // so we swap the emails and its verified status if (existingSecondaryUser?.userId === user.id) { await prisma.secondaryEmail.update({ where: { id: existingSecondaryUser.id, userId: user.id, }, data: { email: oldEmail, emailVerified: user.emailVerified, }, }); } await cleanUpVerificationTokens(foundToken.id); return res.status(200).json({ updatedEmail, }); } await prisma.user.update({ where: { id: user.id, }, data: { emailVerified: new Date(), }, }); const hasCompletedOnboarding = user.completedOnboarding; await moveUserToMatchingOrg({ email: user.email }); const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(); return res.redirect(`${WEBAPP_URL}${hasCompletedOnboarding ? "/event-types" : gettingStartedPath}`); } export async function cleanUpVerificationTokens(id: number) { // Delete token from DB after it has been used await prisma.verificationToken.delete({ where: { id, }, }); }