diff --git a/apps/web/pages/api/auth/reset-password.ts b/apps/web/pages/api/auth/reset-password.ts
index 57466aa203..e2d1109c68 100644
--- a/apps/web/pages/api/auth/reset-password.ts
+++ b/apps/web/pages/api/auth/reset-password.ts
@@ -42,6 +42,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
data: {
password: hashedPassword,
+ emailVerified: new Date(),
},
});
} catch (e) {
diff --git a/apps/web/pages/api/auth/verify-email.ts b/apps/web/pages/api/auth/verify-email.ts
index 3890cdd6a7..1ebe572d72 100644
--- a/apps/web/pages/api/auth/verify-email.ts
+++ b/apps/web/pages/api/auth/verify-email.ts
@@ -4,6 +4,7 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
+import { userMetadata } from "@calcom/prisma/zod-utils";
const verifySchema = z.object({
token: z.string(),
@@ -26,23 +27,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Token expired" });
}
- const user = await prisma.user.update({
+ 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 isnt in use
+ const existingUser = await prisma.user.findUnique({
+ where: { email: userMetadataParsed?.emailChangeWaitingForVerification },
+ select: {
+ id: true,
+ },
+ });
+
+ if (existingUser) {
+ return res.status(401).json({ message: "A User already exists with this 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,
+ },
+ });
+
+ 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;
+
+ return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
+}
+
+async function cleanUpVerificationTokens(id: number) {
// Delete token from DB after it has been used
await prisma.verificationToken.delete({
where: {
- id: foundToken?.id,
+ id,
},
});
-
- const hasCompletedOnboarding = user.completedOnboarding;
-
- res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
}
diff --git a/apps/web/pages/auth/verify-email-change.tsx b/apps/web/pages/auth/verify-email-change.tsx
new file mode 100644
index 0000000000..e5b9d49df7
--- /dev/null
+++ b/apps/web/pages/auth/verify-email-change.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import type { GetServerSidePropsContext } from "next";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { Toaster } from "react-hot-toast";
+import { z } from "zod";
+
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { showToast } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+interface PageProps {
+ token: string;
+ updateSession: string;
+ updatedEmail: string;
+}
+
+function VerifyEmailChange(props: PageProps) {
+ const { update } = useSession();
+ const { t, isLocaleReady } = useLocale();
+ const router = useRouter();
+
+ useEffect(() => {
+ async function updateSession() {
+ await update({ email: props.updatedEmail });
+ router.push("/event-types");
+ }
+ if (props.updateSession) {
+ updateSession();
+ if (isLocaleReady) {
+ showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success");
+ }
+ } else {
+ if (isLocaleReady) {
+ showToast(t("verify_email_change_failure_toast"), "error");
+ }
+ }
+ // We only need this to run on inital mount. These props can't and won't change due to it being fetched serveside.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ );
+}
+
+const tokenSchema = z.object({
+ token: z.string(),
+});
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { token } = tokenSchema.parse(context.query);
+
+ if (!token) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const params = new URLSearchParams({
+ token,
+ });
+
+ const response = await fetch(`${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`, {
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ return {
+ props: {
+ updateSession: false,
+ token,
+ updatedEmail: false,
+ },
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ props: {
+ updateSession: true,
+ token,
+ updatedEmail: data.updatedEmail ?? null,
+ },
+ };
+}
+
+export default VerifyEmailChange;
+VerifyEmailChange.PageWrapper = PageWrapper;
diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx
index 986a2d5979..0a95da3305 100644
--- a/apps/web/pages/settings/my-account/profile.tsx
+++ b/apps/web/pages/settings/my-account/profile.tsx
@@ -93,8 +93,6 @@ const ProfileView = () => {
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => {
await update(res);
- showToast(t("settings_updated_successfully"), "success");
-
// signout user only in case of password reset
if (res.signOutUser && tempFormValues && res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
@@ -105,6 +103,12 @@ const ProfileView = () => {
utils.viewer.shouldVerifyEmail.invalidate();
}
+ if (res.hasEmailBeenChanged && res.sendEmailVerification) {
+ showToast(t("change_of_email_toast", { email: tempFormValues?.email }), "success");
+ } else {
+ showToast(t("settings_updated_successfully"), "success");
+ }
+
setConfirmAuthEmailChangeWarningDialogOpen(false);
setTempFormValues(null);
},
@@ -342,6 +346,20 @@ const ProfileView = () => {
type="creation"
Icon={AlertTriangle}>
+
+
+
+ {t("old_email_address")}
+
+
{user.email}
+
+
+
+ {t("new_email_address")}
+
+
{tempFormValues?.email}
+
+
{