From cf32287109aaeeee0fe2ee0ff2c73c5bbee71434 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Sat, 17 Sep 2022 18:27:25 -0400 Subject: [PATCH] Add password on confirmation when changing email (#4503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add email confirmation * Confirm password only if email was changed Co-authored-by: Omar López --- .../pages/v2/settings/my-account/profile.tsx | 50 +++++++- apps/web/public/static/locales/en/common.json | 1 + packages/trpc/server/routers/viewer/auth.tsx | 113 +++++++++++------- packages/ui/v2/core/form/fields.tsx | 2 +- 4 files changed, 120 insertions(+), 46 deletions(-) diff --git a/apps/web/pages/v2/settings/my-account/profile.tsx b/apps/web/pages/v2/settings/my-account/profile.tsx index f3a1fb8a8b..17662f3228 100644 --- a/apps/web/pages/v2/settings/my-account/profile.tsx +++ b/apps/web/pages/v2/settings/my-account/profile.tsx @@ -59,6 +59,8 @@ const ProfileView = () => { }, }); + const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false); + const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState(""); const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); const [hasDeleteErrors, setHasDeleteErrors] = useState(false); const [deleteErrorMessage, setDeleteErrorMessage] = useState(""); @@ -82,6 +84,16 @@ const ProfileView = () => { } }; + const confirmPasswordMutation = trpc.useMutation("viewer.auth.verifyPassword", { + onSuccess() { + mutation.mutate(formMethods.getValues()); + setConfirmPasswordOpen(false); + }, + onError() { + setConfirmPasswordDeleteErrorMessage(t("incorrect_password")); + }, + }); + const onDeleteMeErrorMutation = (error: TRPCClientErrorLike) => { setHasDeleteErrors(true); setDeleteErrorMessage(errorMessages[error.message]); @@ -94,6 +106,12 @@ const ProfileView = () => { }, }); + const onConfirmPassword = (e: Event | React.MouseEvent) => { + e.preventDefault(); + const password = passwordRef.current.value; + confirmPasswordMutation.mutate({ passwordInput: password }); + }; + const onConfirmButton = (e: Event | React.MouseEvent) => { e.preventDefault(); const totpCode = form.getValues("totpCode"); @@ -135,7 +153,11 @@ const ProfileView = () => {
{ - mutation.mutate(values); + if (values.email !== user?.email) { + setConfirmPasswordOpen(true); + } else { + mutation.mutate(values); + } }}>
@@ -231,6 +253,32 @@ const ProfileView = () => { + + {/* If changing email, confirm password */} + + e && onConfirmPassword(e)}> + <> + + + {confirmPasswordErrorMessage && } + + + ); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index dfc6c13c1d..c5ff45c37a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1266,5 +1266,6 @@ "no_calendar_installed_description": "You have not yet connected any of your calendars", "add_a_calendar": "Add a calendar", "change_email_hint": "You may need to log out and back in to see any change take effect", + "confirm_password_change_email": "Please confirm your password before changing your email address", "seats": "seats" } diff --git a/packages/trpc/server/routers/viewer/auth.tsx b/packages/trpc/server/routers/viewer/auth.tsx index e8959d4360..ac8c0a6350 100644 --- a/packages/trpc/server/routers/viewer/auth.tsx +++ b/packages/trpc/server/routers/viewer/auth.tsx @@ -8,56 +8,81 @@ import { TRPCError } from "@trpc/server"; import { createProtectedRouter } from "../../createRouter"; -export const authRouter = createProtectedRouter().mutation("changePassword", { - input: z.object({ - oldPassword: z.string(), - newPassword: z.string(), - }), - async resolve({ input, ctx }) { - const { oldPassword, newPassword } = input; +export const authRouter = createProtectedRouter() + .mutation("changePassword", { + input: z.object({ + oldPassword: z.string(), + newPassword: z.string(), + }), + async resolve({ input, ctx }) { + const { oldPassword, newPassword } = input; - const { user } = ctx; + const { user } = ctx; - if (user.identityProvider !== IdentityProvider.CAL) { - throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" }); - } + if (user.identityProvider !== IdentityProvider.CAL) { + throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" }); + } - const currentPasswordQuery = await prisma.user.findFirst({ - where: { - id: user.id, - }, - select: { - password: true, - }, - }); + const currentPasswordQuery = await prisma.user.findFirst({ + where: { + id: user.id, + }, + select: { + password: true, + }, + }); - const currentPassword = currentPasswordQuery?.password; + const currentPassword = currentPasswordQuery?.password; - if (!currentPassword) { - throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" }); - } + if (!currentPassword) { + throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" }); + } - const passwordsMatch = await verifyPassword(oldPassword, currentPassword); - if (!passwordsMatch) { - throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" }); - } + const passwordsMatch = await verifyPassword(oldPassword, currentPassword); + if (!passwordsMatch) { + throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" }); + } - if (oldPassword === newPassword) { - throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" }); - } + if (oldPassword === newPassword) { + throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" }); + } - if (!validPassword(newPassword)) { - throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" }); - } + if (!validPassword(newPassword)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" }); + } - const hashedPassword = await hashPassword(newPassword); - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - password: hashedPassword, - }, - }); - }, -}); + const hashedPassword = await hashPassword(newPassword); + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + password: hashedPassword, + }, + }); + }, + }) + .mutation("verifyPassword", { + input: z.object({ + passwordInput: z.string(), + }), + async resolve({ input, ctx }) { + const user = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + }); + + if (!user?.password) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + + const passwordsMatch = await verifyPassword(input.passwordInput, user.password); + + if (!passwordsMatch) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return; + }, + }); diff --git a/packages/ui/v2/core/form/fields.tsx b/packages/ui/v2/core/form/fields.tsx index ced5361218..dae41a3268 100644 --- a/packages/ui/v2/core/form/fields.tsx +++ b/packages/ui/v2/core/form/fields.tsx @@ -269,7 +269,7 @@ export const PasswordField = forwardRef(funct