diff --git a/apps/web/components/auth/BackupCode.tsx b/apps/web/components/auth/BackupCode.tsx
new file mode 100644
index 00000000000..a9121d815eb
--- /dev/null
+++ b/apps/web/components/auth/BackupCode.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { useFormContext } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Label, TextField } from "@calcom/ui";
+
+export default function TwoFactor({ center = true }) {
+ const { t } = useLocale();
+ const methods = useFormContext();
+
+ return (
+
+
+
+
{t("backup_code_instructions")}
+
+
+
+ );
+}
diff --git a/apps/web/components/auth/TwoFactor.tsx b/apps/web/components/auth/TwoFactor.tsx
index e074639fe2e..f46aa3b7e34 100644
--- a/apps/web/components/auth/TwoFactor.tsx
+++ b/apps/web/components/auth/TwoFactor.tsx
@@ -5,7 +5,7 @@ import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Input } from "@calcom/ui";
-export default function TwoFactor({ center = true }) {
+export default function TwoFactor({ center = true, autoFocus = true }) {
const [value, onChange] = useState("");
const { t } = useLocale();
const methods = useFormContext();
@@ -40,7 +40,7 @@ export default function TwoFactor({ center = true }) {
name={`2fa${index + 1}`}
inputMode="decimal"
{...digit}
- autoFocus={index === 0}
+ autoFocus={autoFocus && index === 0}
autoComplete="one-time-code"
/>
))}
diff --git a/apps/web/components/settings/DisableTwoFactorModal.tsx b/apps/web/components/settings/DisableTwoFactorModal.tsx
index 385e775f320..46d49ce62aa 100644
--- a/apps/web/components/settings/DisableTwoFactorModal.tsx
+++ b/apps/web/components/settings/DisableTwoFactorModal.tsx
@@ -5,6 +5,7 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField } from "@calcom/ui";
+import BackupCode from "@components/auth/BackupCode";
import TwoFactor from "@components/auth/TwoFactor";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
@@ -20,6 +21,7 @@ interface DisableTwoFactorAuthModalProps {
}
interface DisableTwoFactorValues {
+ backupCode: string;
totpCode: string;
password: string;
}
@@ -33,11 +35,19 @@ const DisableTwoFactorAuthModal = ({
}: DisableTwoFactorAuthModalProps) => {
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
+ const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
const { t } = useLocale();
const form = useForm();
- async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
+ const resetForm = (clearPassword = true) => {
+ if (clearPassword) form.setValue("password", "");
+ form.setValue("backupCode", "");
+ form.setValue("totpCode", "");
+ setErrorMessage(null);
+ };
+
+ async function handleDisable({ password, totpCode, backupCode }: DisableTwoFactorValues) {
if (isDisabling) {
return;
}
@@ -45,8 +55,10 @@ const DisableTwoFactorAuthModal = ({
setErrorMessage(null);
try {
- const response = await TwoFactorAuthAPI.disable(password, totpCode);
+ const response = await TwoFactorAuthAPI.disable(password, totpCode, backupCode);
if (response.status === 200) {
+ setTwoFactorLostAccess(false);
+ resetForm();
onDisable();
return;
}
@@ -54,12 +66,14 @@ const DisableTwoFactorAuthModal = ({
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
- }
- if (body.error === ErrorCode.SecondFactorRequired) {
+ } else if (body.error === ErrorCode.SecondFactorRequired) {
setErrorMessage(t("2fa_required"));
- }
- if (body.error === ErrorCode.IncorrectTwoFactorCode) {
+ } else if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(t("incorrect_2fa"));
+ } else if (body.error === ErrorCode.IncorrectBackupCode) {
+ setErrorMessage(t("incorrect_backup_code"));
+ } else if (body.error === ErrorCode.MissingBackupCodes) {
+ setErrorMessage(t("missing_backup_codes"));
} else {
setErrorMessage(t("something_went_wrong"));
}
@@ -78,6 +92,7 @@ const DisableTwoFactorAuthModal = ({
{!disablePassword && (
)}
-
+ {twoFactorLostAccess ? (
+
+ ) : (
+
+ )}
{errorMessage &&
{errorMessage}
}
+
diff --git a/apps/web/components/settings/EnableTwoFactorModal.tsx b/apps/web/components/settings/EnableTwoFactorModal.tsx
index 099558a8af0..0ed406787fa 100644
--- a/apps/web/components/settings/EnableTwoFactorModal.tsx
+++ b/apps/web/components/settings/EnableTwoFactorModal.tsx
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
-import { Button, Dialog, DialogContent, DialogFooter, Form, TextField } from "@calcom/ui";
+import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField, showToast } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
@@ -28,6 +28,7 @@ interface EnableTwoFactorModalProps {
enum SetupStep {
ConfirmPassword,
+ DisplayBackupCodes,
DisplayQrCode,
EnterTotpCode,
}
@@ -54,16 +55,25 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
+ [SetupStep.DisplayBackupCodes]: t("backup_code_instructions"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
+ const [backupCodes, setBackupCodes] = useState([]);
+ const [backupCodesUrl, setBackupCodesUrl] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
+ const resetState = () => {
+ setPassword("");
+ setErrorMessage(null);
+ setStep(SetupStep.ConfirmPassword);
+ };
+
async function handleSetup(e: React.FormEvent) {
e.preventDefault();
@@ -79,6 +89,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
+ setBackupCodes(body.backupCodes);
+
+ // create backup codes download url
+ const textBlob = new Blob([body.backupCodes.map(formatBackupCode).join("\n")], {
+ type: "text/plain",
+ });
+ if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl);
+ setBackupCodesUrl(URL.createObjectURL(textBlob));
+
setDataUri(body.dataUri);
setSecret(body.secret);
setStep(SetupStep.DisplayQrCode);
@@ -113,7 +132,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
- onEnable();
+ setStep(SetupStep.DisplayBackupCodes);
return;
}
@@ -141,13 +160,18 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
}
}, [form, handleEnableRef, totpCode]);
+ const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;
+
return (