From 516bdc4d2bf9713a8b18d9c795b9a2efcbf27cb2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 25 Feb 2023 17:49:11 +0100 Subject: [PATCH 01/10] refactor: Improve settings code --- public/locales/en-US/translation.json | 4 ++++ src/App.tsx | 5 +++++ .../AliasDetailRoute/AliasPreferencesForm.tsx | 2 +- .../SelectField.tsx | 0 .../SettingsAliasPreferencesRoute.tsx} | 2 +- src/routes/SettingsRoute.tsx | 21 +++++++++++++++++-- 6 files changed, 30 insertions(+), 4 deletions(-) rename src/route-widgets/{SettingsRoute => AliasDetailRoute}/SelectField.tsx (100%) rename src/{route-widgets/SettingsRoute/AliasesPreferencesForm.tsx => routes/SettingsAliasPreferencesRoute.tsx} (99%) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index fcb479e..cadb80d 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -284,6 +284,10 @@ "description": "Select default values for your aliases. This only affects aliases you haven't set a custom value for.", "saveAction": "Save preferences" } + }, + "actions": { + "enable2fa": "2-Factor-Authentication", + "aliasPreferences": "Alias Preferences" } }, "LogoutRoute": { diff --git a/src/App.tsx b/src/App.tsx index b58d2d1..208316b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ import ReportsRoute from "~/routes/ReportsRoute" import ReservedAliasDetailRoute from "~/routes/ReservedAliasDetailRoute" import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute" import RootRoute from "~/routes/Root" +import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute" import SettingsRoute from "~/routes/SettingsRoute" import SignupRoute from "~/routes/SignupRoute" import VerifyEmailRoute from "~/routes/VerifyEmailRoute" @@ -91,6 +92,10 @@ const router = createBrowserRouter([ path: "/settings", element: , }, + { + path: "/settings/alias-preferences", + element: , + }, { path: "/reports", loader: getServerSettings, diff --git a/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx b/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx index 2b36c71..e08ca66 100644 --- a/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx +++ b/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx @@ -21,7 +21,7 @@ import { import {useErrorSuccessSnacks} from "~/hooks" import {queryClient} from "~/constants/react-query" import {AuthContext, FormikAutoLockNavigation} from "~/components" -import SelectField from "~/route-widgets/SettingsRoute/SelectField" +import SelectField from "~/route-widgets/AliasDetailRoute/SelectField" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" export interface AliasPreferencesFormProps { diff --git a/src/route-widgets/SettingsRoute/SelectField.tsx b/src/route-widgets/AliasDetailRoute/SelectField.tsx similarity index 100% rename from src/route-widgets/SettingsRoute/SelectField.tsx rename to src/route-widgets/AliasDetailRoute/SelectField.tsx diff --git a/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx b/src/routes/SettingsAliasPreferencesRoute.tsx similarity index 99% rename from src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx rename to src/routes/SettingsAliasPreferencesRoute.tsx index 1751b70..b32d20f 100644 --- a/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx +++ b/src/routes/SettingsAliasPreferencesRoute.tsx @@ -44,7 +44,7 @@ interface Form { detail?: string } -export default function AliasesPreferencesForm(): ReactElement { +export default function SettingsAliasPreferencesRoute(): ReactElement { const {_updateUser} = useContext(AuthContext) const user = useUser() const {showError, showSuccess} = useErrorSuccessSnacks() diff --git a/src/routes/SettingsRoute.tsx b/src/routes/SettingsRoute.tsx index 65b6543..f3e7b27 100644 --- a/src/routes/SettingsRoute.tsx +++ b/src/routes/SettingsRoute.tsx @@ -1,15 +1,32 @@ import {useTranslation} from "react-i18next" +import {GoSettings} from "react-icons/go" +import {BsShieldLockFill} from "react-icons/bs" +import {Link} from "react-router-dom" import React, {ReactElement} from "react" +import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material" + import {SimplePageBuilder} from "~/components" -import AliasesPreferencesForm from "~/route-widgets/SettingsRoute/AliasesPreferencesForm" export default function SettingsRoute(): ReactElement { const {t} = useTranslation() return ( - + + + + + + + + + + + + + + ) } From 31288c9e79ae4c24c8201d63238daf51c57a4954 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 25 Feb 2023 21:47:38 +0100 Subject: [PATCH 02/10] feat: Add get has 2fa enabled --- public/locales/en-US/translation.json | 6 ++++- src/App.tsx | 5 +++++ src/apis/get-has-2fa-enabled.ts | 9 ++++++++ src/routes/Settings2FARoute.tsx | 32 +++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/apis/get-has-2fa-enabled.ts create mode 100644 src/routes/Settings2FARoute.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index cadb80d..e7d8963 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -286,8 +286,12 @@ } }, "actions": { - "enable2fa": "2-Factor-Authentication", + "enable2fa": "Two-Factor-Authentication", "aliasPreferences": "Alias Preferences" + }, + "2fa": { + "title": "Two-Factor-Authentication", + "alreadyEnabled": "You have successfully enabled 2FA!" } }, "LogoutRoute": { diff --git a/src/App.tsx b/src/App.tsx index 208316b..dcdef42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ import ReportsRoute from "~/routes/ReportsRoute" import ReservedAliasDetailRoute from "~/routes/ReservedAliasDetailRoute" import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute" import RootRoute from "~/routes/Root" +import Settings2FARoute from "~/routes/Settings2FARoute" import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute" import SettingsRoute from "~/routes/SettingsRoute" import SignupRoute from "~/routes/SignupRoute" @@ -96,6 +97,10 @@ const router = createBrowserRouter([ path: "/settings/alias-preferences", element: , }, + { + path: "/settings/2fa", + element: , + }, { path: "/reports", loader: getServerSettings, diff --git a/src/apis/get-has-2fa-enabled.ts b/src/apis/get-has-2fa-enabled.ts new file mode 100644 index 0000000..80063b2 --- /dev/null +++ b/src/apis/get-has-2fa-enabled.ts @@ -0,0 +1,9 @@ +import {client} from "~/constants/axios-client" + +export default async function getHas2FAEnabled(): Promise { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/setup-otp/`, { + withCredentials: true, + }) + + return data.enabled +} diff --git a/src/routes/Settings2FARoute.tsx b/src/routes/Settings2FARoute.tsx new file mode 100644 index 0000000..0f7e9c9 --- /dev/null +++ b/src/routes/Settings2FARoute.tsx @@ -0,0 +1,32 @@ +import {ReactElement} from "react" +import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" + +import {useQuery} from "@tanstack/react-query" +import {Alert} from "@mui/material" + +import {QueryResult, SimplePageBuilder} from "~/components" +import getHas2FAEnabled from "~/apis/get-has-2fa-enabled" + +export default function Settings2FARoute(): ReactElement { + const {t} = useTranslation() + const query = useQuery(["get_2fa_enabled"], getHas2FAEnabled) + + return ( + + query={query}> + {has2FAEnabled => + has2FAEnabled ? ( + <> + + {t("routes.SettingsRoute.2fa.alreadyEnabled")} + + + ) : ( + <> + ) + } + + + ) +} From 564861fc9ff5a2b49f9f7e09f25077a569b9452f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 11:22:35 +0100 Subject: [PATCH 03/10] feat: Add setup 2FA --- package.json | 2 +- public/locales/en-US/translation.json | 18 +- src/App.tsx | 3 +- src/apis/index.ts | 6 + src/apis/setup-2fa-verify.ts | 19 +++ src/apis/setup-2fa.ts | 18 ++ src/hooks/use-error-success-snacks.ts | 14 +- .../Settings2FARoute/Setup2FA.tsx | 64 +++++++ .../Settings2FARoute/VerifyOTPForm.tsx | 158 ++++++++++++++++++ src/routes/Settings2FARoute.tsx | 6 +- yarn.lock | 18 +- 11 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 src/apis/setup-2fa-verify.ts create mode 100644 src/apis/setup-2fa.ts create mode 100644 src/route-widgets/Settings2FARoute/Setup2FA.tsx create mode 100644 src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx diff --git a/package.json b/package.json index 9044539..45423c9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-dom": "^18.2.0", "react-i18next": "^12.0.0", "react-icons": "^4.4.0", + "react-qr-code": "^2.0.11", "react-router-dom": "^6.4.2", "react-use": "^17.4.0", "secure-random-password": "^0.2.3", @@ -61,7 +62,6 @@ "@types/deep-equal": "^1.0.1", "@types/jest": "^29.2.4", "@types/lodash": "^4.14.191", - "@types/node": "^18.14.0", "@types/openpgp": "^4.4.18", "@types/react-dom": "^18.0.11", "@types/react-icons": "^3.0.0", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index e7d8963..9bce3e8 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -291,7 +291,23 @@ }, "2fa": { "title": "Two-Factor-Authentication", - "alreadyEnabled": "You have successfully enabled 2FA!" + "alreadyEnabled": "You have successfully enabled 2FA!", + "setup": { + "description": "Enable 2FA to add an extra layer of security to your account. Each time you log in, you will need to enter a code generated from your authenticator app. This makes it harder for an attacker to hack into your account as they would need to have access to your phone.", + "setupLabel": "Enable 2FA", + "code": { + "label": "Code", + "description": "Enter the code generated by your authenticator app.", + "onlyDigits": "The code can only contain digits." + }, + "submit": "Enable 2FA", + "expired": "The verification time for your current Two-Factor-Authentication code has expired. A new code has been generated.", + "recoveryCodes": { + "title": "Note down your recovery codes", + "description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.", + "submit": "I have noted down my recovery codes" + } + } } }, "LogoutRoute": { diff --git a/src/App.tsx b/src/App.tsx index dcdef42..79c00e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,8 @@ import {RouterProvider, createBrowserRouter} from "react-router-dom" import {SnackbarProvider} from "notistack" -import React, {ReactElement} from "react" - import {QueryClientProvider} from "@tanstack/react-query" import {CssBaseline, Theme, ThemeProvider} from "@mui/material" +import React, {ReactElement} from "react" import {queryClient} from "~/constants/react-query" import {getServerSettings} from "~/apis" diff --git a/src/apis/index.ts b/src/apis/index.ts index fbb1e32..1a76ae7 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -58,3 +58,9 @@ export * from "./delete-reserved-alias" export {default as deleteReservedAlias} from "./delete-reserved-alias" export * from "./get-latest-cron-report" export {default as getLatestCronReport} from "./get-latest-cron-report" +export * from "./get-has-2fa-enabled" +export {default as getHas2FAEnabled} from "./get-has-2fa-enabled" +export * from "./setup-2fa" +export {default as setup2FA} from "./setup-2fa" +export * from "./setup-2fa-verify" +export {default as verify2FASetup} from "./setup-2fa-verify" diff --git a/src/apis/setup-2fa-verify.ts b/src/apis/setup-2fa-verify.ts new file mode 100644 index 0000000..372c13a --- /dev/null +++ b/src/apis/setup-2fa-verify.ts @@ -0,0 +1,19 @@ +import {client} from "~/constants/axios-client" + +export interface Verify2FASetupData { + code: string +} + +export default async function verify2FASetup({code}: Verify2FASetupData): Promise { + const {data} = await client.post( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/setup-otp/verify`, + { + code, + }, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/setup-2fa.ts b/src/apis/setup-2fa.ts new file mode 100644 index 0000000..4d3f03b --- /dev/null +++ b/src/apis/setup-2fa.ts @@ -0,0 +1,18 @@ +import {client} from "~/constants/axios-client" + +export interface Setup2FAResponse { + secret: string + recoveryCodes: string[] +} + +export default async function setup2FA(): Promise { + const {data} = await client.post( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/setup-otp/`, + {}, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/hooks/use-error-success-snacks.ts b/src/hooks/use-error-success-snacks.ts index 009b244..8656c73 100644 --- a/src/hooks/use-error-success-snacks.ts +++ b/src/hooks/use-error-success-snacks.ts @@ -8,7 +8,7 @@ import {parseFastAPIError} from "~/utils" export interface UseErrorSuccessSnacksResult { showSuccess: (message: string) => void - showError: (error: Error) => void + showError: (error: Error | string) => void } export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult { @@ -27,14 +27,16 @@ export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult { autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION, }) } - const showError = (error: Error) => { + const showError = (error: Error | string) => { let message - try { - const parsedError = parseFastAPIError(error as AxiosError) + if (typeof error !== "string") { + try { + const parsedError = parseFastAPIError(error as AxiosError) - message = parsedError.detail - } catch (e) {} + message = parsedError.detail + } catch (e) {} + } $errorSnackbarKey.current = enqueueSnackbar(message || t("general.defaultError"), { variant: "error", diff --git a/src/route-widgets/Settings2FARoute/Setup2FA.tsx b/src/route-widgets/Settings2FARoute/Setup2FA.tsx new file mode 100644 index 0000000..327e772 --- /dev/null +++ b/src/route-widgets/Settings2FARoute/Setup2FA.tsx @@ -0,0 +1,64 @@ +import {ReactElement} from "react" +import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {BsShieldLockFill} from "react-icons/bs" +// @ts-ignore +import {authenticator} from "@otplib/preset-browser" +import {useMutation} from "@tanstack/react-query" +import {LoadingButton} from "@mui/lab" +import {Grid, Typography} from "@mui/material" + +import {Setup2FAResponse, setup2FA} from "~/apis" +import {useErrorSuccessSnacks} from "~/hooks" + +import VerifyOTPForm from "./VerifyOTPForm" + +export interface Setup2FAProps { + onSuccess: () => void +} + +export default function Setup2FA({onSuccess}: Setup2FAProps): ReactElement { + const {t} = useTranslation() + const {showError} = useErrorSuccessSnacks() + + const { + data: {secret, recoveryCodes} = {}, + mutate, + isLoading, + reset, + } = useMutation(setup2FA, { + onError: showError, + }) + + return ( + + + + {t("routes.SettingsRoute.2fa.setup.description")} + + + + {secret ? ( + { + reset() + mutate() + }} + secret={secret} + recoveryCodes={recoveryCodes!} + onSuccess={onSuccess} + /> + ) : ( + mutate()} + loading={isLoading} + variant="contained" + startIcon={} + > + {t("routes.SettingsRoute.2fa.setup.setupLabel")} + + )} + + + ) +} diff --git a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx new file mode 100644 index 0000000..7e59df3 --- /dev/null +++ b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx @@ -0,0 +1,158 @@ +import * as yup from "yup" +import {ReactElement, useState} from "react" +import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {useFormik} from "formik" +import {BsShieldLockFill} from "react-icons/bs" +import QRCode from "react-qr-code" + +import {useMutation} from "@tanstack/react-query" +import {LoadingButton} from "@mui/lab" + +import {Verify2FASetupData, verify2FASetup} from "~/apis" +import {useErrorSuccessSnacks, useUser} from "~/hooks" +import { + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + InputAdornment, + TextField, +} from "@mui/material" +import {parseFastAPIError} from "~/utils" + +export interface VerifyOTPFormProps { + onSuccess: () => void + onRecreateRequired: () => void + secret: string + recoveryCodes: string[] +} + +const generateOTPAuthUri = (secret: string, email: string): string => + `otpauth://totp/KleckRelay:${email}?secret=${secret}&issuer=KleckRelay` + +export default function Settings2FARoute({ + onSuccess, + recoveryCodes, + onRecreateRequired, + secret, +}: VerifyOTPFormProps): ReactElement { + const {t} = useTranslation() + const {showError} = useErrorSuccessSnacks() + const user = useUser() + const theme = useTheme() + + const [showRecoveryCodes, setShowRecoveryCodes] = useState(false) + + const schema = yup.object().shape({ + code: yup + .string() + .required() + .length(6) + .matches(/^[0-9]+$/, t("routes.SettingsRoute.2fa.setup.code.onlyDigits").toString()) + .label(t("routes.SettingsRoute.2fa.setup.code.label")), + }) + + const {mutateAsync} = useMutation(verify2FASetup, { + onSuccess: () => setShowRecoveryCodes(true), + onError: error => { + if (error.response?.status === 409 || error.response?.status === 410) { + showError(t("routes.SettingsRoute.2fa.setup.expired").toString()) + onRecreateRequired() + } else { + showError(error) + } + }, + }) + const formik = useFormik({ + initialValues: { + code: "", + }, + validationSchema: schema, + onSubmit: async (values, {setErrors}) => { + try { + schema.validateSync(values) + await mutateAsync(values) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, + }) + + return ( + <> +
+ + +
+ +
+
+ + + + + + + ), + }} + /> + + + + {t("routes.SettingsRoute.2fa.setup.submit")} + + + + +
+
+ setShowRecoveryCodes(false)}> + {t("routes.SettingsRoute.2fa.setup.recoveryCodes.title")} + + + {recoveryCodes.map(code => ( +

{code}

+ ))} +
+ + {t("routes.SettingsRoute.2fa.setup.recoveryCodes.description")} + +
+ + + +
+ + ) +} diff --git a/src/routes/Settings2FARoute.tsx b/src/routes/Settings2FARoute.tsx index 0f7e9c9..8595c14 100644 --- a/src/routes/Settings2FARoute.tsx +++ b/src/routes/Settings2FARoute.tsx @@ -6,11 +6,13 @@ import {useQuery} from "@tanstack/react-query" import {Alert} from "@mui/material" import {QueryResult, SimplePageBuilder} from "~/components" +import Setup2FA from "~/route-widgets/Settings2FARoute/Setup2FA" import getHas2FAEnabled from "~/apis/get-has-2fa-enabled" export default function Settings2FARoute(): ReactElement { const {t} = useTranslation() - const query = useQuery(["get_2fa_enabled"], getHas2FAEnabled) + const queryKey = ["get_2fa_enabled"] + const query = useQuery(queryKey, getHas2FAEnabled) return ( @@ -23,7 +25,7 @@ export default function Settings2FARoute(): ReactElement { ) : ( - <> + ) } diff --git a/yarn.lock b/yarn.lock index cc78031..4d7337a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1406,11 +1406,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.5.tgz#6a31f820c1077c3f8ce44f9e203e68a176e8f59e" integrity sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q== -"@types/node@^18.14.0": - version "18.14.0" - resolved "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0" - integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A== - "@types/openpgp@^4.4.18": version "4.4.18" resolved "https://registry.yarnpkg.com/@types/openpgp/-/openpgp-4.4.18.tgz#b523e7a97646069756a01faf0569e198fe4d0dc1" @@ -4643,6 +4638,11 @@ pvutils@^1.1.3: resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== +qr.js@0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" + integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -4699,6 +4699,14 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-qr-code@^2.0.11: + version "2.0.11" + resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.11.tgz#444c759a2100424972e17135fbe0e32eaffa19e8" + integrity sha512-P7mvVM5vk9NjGdHMt4Z0KWeeJYwRAtonHTghZT2r+AASinLUUKQ9wfsGH2lPKsT++gps7hXmaiMGRvwTDEL9OA== + dependencies: + prop-types "^15.8.1" + qr.js "0.0.0" + react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" From 7ec6b81b5e0a111bf9026494837866ab5855ebe3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 11:53:08 +0100 Subject: [PATCH 04/10] feat: Add disable 2FA --- public/locales/en-US/translation.json | 14 +++ src/apis/delete-2fa.ts | 22 ++++ src/apis/index.ts | 2 + .../Settings2FARoute/Delete2FA.tsx | 105 ++++++++++++++++++ src/routes/Settings2FARoute.tsx | 18 ++- 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 src/apis/delete-2fa.ts create mode 100644 src/route-widgets/Settings2FARoute/Delete2FA.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 9bce3e8..bf3a3fa 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -307,6 +307,20 @@ "description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.", "submit": "I have noted down my recovery codes" } + }, + "delete": { + "showAction": "Disable 2FA", + "askType": { + "code": "I have my 2FA code", + "recoveryCode": "I have a recovery code" + }, + "askCode": { + "label": "Code" + }, + "askRecoveryCode": { + "label": "Recovery Code" + }, + "submit": "Disable 2FA" } } }, diff --git a/src/apis/delete-2fa.ts b/src/apis/delete-2fa.ts new file mode 100644 index 0000000..cf98b2b --- /dev/null +++ b/src/apis/delete-2fa.ts @@ -0,0 +1,22 @@ +import {SimpleDetailResponse} from "~/server-types" +import {client} from "~/constants/axios-client" + +export interface Delete2FAData { + code?: string + recoveryCode?: string +} + +export default async function delete2FA({ + recoveryCode, + code, +}: Delete2FAData): Promise { + const {data} = await client.delete(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/setup-otp`, { + withCredentials: true, + data: { + code, + recoveryCode, + }, + }) + + return data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index 1a76ae7..b131e41 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -64,3 +64,5 @@ export * from "./setup-2fa" export {default as setup2FA} from "./setup-2fa" export * from "./setup-2fa-verify" export {default as verify2FASetup} from "./setup-2fa-verify" +export * from "./delete-2fa" +export {default as delete2FA} from "./delete-2fa" diff --git a/src/route-widgets/Settings2FARoute/Delete2FA.tsx b/src/route-widgets/Settings2FARoute/Delete2FA.tsx new file mode 100644 index 0000000..771d57e --- /dev/null +++ b/src/route-widgets/Settings2FARoute/Delete2FA.tsx @@ -0,0 +1,105 @@ +import {ReactElement, useState} from "react" +import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {BsPhone, BsShieldLockFill} from "react-icons/bs" +import {MdSettingsBackupRestore} from "react-icons/md" + +import {useMutation} from "@tanstack/react-query" +import {Button, Grid, TextField} from "@mui/material" +import {LoadingButton} from "@mui/lab" + +import {Delete2FAData, delete2FA} from "~/apis" +import {useErrorSuccessSnacks} from "~/hooks" +import {SimpleDetailResponse} from "~/server-types" + +export interface Delete2FAProps { + onSuccess: () => void +} + +export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement { + const {t} = useTranslation() + const {showError} = useErrorSuccessSnacks() + const {mutate} = useMutation(delete2FA, { + onSuccess, + onError: showError, + }) + + const [view, setView] = useState<"showAction" | "askType" | "askCode" | "askRecoveryCode">( + "showAction", + ) + const [value, setValue] = useState("") + + switch (view) { + case "showAction": + return ( + + ) + + case "askType": + return ( + + + + + + + + + ) + + case "askCode": + return ( + + + setValue(e.target.value)} + /> + + + mutate({code: value})} + variant="contained" + startIcon={} + > + {t("routes.SettingsRoute.2fa.delete.submit")} + + + + ) + + case "askRecoveryCode": + return ( + + + setValue(e.target.value)} + /> + + + mutate({recoveryCode: value})} + variant="contained" + startIcon={} + > + {t("routes.SettingsRoute.2fa.delete.submit")} + + + + ) + } +} diff --git a/src/routes/Settings2FARoute.tsx b/src/routes/Settings2FARoute.tsx index 8595c14..068dc3c 100644 --- a/src/routes/Settings2FARoute.tsx +++ b/src/routes/Settings2FARoute.tsx @@ -3,9 +3,10 @@ import {AxiosError} from "axios" import {useTranslation} from "react-i18next" import {useQuery} from "@tanstack/react-query" -import {Alert} from "@mui/material" +import {Alert, Grid} from "@mui/material" import {QueryResult, SimplePageBuilder} from "~/components" +import Delete2FA from "~/route-widgets/Settings2FARoute/Delete2FA" import Setup2FA from "~/route-widgets/Settings2FARoute/Setup2FA" import getHas2FAEnabled from "~/apis/get-has-2fa-enabled" @@ -19,11 +20,16 @@ export default function Settings2FARoute(): ReactElement { query={query}> {has2FAEnabled => has2FAEnabled ? ( - <> - - {t("routes.SettingsRoute.2fa.alreadyEnabled")} - - + + + + {t("routes.SettingsRoute.2fa.alreadyEnabled")} + + + + + + ) : ( ) From 47eee0db4a692dc9367637815ad420b0a1d41987 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 11:56:58 +0100 Subject: [PATCH 05/10] fix: Improve UI --- src/routes/SettingsAliasPreferencesRoute.tsx | 487 +++++++++---------- 1 file changed, 235 insertions(+), 252 deletions(-) diff --git a/src/routes/SettingsAliasPreferencesRoute.tsx b/src/routes/SettingsAliasPreferencesRoute.tsx index b32d20f..e8dd998 100644 --- a/src/routes/SettingsAliasPreferencesRoute.tsx +++ b/src/routes/SettingsAliasPreferencesRoute.tsx @@ -17,7 +17,6 @@ import { InputAdornment, MenuItem, TextField, - Typography, useMediaQuery, useTheme, } from "@mui/material" @@ -31,7 +30,7 @@ import { IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, PROXY_USER_AGENT_TYPE_NAME_MAP, } from "~/constants/enum-mappings" -import {AuthContext} from "~/components" +import {AuthContext, SimplePageBuilder} from "~/components" interface Form { removeTrackers: boolean @@ -121,262 +120,246 @@ export default function SettingsAliasPreferencesRoute(): ReactElement { const isLarge = useMediaQuery(theme.breakpoints.up("md")) return ( -
- - - - {t("routes.SettingsRoute.forms.aliasPreferences.title")} - - - - - {t("routes.SettingsRoute.forms.aliasPreferences.description")} - - - - - - - + + + + + + } + labelPlacement="start" + label={t("relations.alias.settings.removeTrackers.label")} + /> + + {(formik.touched.createMailReport && + formik.errors.createMailReport) || + t("relations.alias.settings.removeTrackers.helperText")} + + + + + + + } + labelPlacement="start" + label={t("relations.alias.settings.createMailReports.label")} + /> + + {(formik.touched.createMailReport && + formik.errors.createMailReport) || + t("relations.alias.settings.createMailReports.helperText")} + + + + + + + } + labelPlacement="start" + label={t("relations.alias.settings.proxyImages.label")} + /> + + {(formik.touched.proxyImages && formik.errors.proxyImages) || + t("relations.alias.settings.proxyImages.helperText")} + + + {t("general.experimentalFeature")} + + + + + + + + + + ), + }} + name="imageProxyFormat" + id="imageProxyFormat" + label={t( + "relations.alias.settings.imageProxyFormat.label", + )} + value={formik.values.imageProxyFormat} onChange={formik.handleChange} - onBlur={formik.handleBlur} - /> - } - labelPlacement="start" - label={t("relations.alias.settings.removeTrackers.label")} - /> - - {(formik.touched.createMailReport && - formik.errors.createMailReport) || - t("relations.alias.settings.removeTrackers.helperText")} - - - - - - - } - labelPlacement="start" - label={t("relations.alias.settings.createMailReports.label")} - /> - - {(formik.touched.createMailReport && - formik.errors.createMailReport) || - t("relations.alias.settings.createMailReports.helperText")} - - - - - - - } - labelPlacement="start" - label={t("relations.alias.settings.proxyImages.label")} - /> - - {(formik.touched.proxyImages && formik.errors.proxyImages) || - t("relations.alias.settings.proxyImages.helperText")} - - - {t("general.experimentalFeature")} - - - - - - - - - - ), - }} - name="imageProxyFormat" - id="imageProxyFormat" - label={t( - "relations.alias.settings.imageProxyFormat.label", - )} - value={formik.values.imageProxyFormat} - onChange={formik.handleChange} - disabled={formik.isSubmitting} - error={ - formik.touched.imageProxyFormat && - Boolean(formik.errors.imageProxyFormat) - } - helperText={ - formik.touched.imageProxyFormat && - formik.errors.imageProxyFormat - } - > - {Object.entries( - IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, - ).map(([value, translationString]) => ( + disabled={formik.isSubmitting} + error={ + formik.touched.imageProxyFormat && + Boolean(formik.errors.imageProxyFormat) + } + helperText={ + formik.touched.imageProxyFormat && + formik.errors.imageProxyFormat + } + > + {Object.entries(IMAGE_PROXY_FORMAT_TYPE_NAME_MAP).map( + ([value, translationString]) => ( {t(translationString)} - ))} - - - {formik.touched.imageProxyFormat && - formik.errors.imageProxyFormat} - - - + ), + )} + + + {formik.touched.imageProxyFormat && + formik.errors.imageProxyFormat} + + - - - - - - {Object.entries(PROXY_USER_AGENT_TYPE_NAME_MAP).map( - ([value, translationString]) => ( - - {t(translationString)} - - ), - )} - - - {(formik.touched.proxyUserAgent && - formik.errors.proxyUserAgent) || - t("relations.alias.settings.proxyUserAgent.helperText")} - - - - - - - } - labelPlacement="start" - label={t("relations.alias.settings.expandUrlShorteners.label")} - /> - - {(formik.touched.expandUrlShorteners && - formik.errors.expandUrlShorteners) || - t( - "relations.alias.settings.expandUrlShorteners.helperText", - )} - - - {t("general.experimentalFeature")} - - - + + + + + + + {Object.entries(PROXY_USER_AGENT_TYPE_NAME_MAP).map( + ([value, translationString]) => ( + + {t(translationString)} + + ), + )} + + + {(formik.touched.proxyUserAgent && formik.errors.proxyUserAgent) || + t("relations.alias.settings.proxyUserAgent.helperText")} + + + + + + + } + labelPlacement="start" + label={t("relations.alias.settings.expandUrlShorteners.label")} + /> + + {(formik.touched.expandUrlShorteners && + formik.errors.expandUrlShorteners) || + t("relations.alias.settings.expandUrlShorteners.helperText")} + + + {t("general.experimentalFeature")} + + - - } - > - {t("routes.SettingsRoute.forms.aliasPreferences.saveAction")} - - - -
+ } + > + {t("routes.SettingsRoute.forms.aliasPreferences.saveAction")} + + +
) } From aba9ff0e116714860cca6960f335ae55f9b09648 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 13:35:37 +0100 Subject: [PATCH 06/10] fix: OTP --- src/hooks/use-error-success-snacks.ts | 6 ++++-- src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/use-error-success-snacks.ts b/src/hooks/use-error-success-snacks.ts index 8656c73..11e301f 100644 --- a/src/hooks/use-error-success-snacks.ts +++ b/src/hooks/use-error-success-snacks.ts @@ -28,9 +28,11 @@ export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult { }) } const showError = (error: Error | string) => { - let message + let message: string | undefined - if (typeof error !== "string") { + if (typeof error === "string") { + message = error + } else { try { const parsedError = parseFastAPIError(error as AxiosError) diff --git a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx index 7e59df3..4b76cbd 100644 --- a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx +++ b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx @@ -21,6 +21,7 @@ import { Grid, InputAdornment, TextField, + useTheme, } from "@mui/material" import {parseFastAPIError} from "~/utils" From 2100fa0ccf1377d4160fa7b0e85cc87527829614 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 14:10:31 +0100 Subject: [PATCH 07/10] fix: Improve OTP --- public/locales/en-US/translation.json | 6 ++++-- src/route-widgets/Settings2FARoute/Delete2FA.tsx | 9 ++++++--- src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index bf3a3fa..e5a8537 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -306,7 +306,8 @@ "title": "Note down your recovery codes", "description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.", "submit": "I have noted down my recovery codes" - } + }, + "success": "You have successfully enabled 2FA!" }, "delete": { "showAction": "Disable 2FA", @@ -320,7 +321,8 @@ "askRecoveryCode": { "label": "Recovery Code" }, - "submit": "Disable 2FA" + "submit": "Disable 2FA", + "success": "You have successfully disabled 2FA!" } } }, diff --git a/src/route-widgets/Settings2FARoute/Delete2FA.tsx b/src/route-widgets/Settings2FARoute/Delete2FA.tsx index 771d57e..eb0c2a0 100644 --- a/src/route-widgets/Settings2FARoute/Delete2FA.tsx +++ b/src/route-widgets/Settings2FARoute/Delete2FA.tsx @@ -18,9 +18,12 @@ export interface Delete2FAProps { export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement { const {t} = useTranslation() - const {showError} = useErrorSuccessSnacks() + const {showSuccess, showError} = useErrorSuccessSnacks() const {mutate} = useMutation(delete2FA, { - onSuccess, + onSuccess: () => { + showSuccess(t("routes.SettingsRoute.2fa.delete.success")) + onSuccess() + }, onError: showError, }) @@ -92,7 +95,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement { mutate({recoveryCode: value})} + onClick={() => mutate({recoveryCode: value.replaceAll("-", "")})} variant="contained" startIcon={} > diff --git a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx index 4b76cbd..dfef00b 100644 --- a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx +++ b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx @@ -42,7 +42,7 @@ export default function Settings2FARoute({ secret, }: VerifyOTPFormProps): ReactElement { const {t} = useTranslation() - const {showError} = useErrorSuccessSnacks() + const {showSuccess, showError} = useErrorSuccessSnacks() const user = useUser() const theme = useTheme() @@ -112,6 +112,7 @@ export default function Settings2FARoute({ ), }} + onSubmit={formik.handleSubmit} /> @@ -127,7 +128,7 @@ export default function Settings2FARoute({ - setShowRecoveryCodes(false)}> + {t("routes.SettingsRoute.2fa.setup.recoveryCodes.title")} + + + + + + + ) +} diff --git a/src/routes/LoginRoute.tsx b/src/routes/LoginRoute.tsx index 2016bb6..7b4fda3 100644 --- a/src/routes/LoginRoute.tsx +++ b/src/routes/LoginRoute.tsx @@ -7,6 +7,7 @@ import {useQueryParams} from "~/hooks" import ConfirmCodeForm from "~/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm" import ConfirmFromDifferentDevice from "~/route-widgets/LoginRoute/ConfirmFromDifferentDevice" import EmailForm from "~/route-widgets/LoginRoute/EmailForm" +import OTPForm from "~/route-widgets/LoginRoute/OTPForm" export default function LoginRoute(): ReactElement { const navigate = useNavigate() @@ -53,6 +54,16 @@ export default function LoginRoute(): ReactElement { onCodeExpired={() => { setStep(0) }} + onOTPRequested={corsToken => { + setStep(2) + setSameRequestToken(corsToken) + }} + />, + setStep(0)} />, ]} index={step} From b1a1a25bf71e22733ce656ed8435fba1dffafde6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 18:20:19 +0100 Subject: [PATCH 09/10] feat: Add recover 2fa --- public/locales/en-US/translation.json | 13 +++ src/App.tsx | 5 + src/route-widgets/LoginRoute/OTPForm.tsx | 1 + src/routes/Recover2FARoute.tsx | 123 +++++++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/routes/Recover2FARoute.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 9057d18..7c6ebbf 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -104,6 +104,19 @@ } } }, + "Recover2FARoute": { + "title": "Recover Two-Factor-Authentication", + "description": "We are very sorry if you lost your codes. Please enter a recovery code to continue. Note that this will disable two-factor authentication for your account. You can enable it again in the settings.", + "forms": { + "recoveryCode": { + "label": "Recovery Code" + }, + "submit": "Disable 2FA" + }, + "unauthorized": "Please make sure to log in first and then reset your two-factor authentication on its screen.", + "canLoginNow": "Two-factor authentication has been disabled. You can now log in.", + "loggedIn": "Two-factor authentication has been disabled. You are now logged in." + }, "CompleteAccountRoute": { "forms": { "generateReports": { diff --git a/src/App.tsx b/src/App.tsx index 79c00e0..c54ecea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import GlobalSettingsRoute from "~/routes/GlobalSettingsRoute" import I18nHandler from "./I18nHandler" import LoginRoute from "~/routes/LoginRoute" import LogoutRoute from "~/routes/LogoutRoute" +import Recover2FARoute from "./routes/Recover2FARoute" import RedirectRoute from "./routes/RedirectRoute" import ReportDetailRoute from "~/routes/ReportDetailRoute" import ReportsRoute from "~/routes/ReportsRoute" @@ -53,6 +54,10 @@ const router = createBrowserRouter([ loader: getServerSettings, element: , }, + { + path: "/auth/recover-2fa", + element: , + }, { path: "/auth/signup", loader: getServerSettings, diff --git a/src/route-widgets/LoginRoute/OTPForm.tsx b/src/route-widgets/LoginRoute/OTPForm.tsx index 95ac2e2..457c21d 100644 --- a/src/route-widgets/LoginRoute/OTPForm.tsx +++ b/src/route-widgets/LoginRoute/OTPForm.tsx @@ -96,6 +96,7 @@ export default function OTPForm({ (delete2FA, { + onSuccess: async () => { + try { + const user = await getMe() + + showSuccess(t("routes.Recover2FARoute.loggedIn").toString()) + login(user) + navigate("/aliases") + } catch (error) { + showSuccess(t("routes.Recover2FARoute.canLoginNow").toString()) + navigate("/auth/login") + } + }, + onError: error => { + if (error.response?.status == 401) { + showError(t("routes.Recover2FARoute.unauthorized").toString()) + navigate("/auth/login") + } else { + showError(error) + } + }, + }) + + const schema = yup.object().shape({ + recoveryCode: yup.string().required().label(t("routes.LoginRoute.forms.otp.code.label")), + }) + + const formik = useFormik
({ + validationSchema: schema, + initialValues: { + recoveryCode: "", + }, + onSubmit: async values => + mutateAsync({ + recoveryCode: values.recoveryCode.replaceAll("-", ""), + }), + }) + + return ( + + + + + + + {t("routes.Recover2FARoute.title")} + + + + + + + + + + {t("routes.Recover2FARoute.description")} + + + + + + + } + > + {t("routes.Recover2FARoute.forms.submit")} + + + + + +
+ ) +} From c7d81b1bb5a40b8d2ba7e50cb7416a6c4dfa4026 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:43:07 +0100 Subject: [PATCH 10/10] fix: types --- .../widgets/StringPoolField/StringPoolField.tsx | 2 +- .../CreateReservedAliasRoute/UsersSelectField.tsx | 2 +- src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/widgets/StringPoolField/StringPoolField.tsx b/src/components/widgets/StringPoolField/StringPoolField.tsx index b3f91c0..1263769 100644 --- a/src/components/widgets/StringPoolField/StringPoolField.tsx +++ b/src/components/widgets/StringPoolField/StringPoolField.tsx @@ -120,9 +120,9 @@ export default function StringPoolField({ }, "") onChange!( - // @ts-ignore { ...event, + // @ts-ignore target: { ...event.target, value: value as string, diff --git a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx index 029d50e..9fb60f9 100644 --- a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx +++ b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx @@ -84,9 +84,9 @@ export default function UsersSelectField({ } onChange!( - // @ts-ignore { ...event, + // @ts-ignore target: { ...event.target, value: selectedUsers as GetAdminUsersResponse["users"], diff --git a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx index dfef00b..87082e1 100644 --- a/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx +++ b/src/route-widgets/Settings2FARoute/VerifyOTPForm.tsx @@ -68,7 +68,11 @@ export default function Settings2FARoute({ } }, }) - const formik = useFormik({ + const formik = useFormik< + Verify2FASetupData & { + detail?: string + } + >({ initialValues: { code: "", }, @@ -112,7 +116,7 @@ export default function Settings2FARoute({ ), }} - onSubmit={formik.handleSubmit} + onSubmit={() => formik.handleSubmit()} />