mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 14:27:23 +00:00
Merge pull request #2731 from Infisical/feat/totp-authenticator
feat: TOTP authenticator
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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, isLoading: isVerifyLoading } =
|
||||
useVerifyUserTotpRegistration();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||
const [totp, setTotp] = useState("");
|
||||
|
||||
const handleTotpVerify = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
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>
|
||||
<form onSubmit={handleTotpVerify}>
|
||||
<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 isLoading={isVerifyLoading} type="submit">
|
||||
Enable MFA
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</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"
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ export const useUpdateOrg = () => {
|
||||
slug,
|
||||
orgId,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa
|
||||
enforceMfa,
|
||||
selectedMfaMethod
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@@ -99,7 +100,8 @@ export const useUpdateOrg = () => {
|
||||
scimEnabled,
|
||||
slug,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa
|
||||
enforceMfa,
|
||||
selectedMfaMethod
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
||||
|
||||
import { MfaMethod } from "../auth/types";
|
||||
|
||||
export type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -12,6 +14,7 @@ export type Organization = {
|
||||
slug: string;
|
||||
defaultMembershipRole: string;
|
||||
enforceMfa: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
};
|
||||
|
||||
export type UpdateOrgDTO = {
|
||||
@@ -22,6 +25,7 @@ export type UpdateOrgDTO = {
|
||||
slug?: string;
|
||||
defaultMembershipRoleSlug?: string;
|
||||
enforceMfa?: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
||||
@@ -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,43 @@ 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateNewTotpRecoveryCodes = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiRequest.post("/api/v1/user/me/totp/recovery-codes");
|
||||
|
||||
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 && [404, 400].includes(error.response?.data?.statusCode)) {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
useLogoutUser,
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
@@ -143,6 +144,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
|
||||
const workspacesWithFaveProp = useMemo(
|
||||
@@ -214,12 +216,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
};
|
||||
|
||||
const changeOrg = async (orgId: string) => {
|
||||
const { token, isMfaEnabled } = await selectOrganization({
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => () => changeOrg(orgId));
|
||||
return;
|
||||
@@ -365,6 +370,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||
<Mfa
|
||||
email={user.email as string}
|
||||
method={requiredMfaMethod}
|
||||
successCallback={mfaSuccessCallback}
|
||||
closeMfa={() => toggleShowMfa.off()}
|
||||
/>
|
||||
|
||||
@@ -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,19 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { token, isMfaEnabled } = await selectOrg.mutateAsync({
|
||||
organizationId: organization.id,
|
||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||
});
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||
.mutateAsync({
|
||||
organizationId: organization.id,
|
||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||
})
|
||||
.finally(() => setIsInitialOrgCheckLoading(false));
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
|
||||
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +218,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="/">
|
||||
|
||||
@@ -29,8 +29,10 @@ import {
|
||||
useSelectOrganization,
|
||||
verifySignupInvite
|
||||
} from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
@@ -59,6 +61,7 @@ export default function SignupInvite() {
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
@@ -184,12 +187,19 @@ export default function SignupInvite() {
|
||||
if (!orgId) throw new Error("You are not part of any organization");
|
||||
|
||||
const completeSignupFlow = async () => {
|
||||
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
|
||||
const {
|
||||
token: mfaToken,
|
||||
isMfaEnabled,
|
||||
mfaMethod
|
||||
} = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(mfaToken);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => completeSignupFlow);
|
||||
return;
|
||||
@@ -390,12 +400,23 @@ export default function SignupInvite() {
|
||||
<title>Sign Up</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
||||
{shouldShowMfa ? (
|
||||
<Mfa
|
||||
email={email}
|
||||
successCallback={mfaSuccessCallback}
|
||||
method={requiredMfaMethod}
|
||||
closeMfa={() => toggleShowMfa.off()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import React, { 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,39 @@ 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();
|
||||
|
||||
const verifyMfa = async () => {
|
||||
useEffect(() => {
|
||||
if (method === MfaMethod.TOTP) {
|
||||
checkUserTotpMfa().then((isVerified) => {
|
||||
if (!isVerified) {
|
||||
SecurityClient.setMfaToken("");
|
||||
setShouldShowTotpRegistration(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { token } = await verifyMfaToken({
|
||||
email,
|
||||
mfaCode
|
||||
mfaCode,
|
||||
mfaMethod: method
|
||||
});
|
||||
|
||||
SecurityClient.setMfaToken("");
|
||||
@@ -92,6 +110,24 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldShowTotpRegistration) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-center text-lg font-bold text-white">
|
||||
Your organization requires mobile authentication to be configured.
|
||||
</div>
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
<TotpRegistration
|
||||
onComplete={async () => {
|
||||
setShouldShowTotpRegistration(false);
|
||||
await successCallback();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||
{!hideLogo && (
|
||||
@@ -101,52 +137,87 @@ 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>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||
<div className="text-l w-full py-1 text-lg">
|
||||
<Button
|
||||
onClick={() => verifyMfa()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{String(t("mfa.verify"))}
|
||||
</Button>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
<form onSubmit={verifyMfa}>
|
||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||
{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>
|
||||
</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>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
)}
|
||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||
<div className="text-l w-full py-1 text-lg">
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{String(t("mfa.verify"))}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||
</div>
|
||||
</form>
|
||||
{method === MfaMethod.TOTP && (
|
||||
<div className="mt-2 flex flex-row justify-center text-sm text-bunker-400 ">
|
||||
<Link href="/verify-email">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Lost your recovery codes? Reset your account
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Button, Input, Spinner } from "@app/components/v2";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||
|
||||
@@ -36,6 +37,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
|
||||
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
|
||||
@@ -66,12 +68,15 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||
if (organizationId) {
|
||||
const finishWithOrgWorkflow = async () => {
|
||||
const { token, isMfaEnabled } = await selectOrganization({ organizationId });
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,10 +172,15 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||
if (organizationId) {
|
||||
const finishWithOrgWorkflow = async () => {
|
||||
const { token, isMfaEnabled } = await selectOrganization({ organizationId });
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => finishWithOrgWorkflow);
|
||||
return;
|
||||
@@ -283,6 +293,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
|
||||
<Mfa
|
||||
email={email}
|
||||
successCallback={mfaSuccessCallback}
|
||||
method={requiredMfaMethod}
|
||||
closeMfa={() => toggleShowMfa.off()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+44
-1
@@ -1,6 +1,6 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Switch, UpgradePlanModal } from "@app/components/v2";
|
||||
import { FormControl, Select, SelectItem, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const OrgGenericAuthSection = () => {
|
||||
@@ -43,6 +44,32 @@ export const OrgGenericAuthSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSelectedMfa = async (selectedMfaMethod: MfaMethod) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.enforceMfa) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
selectedMfaMethod
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated selected MFA method",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: (err as { response: { data: { message: string } } }).response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div className="py-4">
|
||||
@@ -62,6 +89,22 @@ export const OrgGenericAuthSection = () => {
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce members to authenticate with MFA in order to access the organization
|
||||
</p>
|
||||
{currentOrg?.enforceMfa && (
|
||||
<FormControl label="Selected 2FA method" className="mt-3">
|
||||
<Select
|
||||
className="min-w-[20rem] border border-mineshaft-500"
|
||||
onValueChange={handleUpdateSelectedMfa}
|
||||
defaultValue={currentOrg.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||
>
|
||||
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||
Email
|
||||
</SelectItem>
|
||||
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||
Mobile Authenticator
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
|
||||
@@ -1,18 +1,108 @@
|
||||
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,
|
||||
ContentLoader,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} 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 {
|
||||
useCreateNewTotpRecoveryCodes,
|
||||
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 { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
||||
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 handleGenerateMoreRecoveryCodes = async () => {
|
||||
try {
|
||||
await createTotpRecoveryCodes();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully generated new recovery codes",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to generate new recovery codes";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
|
||||
await mutateAsync({
|
||||
selectedMfaMethod: mfaMethod
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated selected 2FA method",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Something went wrong while updating selected 2FA method.",
|
||||
type: "error"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMfa = async (state: boolean) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
@@ -47,31 +137,96 @@ 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-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||
{user && (
|
||||
<Switch
|
||||
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
|
||||
</Switch>
|
||||
)}
|
||||
{user?.isMfaEnabled && (
|
||||
<FormControl label="Selected 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-8 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="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 className="text-sm text-gray-400">
|
||||
For added security, you can configure a mobile authenticator and set it as your
|
||||
selected 2FA method.
|
||||
</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 configured authenticator?"
|
||||
subTitle="This action is irreversible. You’ll have to go through the setup process to enable it again."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleTotpDeletion}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import ProjectService from "@app/services/ProjectService";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
@@ -57,6 +58,7 @@ export const UserInfoSSOStep = ({
|
||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||
const [attributionSource, setAttributionSource] = useState("");
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
@@ -178,12 +180,15 @@ export const UserInfoSSOStep = ({
|
||||
|
||||
const completeSignupFlow = async () => {
|
||||
try {
|
||||
const { isMfaEnabled, token } = await selectOrganization({
|
||||
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => completeSignupFlow);
|
||||
return;
|
||||
@@ -231,6 +236,7 @@ export const UserInfoSSOStep = ({
|
||||
hideLogo
|
||||
email={username}
|
||||
successCallback={mfaSuccessCallback}
|
||||
method={requiredMfaMethod}
|
||||
closeMfa={() => toggleShowMfa.off()}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user