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