mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 23:28:25 +00:00
Improve personal settings 2FA form
This commit is contained in:
@@ -508,7 +508,7 @@ export const useListUserGroupMemberships = (username: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetUserTotpRegistration = () => {
|
export const useGetUserTotpRegistration = (options?: { enabled?: boolean }) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: userKeys.totpRegistration,
|
queryKey: userKeys.totpRegistration,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -517,7 +517,8 @@ export const useGetUserTotpRegistration = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
},
|
||||||
|
enabled: options?.enabled ?? true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+395
-112
@@ -1,6 +1,8 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
import { RecoveryCodesDownload } from "@app/components/mfa/RecoveryCodesDownload";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -8,9 +10,9 @@ import {
|
|||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmailServiceSetupModal,
|
EmailServiceSetupModal,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem
|
||||||
Switch
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||||
@@ -18,9 +20,13 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
|
|||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import {
|
import {
|
||||||
useCreateNewTotpRecoveryCodes,
|
useCreateNewTotpRecoveryCodes,
|
||||||
useDeleteUserTotpConfiguration
|
useDeleteUserTotpConfiguration,
|
||||||
|
useVerifyUserTotpRegistration
|
||||||
} from "@app/hooks/api/users/mutation";
|
} from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
import {
|
||||||
|
useGetUserTotpConfiguration,
|
||||||
|
useGetUserTotpRegistration
|
||||||
|
} from "@app/hooks/api/users/queries";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -28,24 +34,65 @@ export const MFASection = () => {
|
|||||||
const { data: user } = useGetUser();
|
const { data: user } = useGetUser();
|
||||||
const { mutateAsync } = useUpdateUserMfa();
|
const { mutateAsync } = useUpdateUserMfa();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
isMfaEnabled: user?.isMfaEnabled || false,
|
||||||
|
selectedMfaMethod: user?.selectedMfaMethod || MfaMethod.EMAIL
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [totpCode, setTotpCode] = useState("");
|
||||||
|
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||||
|
const [showMobileAuthSetup, setShowMobileAuthSetup] = useState(false);
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"setUpEmail",
|
"setUpEmail",
|
||||||
"deleteTotpConfig"
|
"deleteTotpConfig",
|
||||||
|
"downloadRecoveryCodes"
|
||||||
] as const);
|
] as const);
|
||||||
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
||||||
const { data: totpConfiguration, isPending: isTotpConfigurationLoading } =
|
const { data: totpConfiguration } = useGetUserTotpConfiguration();
|
||||||
useGetUserTotpConfiguration();
|
const { data: totpRegistration, isPending: isTotpRegistrationLoading } =
|
||||||
|
useGetUserTotpRegistration({
|
||||||
|
enabled: showMobileAuthSetup
|
||||||
|
});
|
||||||
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||||
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
||||||
|
const { mutateAsync: verifyUserTotp } = useVerifyUserTotpRegistration();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
|
||||||
|
// Update form data when user data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setFormData({
|
||||||
|
isMfaEnabled: user.isMfaEnabled,
|
||||||
|
selectedMfaMethod: user.selectedMfaMethod || MfaMethod.EMAIL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateQRCode = async () => {
|
||||||
|
if (totpRegistration?.otpUrl) {
|
||||||
|
const url = await QRCode.toDataURL(totpRegistration.otpUrl);
|
||||||
|
setQrCodeUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showMobileAuthSetup && totpRegistration?.otpUrl) {
|
||||||
|
generateQRCode();
|
||||||
|
}
|
||||||
|
}, [totpRegistration, showMobileAuthSetup]);
|
||||||
|
|
||||||
const handleTotpDeletion = async () => {
|
const handleTotpDeletion = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteTotpConfiguration();
|
await deleteTotpConfiguration();
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
selectedMfaMethod: MfaMethod.EMAIL
|
||||||
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted mobile authenticator",
|
text: "Successfully deleted mobile authenticator and switched to email authentication",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,30 +129,35 @@ export const MFASection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
const handleFormDataChange = (field: string, value: any) => {
|
||||||
try {
|
setFormData((prev) => ({
|
||||||
if (!user) return;
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
|
||||||
await mutateAsync({
|
// Show mobile auth setup when mobile authenticator is selected and we're enabling 2FA
|
||||||
selectedMfaMethod: mfaMethod
|
if (field === "selectedMfaMethod" && value === MfaMethod.TOTP && formData.isMfaEnabled) {
|
||||||
});
|
setShowMobileAuthSetup(true);
|
||||||
|
} else if (field === "selectedMfaMethod" && value !== MfaMethod.TOTP) {
|
||||||
createNotification({
|
setShowMobileAuthSetup(false);
|
||||||
text: "Successfully updated selected 2FA method",
|
setTotpCode("");
|
||||||
type: "success"
|
setShouldShowRecoveryCodes.off();
|
||||||
});
|
if (totpConfiguration?.isVerified) {
|
||||||
} catch (err) {
|
deleteTotpConfiguration().catch(console.error);
|
||||||
createNotification({
|
}
|
||||||
text: "Something went wrong while updating selected 2FA method.",
|
} else if (field === "isMfaEnabled" && value && formData.selectedMfaMethod === MfaMethod.TOTP) {
|
||||||
type: "error"
|
setShowMobileAuthSetup(true);
|
||||||
});
|
} else if (field === "isMfaEnabled" && !value) {
|
||||||
console.error(err);
|
setShowMobileAuthSetup(false);
|
||||||
|
setTotpCode("");
|
||||||
|
setShouldShowRecoveryCodes.off();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMfa = async (state: boolean) => {
|
const handleSaveChanges = async () => {
|
||||||
try {
|
try {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
if (user.authMethods.includes(AuthMethod.LDAP)) {
|
if (user.authMethods.includes(AuthMethod.LDAP)) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Two-factor authentication is not available for LDAP users.",
|
text: "Two-factor authentication is not available for LDAP users.",
|
||||||
@@ -114,119 +166,350 @@ export const MFASection = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await mutateAsync({
|
if (!serverDetails?.emailConfigured && formData.isMfaEnabled) {
|
||||||
isMfaEnabled: state
|
handlePopUpOpen("setUpEmail");
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
setIsLoading(true);
|
||||||
text: `${
|
|
||||||
newUser.isMfaEnabled
|
// If enabling 2FA with mobile authenticator, verify TOTP first
|
||||||
? "Successfully turned on two-factor authentication."
|
if (
|
||||||
: "Successfully turned off two-factor authentication."
|
formData.isMfaEnabled &&
|
||||||
}`,
|
formData.selectedMfaMethod === MfaMethod.TOTP &&
|
||||||
type: "success"
|
!totpConfiguration?.isVerified
|
||||||
});
|
) {
|
||||||
|
if (!totpCode.trim()) {
|
||||||
|
createNotification({
|
||||||
|
text: "Please enter the verification code from your authenticator app",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyUserTotp({ totp: totpCode });
|
||||||
|
|
||||||
|
handlePopUpOpen("downloadRecoveryCodes");
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully configured mobile authenticator. Please save your recovery codes!",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: userKeys.totpConfiguration });
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to verify TOTP code. Please try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If disabling 2FA and there's a TOTP configuration, delete it
|
||||||
|
if (!formData.isMfaEnabled && user.isMfaEnabled && totpConfiguration?.isVerified) {
|
||||||
|
try {
|
||||||
|
await deleteTotpConfiguration();
|
||||||
|
createNotification({
|
||||||
|
text: "Mobile authenticator removed",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh TOTP configuration
|
||||||
|
await queryClient.invalidateQueries({ queryKey: userKeys.totpConfiguration });
|
||||||
|
} catch {
|
||||||
|
// Continue with disabling 2FA even if TOTP deletion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: any = {};
|
||||||
|
|
||||||
|
// Only update if values have changed
|
||||||
|
if (formData.isMfaEnabled !== user.isMfaEnabled) {
|
||||||
|
updates.isMfaEnabled = formData.isMfaEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.selectedMfaMethod !== user.selectedMfaMethod) {
|
||||||
|
updates.selectedMfaMethod = formData.selectedMfaMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await mutateAsync(updates);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated two-factor authentication settings",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form state
|
||||||
|
setShowMobileAuthSetup(false);
|
||||||
|
setTotpCode("");
|
||||||
|
setShouldShowRecoveryCodes.off();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Something went wrong while toggling the two-factor authentication.",
|
text: "Something went wrong while updating two-factor authentication settings.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasChanges =
|
||||||
|
user &&
|
||||||
|
(formData.isMfaEnabled !== user.isMfaEnabled ||
|
||||||
|
formData.selectedMfaMethod !== user.selectedMfaMethod);
|
||||||
|
|
||||||
|
const isFormValid = () => {
|
||||||
|
if (!formData.isMfaEnabled) return true;
|
||||||
|
if (formData.selectedMfaMethod === MfaMethod.EMAIL) return true;
|
||||||
|
if (formData.selectedMfaMethod === MfaMethod.TOTP) {
|
||||||
|
if (totpConfiguration?.isVerified) return true;
|
||||||
|
return totpCode.trim().length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<form
|
||||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSaveChanges();
|
||||||
|
}}
|
||||||
|
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<h2 className="mb-6 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</h2>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<Switch
|
<div className="space-y-4">
|
||||||
className="data-[state=checked]:bg-primary"
|
<div className="max-w-md">
|
||||||
id="isTwoFAEnabled"
|
<FormControl
|
||||||
isChecked={user?.isMfaEnabled}
|
label="Enable 2-factor authentication"
|
||||||
onCheckedChange={(state) => {
|
helperText="Adds an extra layer of security to your account"
|
||||||
if (serverDetails?.emailConfigured) {
|
>
|
||||||
toggleMfa(state as boolean);
|
<Select
|
||||||
} else {
|
value={formData.isMfaEnabled ? "enabled" : "disabled"}
|
||||||
handlePopUpOpen("setUpEmail");
|
onValueChange={(value) =>
|
||||||
}
|
handleFormDataChange("isMfaEnabled", value === "enabled")
|
||||||
}}
|
}
|
||||||
>
|
className="w-full"
|
||||||
Enable 2-factor authentication
|
>
|
||||||
</Switch>
|
<SelectItem value="disabled">Disabled</SelectItem>
|
||||||
)}
|
<SelectItem value="enabled">Enabled</SelectItem>
|
||||||
{user?.isMfaEnabled && (
|
</Select>
|
||||||
<FormControl label="Selected 2FA method" className="mt-3">
|
</FormControl>
|
||||||
<Select
|
</div>
|
||||||
className="min-w-[20rem] border border-mineshaft-500"
|
|
||||||
onValueChange={updateSelectedMfa}
|
{formData.isMfaEnabled && (
|
||||||
defaultValue={user.selectedMfaMethod ?? MfaMethod.EMAIL}
|
<div className="max-w-md">
|
||||||
>
|
<FormControl
|
||||||
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
label="Authentication method"
|
||||||
Email
|
helperText="Choose your preferred method for two-factor authentication"
|
||||||
</SelectItem>
|
>
|
||||||
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
<Select
|
||||||
Mobile Authenticator
|
value={formData.selectedMfaMethod}
|
||||||
</SelectItem>
|
onValueChange={(value) =>
|
||||||
</Select>
|
handleFormDataChange("selectedMfaMethod", value as MfaMethod)
|
||||||
</FormControl>
|
}
|
||||||
)}
|
className="w-full"
|
||||||
<div className="mt-8 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
>
|
||||||
{isTotpConfigurationLoading ? (
|
<SelectItem value={MfaMethod.EMAIL}>Email</SelectItem>
|
||||||
<ContentLoader />
|
<SelectItem value={MfaMethod.TOTP}>Mobile Authenticator</SelectItem>
|
||||||
) : (
|
</Select>
|
||||||
<div>
|
</FormControl>
|
||||||
{totpConfiguration?.isVerified ? (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
|
||||||
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
|
||||||
</Button>
|
|
||||||
<Button colorSchema="secondary" onClick={handleGenerateMoreRecoveryCodes}>
|
|
||||||
Generate more codes
|
|
||||||
</Button>
|
|
||||||
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes && (
|
|
||||||
<div className="mt-4 bg-mineshaft-600 p-4">
|
|
||||||
{totpConfiguration.recoveryCodes.map((code) => (
|
|
||||||
<div key={code}>{code}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<>
|
|
||||||
<div className="text-sm text-gray-400">
|
{showMobileAuthSetup && !totpConfiguration?.isVerified && (
|
||||||
For added security, you can configure a mobile authenticator and set it as your
|
<div className="space-y-6">
|
||||||
selected 2FA method.
|
<h3 className="mb-6 text-lg font-semibold text-mineshaft-100">
|
||||||
|
Setup Mobile Authenticator
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h4 className="mb-2 text-sm font-medium text-mineshaft-200">
|
||||||
|
Step 1: Scan QR Code
|
||||||
|
</h4>
|
||||||
|
<p className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Download a two-factor authentication app (Google Authenticator, Authy, etc.) and
|
||||||
|
scan the QR code below
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isTotpRegistrationLoading && (
|
||||||
|
<div className="py-12">
|
||||||
|
<ContentLoader />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isTotpRegistrationLoading && qrCodeUrl && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 w-72 pl-4">
|
||||||
|
<div className="inline-flex items-center justify-center rounded-xl bg-white p-6 shadow-lg">
|
||||||
|
<img
|
||||||
|
src={qrCodeUrl}
|
||||||
|
alt="QR Code for mobile authenticator setup"
|
||||||
|
className="h-48 w-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{totpRegistration?.otpUrl && (
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs text-mineshaft-400">
|
||||||
|
Can't scan? Enter this code manually:{" "}
|
||||||
|
<code className="rounded bg-mineshaft-700 px-3 py-1 font-mono text-sm text-mineshaft-100">
|
||||||
|
{totpRegistration.otpUrl.split("secret=")[1]?.split("&")[0] ||
|
||||||
|
"Loading..."}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-6 mt-6 flex min-w-full">
|
|
||||||
<TotpRegistration
|
<div>
|
||||||
onComplete={async () => {
|
<h4 className="mb-2 text-sm font-medium text-mineshaft-200">
|
||||||
await queryClient.invalidateQueries({ queryKey: userKeys.totpConfiguration });
|
Step 2: Enter verification code
|
||||||
}}
|
</h4>
|
||||||
/>
|
<p className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Enter the 6-digit code from your authenticator app to complete setup
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-w-48">
|
||||||
|
<FormControl isRequired>
|
||||||
|
<Input
|
||||||
|
value={totpCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||||||
|
setTotpCode(value);
|
||||||
|
}}
|
||||||
|
onPaste={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pastedData = e.clipboardData
|
||||||
|
.getData("text")
|
||||||
|
.replace(/\D/g, "")
|
||||||
|
.slice(0, 6);
|
||||||
|
setTotpCode(pastedData);
|
||||||
|
}}
|
||||||
|
placeholder="Enter 2FA code"
|
||||||
|
className="font-mono tracking-wider"
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex gap-2 pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
if (user) {
|
||||||
|
setFormData({
|
||||||
|
isMfaEnabled: user.isMfaEnabled,
|
||||||
|
selectedMfaMethod: user.selectedMfaMethod || MfaMethod.EMAIL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowMobileAuthSetup(false);
|
||||||
|
setTotpCode("");
|
||||||
|
setShouldShowRecoveryCodes.off();
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="border-mineshaft-500 text-mineshaft-300 hover:border-mineshaft-400"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user?.isMfaEnabled && totpConfiguration?.isVerified && (
|
||||||
|
<div className="mt-8 border-t border-mineshaft-600 pt-6">
|
||||||
|
<h3 className="mb-4 text-lg font-semibold text-mineshaft-100">
|
||||||
|
Mobile Authenticator Management
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={setShouldShowRecoveryCodes.toggle}
|
||||||
|
>
|
||||||
|
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={handleGenerateMoreRecoveryCodes}
|
||||||
|
>
|
||||||
|
Generate more codes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => handlePopUpOpen("deleteTotpConfig")}
|
||||||
|
>
|
||||||
|
Remove Authenticator
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shouldShowRecoveryCodes && (
|
||||||
|
<div className="mt-6 grid w-full grid-cols-4 gap-3">
|
||||||
|
{totpConfiguration.recoveryCodes.map((code) => (
|
||||||
|
<code
|
||||||
|
key={code}
|
||||||
|
className="block rounded bg-mineshaft-800 px-3 py-2 text-center font-mono text-sm text-mineshaft-100"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<EmailServiceSetupModal
|
<EmailServiceSetupModal
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteTotpConfig.isOpen}
|
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||||
title="Are you sure you want to delete the configured authenticator?"
|
title="Remove mobile authenticator?"
|
||||||
subTitle="This action is irreversible. You’ll have to go through the setup process to enable it again."
|
subTitle="This action is irreversible. You'll have to go through the setup process to enable it again."
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||||
deleteKey="confirm"
|
deleteKey="confirm"
|
||||||
onDeleteApproved={handleTotpDeletion}
|
onDeleteApproved={handleTotpDeletion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RecoveryCodesDownload
|
||||||
|
isOpen={popUp.downloadRecoveryCodes?.isOpen || false}
|
||||||
|
onClose={() => handlePopUpClose("downloadRecoveryCodes")}
|
||||||
|
recoveryCodes={totpRegistration?.recoveryCodes || []}
|
||||||
|
onDownloadComplete={() => handlePopUpClose("downloadRecoveryCodes")}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user