mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 06:35:21 +00:00
feat: initial implementation for totp authenticator
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { useGetUserTotpRegistration } from "@app/hooks/api";
|
||||
import { useVerifyUserTotpRegistration } from "@app/hooks/api/users/mutation";
|
||||
|
||||
import { createNotification } from "../notifications";
|
||||
import { Button, ContentLoader, Input } from "../v2";
|
||||
|
||||
type Props = {
|
||||
onComplete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
const TotpRegistration = ({ onComplete }: Props) => {
|
||||
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
||||
const { mutateAsync: verifyUserTotp } = useVerifyUserTotpRegistration();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||
const [totp, setTotp] = useState("");
|
||||
|
||||
const handleTotpVerify = async () => {
|
||||
await verifyUserTotp({
|
||||
totp
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully configured mobile authenticator",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const generateQRCode = async () => {
|
||||
if (registration?.otpUrl) {
|
||||
const url = await QRCode.toDataURL(registration.otpUrl);
|
||||
setQrCodeUrl(url);
|
||||
}
|
||||
};
|
||||
|
||||
generateQRCode();
|
||||
}, [registration]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoader />;
|
||||
}
|
||||
return (
|
||||
<div className="flex max-w-sm flex-col text-bunker-200">
|
||||
<div className="mb-4 text-center">
|
||||
Download a two-step verification app (Duo, Google Authenticator, etc.) and scan the QR code.
|
||||
</div>
|
||||
<div className="mb-10 flex items-center justify-center">
|
||||
<img src={qrCodeUrl} alt="registration-qr" />
|
||||
</div>
|
||||
<div className="mb-4 text-center">Enter the resulting verification code</div>
|
||||
<div className="mb-4 flex flex-row gap-2">
|
||||
<Input
|
||||
onChange={(e) => setTotp(e.target.value)}
|
||||
value={totp}
|
||||
placeholder="Verification code"
|
||||
/>
|
||||
<Button onClick={handleTotpVerify}>Enable MFA</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotpRegistration;
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Login2Res,
|
||||
LoginLDAPDTO,
|
||||
LoginLDAPRes,
|
||||
MfaMethod,
|
||||
ResetPasswordDTO,
|
||||
SendMfaTokenDTO,
|
||||
SRP1DTO,
|
||||
@@ -65,10 +66,11 @@ export const selectOrganization = async (data: {
|
||||
organizationId: string;
|
||||
userAgent?: UserAgentType;
|
||||
}) => {
|
||||
const { data: res } = await apiRequest.post<{ token: string; isMfaEnabled: boolean }>(
|
||||
"/api/v3/auth/select-organization",
|
||||
data
|
||||
);
|
||||
const { data: res } = await apiRequest.post<{
|
||||
token: string;
|
||||
isMfaEnabled: boolean;
|
||||
mfaMethod?: MfaMethod;
|
||||
}>("/api/v3/auth/select-organization", data);
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -154,10 +156,19 @@ export const useSendMfaToken = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => {
|
||||
export const verifyMfaToken = async ({
|
||||
email,
|
||||
mfaCode,
|
||||
mfaMethod
|
||||
}: {
|
||||
email: string;
|
||||
mfaCode: string;
|
||||
mfaMethod: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||
email,
|
||||
mfaToken: mfaCode
|
||||
mfaToken: mfaCode,
|
||||
mfaMethod
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -165,10 +176,11 @@ export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCod
|
||||
|
||||
export const useVerifyMfaToken = () => {
|
||||
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
||||
mutationFn: async ({ email, mfaCode }) => {
|
||||
mutationFn: async ({ email, mfaCode, mfaMethod }) => {
|
||||
return verifyMfaToken({
|
||||
email,
|
||||
mfaCode
|
||||
mfaCode,
|
||||
mfaMethod
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -302,3 +314,9 @@ export const useGetAuthToken = () =>
|
||||
onSuccess: (data) => setAuthToken(data.token),
|
||||
retry: 0
|
||||
});
|
||||
|
||||
export const checkUserTotpMfa = async () => {
|
||||
const { data } = await apiRequest.get<{ isVerified: boolean }>("/api/v2/auth/mfa/check/totp");
|
||||
|
||||
return data.isVerified;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export type SendMfaTokenDTO = {
|
||||
export type VerifyMfaTokenDTO = {
|
||||
email: string;
|
||||
mfaCode: string;
|
||||
mfaMethod: MfaMethod;
|
||||
};
|
||||
|
||||
export type VerifyMfaTokenRes = {
|
||||
@@ -149,3 +150,8 @@ export type GetBackupEncryptedPrivateKeyDTO = {
|
||||
export enum UserAgentType {
|
||||
CLI = "cli"
|
||||
}
|
||||
|
||||
export enum MfaMethod {
|
||||
EMAIL = "email",
|
||||
TOTP = "totp"
|
||||
}
|
||||
|
||||
@@ -21,12 +21,13 @@ export {
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useGetUserAction,
|
||||
useGetUserTotpRegistration,
|
||||
useListUserGroupMemberships,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useRevokeMySessions,
|
||||
useUpdateMfaEnabled,
|
||||
useUpdateOrgMembership,
|
||||
useUpdateUserAuthMethods
|
||||
useUpdateUserAuthMethods,
|
||||
useUpdateUserMfa
|
||||
} from "./queries";
|
||||
export { userKeys } from "./query-keys";
|
||||
|
||||
@@ -114,3 +114,29 @@ export const useUpdateUserProjectFavorites = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyUserTotpRegistration = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ totp }: { totp: string }) => {
|
||||
await apiRequest.post("/api/v1/user/me/totp/verify", {
|
||||
totp
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUserTotpConfiguration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiRequest.delete("/api/v1/user/me/totp");
|
||||
|
||||
return {};
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { setAuthToken } from "@app/reactQuery";
|
||||
|
||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||
import { MfaMethod } from "../auth/types";
|
||||
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { userKeys } from "./query-keys";
|
||||
@@ -390,14 +392,21 @@ export const useRevokeMySessions = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMfaEnabled = () => {
|
||||
export const useUpdateUserMfa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => {
|
||||
mutationFn: async ({
|
||||
isMfaEnabled,
|
||||
selectedMfaMethod
|
||||
}: {
|
||||
isMfaEnabled?: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
}) => {
|
||||
const {
|
||||
data: { user }
|
||||
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
||||
isMfaEnabled
|
||||
isMfaEnabled,
|
||||
selectedMfaMethod
|
||||
});
|
||||
|
||||
return user;
|
||||
@@ -446,3 +455,39 @@ export const useListUserGroupMemberships = (username: string) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserTotpRegistration = () => {
|
||||
return useQuery({
|
||||
queryKey: userKeys.totpRegistration,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.post<{ otpUrl: string; recoveryCodes: string[] }>(
|
||||
"/api/v1/user/me/totp/register"
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserTotpConfiguration = () => {
|
||||
return useQuery({
|
||||
queryKey: userKeys.totpConfiguration,
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const { data } = await apiRequest.get<{ isVerified: boolean; recoveryCodes: string[] }>(
|
||||
"/api/v1/user/me/totp"
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.data?.statusCode === 404) {
|
||||
return {
|
||||
isVerified: false,
|
||||
recoveryCodes: []
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ export const userKeys = {
|
||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||
mySessions: ["sessions"] as const,
|
||||
listUsers: ["user-list"] as const,
|
||||
totpRegistration: ["totp-registration"],
|
||||
totpConfiguration: ["totp-configuration"],
|
||||
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MfaMethod } from "../auth/types";
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||
|
||||
@@ -26,6 +27,7 @@ export type User = {
|
||||
authProvider?: AuthMethod;
|
||||
authMethods: AuthMethod[];
|
||||
isMfaEnabled: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
seenIps: string[];
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
useLogoutUser,
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||
import { MfaMethod, UserAgentType } from "@app/hooks/api/auth/types";
|
||||
import { Organization } from "@app/hooks/api/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||
@@ -46,6 +46,7 @@ export default function LoginPage() {
|
||||
const selectOrg = useSelectOrganization();
|
||||
const { data: user, isLoading: userLoading } = useGetUser();
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
|
||||
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
@@ -90,15 +91,17 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { token, isMfaEnabled } = await selectOrg.mutateAsync({
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrg.mutateAsync({
|
||||
organizationId: organization.id,
|
||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
|
||||
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +216,11 @@ export default function LoginPage() {
|
||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||
</Head>
|
||||
{shouldShowMfa ? (
|
||||
<Mfa email={user.email as string} successCallback={mfaSuccessCallback} />
|
||||
<Mfa
|
||||
email={user.email as string}
|
||||
successCallback={mfaSuccessCallback}
|
||||
method={requiredMfaMethod}
|
||||
/>
|
||||
) : (
|
||||
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
||||
<Link href="/">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -6,10 +6,12 @@ import { useRouter } from "next/router";
|
||||
import { t } from "i18next";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useSendMfaToken } from "@app/hooks/api";
|
||||
import { verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
|
||||
// The style for the verification code input
|
||||
const codeInputProps = {
|
||||
@@ -36,23 +38,37 @@ type Props = {
|
||||
closeMfa?: () => void;
|
||||
hideLogo?: boolean;
|
||||
email: string;
|
||||
method: MfaMethod;
|
||||
};
|
||||
|
||||
export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
||||
export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => {
|
||||
const [mfaCode, setMfaCode] = useState("");
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
|
||||
|
||||
const sendMfaToken = useSendMfaToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (method === MfaMethod.TOTP) {
|
||||
checkUserTotpMfa().then((isVerified) => {
|
||||
if (!isVerified) {
|
||||
SecurityClient.setMfaToken("");
|
||||
setShouldShowTotpRegistration(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyMfa = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { token } = await verifyMfaToken({
|
||||
email,
|
||||
mfaCode
|
||||
mfaCode,
|
||||
mfaMethod: method
|
||||
});
|
||||
|
||||
SecurityClient.setMfaToken("");
|
||||
@@ -92,6 +108,23 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldShowTotpRegistration) {
|
||||
return (
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
<TotpRegistration
|
||||
onComplete={async () => {
|
||||
setShouldShowTotpRegistration(false);
|
||||
|
||||
await successCallback();
|
||||
if (closeMfa) {
|
||||
closeMfa();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
{!hideLogo && (
|
||||
@@ -101,18 +134,40 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<>
|
||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
||||
</>
|
||||
)}
|
||||
{method === MfaMethod.TOTP && (
|
||||
<>
|
||||
<p className="text-l mb-4 flex max-w-xs justify-center text-center font-bold text-bunker-100">
|
||||
Authenticator MFA Required
|
||||
</p>
|
||||
<p className="text-l flex max-w-xs justify-center text-center text-bunker-300">
|
||||
Open the authenticator app on your mobile device to get your verification code or enter
|
||||
a recovery code.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
className="mt-6 mb-2"
|
||||
{...codeInputProps}
|
||||
/>
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
className="mt-6 mb-2"
|
||||
{...codeInputProps}
|
||||
/>
|
||||
)}
|
||||
{method === MfaMethod.TOTP && (
|
||||
<div className="mt-6 mb-4">
|
||||
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
@@ -132,21 +187,23 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
{isLoadingResend
|
||||
? t("signup.step2-resend-progress")
|
||||
: t("signup.step2-resend-submit")}
|
||||
</span>
|
||||
</button>
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
{isLoadingResend
|
||||
? t("signup.step2-resend-progress")
|
||||
: t("signup.step2-resend-submit")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||
</div>
|
||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,84 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, EmailServiceSetupModal } from "@app/components/v2";
|
||||
import { useGetUser, useUpdateMfaEnabled } from "@app/hooks/api";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
ContentLoader,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { useDeleteUserTotpConfiguration } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const MFASection = () => {
|
||||
const { data: user } = useGetUser();
|
||||
const { mutateAsync } = useUpdateMfaEnabled();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
||||
const { mutateAsync } = useUpdateUserMfa();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"setUpEmail",
|
||||
"deleteTotpConfig"
|
||||
] as const);
|
||||
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
||||
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
||||
useGetUserTotpConfiguration();
|
||||
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const handleTotpDeletion = async () => {
|
||||
try {
|
||||
await deleteTotpConfiguration();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted mobile authenticator",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTotpConfig");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to delete mobile authenticator";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
|
||||
await mutateAsync({
|
||||
selectedMfaMethod: mfaMethod
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated preferred 2FA method",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Something went wrong while updating preferred 2FA method.",
|
||||
type: "error"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMfa = async (state: boolean) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
@@ -47,31 +113,86 @@ export const MFASection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||
{user && (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="isTwoFAEnabled"
|
||||
isChecked={user?.isMfaEnabled}
|
||||
onCheckedChange={(state) => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
toggleMfa(state as boolean);
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
}
|
||||
}}
|
||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||
{user && (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="isTwoFAEnabled"
|
||||
isChecked={user?.isMfaEnabled}
|
||||
onCheckedChange={(state) => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
toggleMfa(state as boolean);
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable 2-factor authentication
|
||||
</Checkbox>
|
||||
)}
|
||||
{user?.isMfaEnabled && (
|
||||
<FormControl label="Preferred 2FA method" className="mt-3">
|
||||
<Select
|
||||
className="min-w-[20rem] border border-mineshaft-500"
|
||||
onValueChange={updateSelectedMfa}
|
||||
defaultValue={user.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||
>
|
||||
Enable 2-factor authentication via your personal email.
|
||||
</Checkbox>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||
Email
|
||||
</SelectItem>
|
||||
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||
Mobile Authenticator
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<div className="mt-12 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
||||
{isTotpConfigurationLoading ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<div>
|
||||
{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="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes.length && (
|
||||
<div className="mt-4 bg-mineshaft-600 p-4">
|
||||
{totpConfiguration.recoveryCodes.map((code) => (
|
||||
<div key={code}>{code}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 flex min-w-full justify-center">
|
||||
<TotpRegistration
|
||||
onComplete={async () => {
|
||||
await queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||
title="Are you sure want to delete the mobile authenticator? You’ll have to go through the setup process to enable it again."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleTotpDeletion}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user