diff --git a/apps/web/components/v2/settings/DisableTwoFactorModal.tsx b/apps/web/components/v2/settings/DisableTwoFactorModal.tsx index f8c5b12ac4..a183c8fa07 100644 --- a/apps/web/components/v2/settings/DisableTwoFactorModal.tsx +++ b/apps/web/components/v2/settings/DisableTwoFactorModal.tsx @@ -1,10 +1,14 @@ -import { SyntheticEvent, useState } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { ErrorCode } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/v2/core/Button"; import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog"; +import { Form, Label } from "@calcom/ui/v2/core/form/fields"; +import { PasswordField } from "@calcom/ui/v2/core/form/fields"; -import { ErrorCode } from "@lib/auth"; +import TwoFactor from "@components/auth/TwoFactor"; import TwoFactorAuthAPI from "./TwoFactorAuthAPI"; @@ -18,20 +22,24 @@ interface DisableTwoFactorAuthModalProps { onDisable: () => void; } +interface DisableTwoFactorValues { + totpCode: string; + password: string; +} + const DisableTwoFactorAuthModal = ({ onDisable, onCancel, open, onOpenChange, }: DisableTwoFactorAuthModalProps) => { - const [password, setPassword] = useState(""); const [isDisabling, setIsDisabling] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const { t } = useLocale(); - async function handleDisable(e: SyntheticEvent) { - e.preventDefault(); + const form = useForm(); + async function handleDisable({ totpCode, password }: DisableTwoFactorValues) { if (isDisabling) { return; } @@ -39,7 +47,7 @@ const DisableTwoFactorAuthModal = ({ setErrorMessage(null); try { - const response = await TwoFactorAuthAPI.disable(password); + const response = await TwoFactorAuthAPI.disable(password, totpCode); if (response.status === 200) { onDisable(); return; @@ -48,6 +56,12 @@ const DisableTwoFactorAuthModal = ({ const body = await response.json(); if (body.error === ErrorCode.IncorrectPassword) { setErrorMessage(t("incorrect_password")); + } + if (body.error === ErrorCode.SecondFactorRequired) { + setErrorMessage(t("2fa_required")); + } + if (body.error === ErrorCode.IncorrectTwoFactorCode) { + setErrorMessage(t("incorrect_2fa")); } else { setErrorMessage(t("something_went_wrong")); } @@ -66,39 +80,31 @@ const DisableTwoFactorAuthModal = ({ description={t("disable_2fa_recommendation")} type="creation" useOwnActionButtons> -
+
- -
- setPassword(e.currentTarget.value)} - className="block w-full rounded-sm border-gray-300 text-sm" - /> -
+ + + + {errorMessage &&

{errorMessage}

}
-
-
- - -
+
+ + +
+ ); diff --git a/apps/web/components/v2/settings/EnableTwoFactorModal.tsx b/apps/web/components/v2/settings/EnableTwoFactorModal.tsx index 55e0fd524d..674c57910a 100644 --- a/apps/web/components/v2/settings/EnableTwoFactorModal.tsx +++ b/apps/web/components/v2/settings/EnableTwoFactorModal.tsx @@ -1,10 +1,13 @@ -import React, { SyntheticEvent, useState } from "react"; +import React, { BaseSyntheticEvent, useState } from "react"; +import { useForm } from "react-hook-form"; +import { ErrorCode } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/v2/core/Button"; import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog"; +import { Form } from "@calcom/ui/v2/core/form/fields"; -import { ErrorCode } from "@lib/auth"; +import TwoFactor from "@components/auth/TwoFactor"; import TwoFactorAuthAPI from "./TwoFactorAuthAPI"; @@ -41,8 +44,14 @@ const WithStep = ({ return step === current ? children : null; }; +interface EnableTwoFactorValues { + totpCode: string; +} + const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: EnableTwoFactorModalProps) => { const { t } = useLocale(); + const form = useForm(); + const setupDescriptions = { [SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"), [SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"), @@ -50,13 +59,12 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable }; const [step, setStep] = useState(SetupStep.ConfirmPassword); const [password, setPassword] = useState(""); - const [totpCode, setTotpCode] = useState(""); const [dataUri, setDataUri] = useState(""); const [secret, setSecret] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - async function handleSetup(e: SyntheticEvent) { + async function handleSetup(e: React.FormEvent) { e.preventDefault(); if (isSubmitting) { @@ -90,10 +98,10 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable } } - async function handleEnable(e: SyntheticEvent) { - e.preventDefault(); + async function handleEnable({ totpCode }: EnableTwoFactorValues, e: BaseSyntheticEvent | undefined) { + e?.preventDefault(); - if (isSubmitting || totpCode.length !== 6) { + if (isSubmitting) { return; } @@ -128,11 +136,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable title={t("enable_2fa")} description={setupDescriptions[step]} type="creation" - useOwnActionButtons - // Icon={Icon.FiAlertTriangle}> - > - {/* */} - + useOwnActionButtons>
@@ -166,64 +170,42 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable

{secret}

- - + +
- -
- setTotpCode(e.currentTarget.value)} - className="block w-full rounded-sm border-gray-300 text-sm" - autoComplete="one-time-code" - /> -
+ {errorMessage &&

{errorMessage}

}
- -
- -
- - - - + + + + + + + + - - - - - -
+
+ ); diff --git a/apps/web/components/v2/settings/TwoFactorAuthAPI.ts b/apps/web/components/v2/settings/TwoFactorAuthAPI.ts index eb01d59c4c..35ef630575 100644 --- a/apps/web/components/v2/settings/TwoFactorAuthAPI.ts +++ b/apps/web/components/v2/settings/TwoFactorAuthAPI.ts @@ -19,10 +19,10 @@ const TwoFactorAuthAPI = { }); }, - async disable(password: string) { + async disable(password: string, code: string) { return fetch("/api/auth/two-factor/totp/disable", { method: "POST", - body: JSON.stringify({ password }), + body: JSON.stringify({ password, code }), headers: { "Content-Type": "application/json", }, diff --git a/apps/web/pages/v2/auth/login.tsx b/apps/web/pages/v2/auth/login.tsx index f93099e2ad..80c66a1dce 100644 --- a/apps/web/pages/v2/auth/login.tsx +++ b/apps/web/pages/v2/auth/login.tsx @@ -159,7 +159,7 @@ export default function Login({ - {twoFactorRequired && } + {twoFactorRequired && } {errorMessage && }
diff --git a/apps/web/pages/v2/settings/my-account/conferencing.tsx b/apps/web/pages/v2/settings/my-account/conferencing.tsx index 36ab05d2c9..9997689716 100644 --- a/apps/web/pages/v2/settings/my-account/conferencing.tsx +++ b/apps/web/pages/v2/settings/my-account/conferencing.tsx @@ -27,21 +27,21 @@ const ConferencingLayout = (props: inferSSRProps) => // }); return ( -
+
{apps.map((app) => (
+ className="flex flex-1 items-center space-x-3 border-b py-5 px-4 rtl:space-x-reverse"> {app.title} -
+

{app.title}

{app.description}

- + diff --git a/apps/web/pages/v2/settings/my-account/profile.tsx b/apps/web/pages/v2/settings/my-account/profile.tsx index bf2b8b049b..ef8ddc6fc7 100644 --- a/apps/web/pages/v2/settings/my-account/profile.tsx +++ b/apps/web/pages/v2/settings/my-account/profile.tsx @@ -1,29 +1,37 @@ import crypto from "crypto"; import { GetServerSidePropsContext } from "next"; import { signOut } from "next-auth/react"; -import { Trans } from "next-i18next"; -import { useRef, useState } from "react"; +import { useRef, useState, BaseSyntheticEvent } from "react"; import { Controller, useForm } from "react-hook-form"; +import { ErrorCode, getSession } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma from "@calcom/prisma"; +import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { trpc } from "@calcom/trpc/react"; +import { AppRouter } from "@calcom/trpc/server/routers/_app"; import { Icon } from "@calcom/ui"; +import { Alert } from "@calcom/ui/Alert"; import Avatar from "@calcom/ui/v2/core/Avatar"; import { Button } from "@calcom/ui/v2/core/Button"; import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/v2/core/Dialog"; import Meta from "@calcom/ui/v2/core/Meta"; -import { Form, Label, TextField } from "@calcom/ui/v2/core/form/fields"; +import { Form, Label, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields"; import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout"; import showToast from "@calcom/ui/v2/core/notifications"; -import { getSession } from "@lib/auth"; import { inferSSRProps } from "@lib/types/inferSSRProps"; +import TwoFactor from "@components/auth/TwoFactor"; import ImageUploader from "@components/v2/settings/ImageUploader"; +interface DeleteAccountValues { + totpCode: string; +} + const ProfileView = (props: inferSSRProps) => { const { t } = useLocale(); + const utils = trpc.useContext(); const { user } = props; // const { data: user, isLoading } = trpc.useQuery(["viewer.me"]); @@ -37,16 +45,16 @@ const ProfileView = (props: inferSSRProps) => { }); const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); + const [hasDeleteErrors, setHasDeleteErrors] = useState(false); + const [deleteErrorMessage, setDeleteErrorMessage] = useState(""); - const deleteAccount = async () => { - await fetch("/api/user/me", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }).catch((e) => { - console.error(`Error Removing user: ${user?.id}, email: ${user?.email} :`, e); - }); + const form = useForm(); + + const onDeleteMeSuccessMutation = async () => { + await utils.invalidateQueries(["viewer.me"]); + showToast(t("Your account was deleted"), "success"); + + setHasDeleteErrors(false); // dismiss any open errors if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") { signOut({ callbackUrl: "/auth/logout?survey=true" }); } else { @@ -54,6 +62,30 @@ const ProfileView = (props: inferSSRProps) => { } }; + const onDeleteMeErrorMutation = (error: TRPCClientErrorLike) => { + setHasDeleteErrors(true); + setDeleteErrorMessage(errorMessages[error.message]); + }; + const deleteMeMutation = trpc.useMutation("viewer.deleteMe", { + onSuccess: onDeleteMeSuccessMutation, + onError: onDeleteMeErrorMutation, + async onSettled() { + await utils.invalidateQueries(["viewer.me"]); + }, + }); + + const onConfirmButton = (e: Event | React.MouseEvent) => { + e.preventDefault(); + const totpCode = form.getValues("totpCode"); + const password = passwordRef.current.value; + deleteMeMutation.mutate({ password, totpCode }); + }; + const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => { + e?.preventDefault(); + const password = passwordRef.current.value; + deleteMeMutation.mutate({ password, totpCode }); + }; + const formMethods = useForm({ defaultValues: { avatar: user.avatar || "", @@ -63,7 +95,16 @@ const ProfileView = (props: inferSSRProps) => { }, }); - const avatarRef = useRef(null!); + const passwordRef = useRef(null!); + + const errorMessages: { [key: string]: string } = { + [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), + [ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`, + [ErrorCode.UserNotFound]: t("no_account_exists"), + [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`, + [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, + [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), + }; return ( <> @@ -163,17 +204,31 @@ const ProfileView = (props: inferSSRProps) => { deleteAccount()}> - {/* Use trans component for translation */} -

- - Anyone who you have shared your account link with will no longer be able to book using it and - any preferences you have saved will be lost - -

+ actionOnClick={(e) => e && onConfirmButton(e)}> + <> +

{t("delete_account_confirmation_message")}

+ + + {user.twoFactorEnabled && ( +
+ + + )} + + {hasDeleteErrors && } +
@@ -203,6 +258,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => name: true, bio: true, avatar: true, + twoFactorEnabled: true, }, }); diff --git a/apps/web/pages/v2/settings/security/password.tsx b/apps/web/pages/v2/settings/security/password.tsx index b09bfe42bc..651eb88bdc 100644 --- a/apps/web/pages/v2/settings/security/password.tsx +++ b/apps/web/pages/v2/settings/security/password.tsx @@ -2,6 +2,7 @@ import { IdentityProvider } from "@prisma/client"; import { Trans } from "next-i18next"; import { Controller, useForm } from "react-hook-form"; +import { identityProviderNameMap } from "@calcom/lib/auth"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Button } from "@calcom/ui/v2/core/Button"; @@ -10,15 +11,13 @@ import { Form, TextField } from "@calcom/ui/v2/core/form/fields"; import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout"; import showToast from "@calcom/ui/v2/core/notifications"; -import { identityProviderNameMap } from "@lib/auth"; - const PasswordView = () => { const { t } = useLocale(); const { data: user } = trpc.useQuery(["viewer.me"]); const mutation = trpc.useMutation("viewer.auth.changePassword", { onSuccess: () => { - showToast(t("password_updated_successfully"), "success"); + showToast(t("password_has_been_changed"), "success"); }, onError: (error) => { showToast(`${t("error_updating_password")}, ${error.message}`, "error"); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index bcab668c57..d3bb515442 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1119,7 +1119,23 @@ "two_factor_auth": "Two factor authentication", "recurring_event_tab_description": "Set up a repeating schedule", "today": "today", - "active": "active", + "appearance": "Appearance", + "appearance_subtitle": "Manage settings for your booking appearance", + "my_account": "My account", + "general": "General", + "calendars": "Calendars", + "2fa_auth": "Two factor auth", + "invoices": "Invoices", + "embeds": "Embeds", + "impersonation": "Impersonation", + "users": "Users", + "profile_description": "Manage settings for your cal profile", + "general_description": "Manage settings for your language and timezone", + "calendars_description": "Configure how your event types interact with your calendars", + "appearance_description": "Manage settings for your booking appearance", + "conferencing_description": "Manage your video conferencing apps for your meetings", + "password_description": "Manage settings for your account passwords", + "2fa_description": "Manage settings for your account passwords", "add_variable": "Add variable", "custom_phone_number": "Custom phone number", "message_template": "Message template", diff --git a/packages/ui/v2/core/Dialog.tsx b/packages/ui/v2/core/Dialog.tsx index 012644257e..b9a05c6e21 100644 --- a/packages/ui/v2/core/Dialog.tsx +++ b/packages/ui/v2/core/Dialog.tsx @@ -71,7 +71,7 @@ type DialogContentProps = React.ComponentProps void; + actionOnClick?: (e: Event | React.MouseEvent) => void; actionOnClose?: () => void; }; diff --git a/packages/ui/v2/core/Shell.tsx b/packages/ui/v2/core/Shell.tsx index 8eb538ec7b..d1f99ae81a 100644 --- a/packages/ui/v2/core/Shell.tsx +++ b/packages/ui/v2/core/Shell.tsx @@ -7,7 +7,6 @@ import { Toaster } from "react-hot-toast"; import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; -import LicenseBanner from "@calcom/features/ee/common/components/LicenseBanner"; import TrialBanner from "@calcom/features/ee/common/components/TrialBanner"; import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; @@ -120,6 +119,7 @@ export function ShellSubHeading(props: { const Layout = (props: LayoutProps) => { const pageTitle = typeof props.heading === "string" ? props.heading : props.title; + const router = useRouter(); return ( <> @@ -135,8 +135,8 @@ const Layout = (props: LayoutProps) => {
-
- {props.SidebarContainer || } +
+ {router.route.startsWith("/v2/settings/") ? <> : }
@@ -514,7 +514,9 @@ function MobileNavigationContainer() { } const MobileNavigation = () => { + const router = useRouter(); const isEmbed = useIsEmbed(); + if (router.route.startsWith("/v2/settings/")) return null; return ( <>