misc: addressed comments 1

This commit is contained in:
Sheen Capadngan
2024-11-15 23:25:32 +08:00
parent 6bae3628c0
commit 433971a72d
13 changed files with 251 additions and 92 deletions
@@ -13,11 +13,13 @@ type Props = {
const TotpRegistration = ({ onComplete }: Props) => {
const { data: registration, isLoading } = useGetUserTotpRegistration();
const { mutateAsync: verifyUserTotp } = useVerifyUserTotpRegistration();
const { mutateAsync: verifyUserTotp, isLoading: isVerifyLoading } =
useVerifyUserTotpRegistration();
const [qrCodeUrl, setQrCodeUrl] = useState("");
const [totp, setTotp] = useState("");
const handleTotpVerify = async () => {
const handleTotpVerify = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
await verifyUserTotp({
totp
});
@@ -54,15 +56,19 @@ const TotpRegistration = ({ onComplete }: Props) => {
<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>
<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>
);
};
+14
View File
@@ -140,3 +140,17 @@ export const useDeleteUserTotpConfiguration = () => {
}
});
};
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 -1
View File
@@ -480,7 +480,7 @@ export const useGetUserTotpConfiguration = () => {
return data;
} catch (error) {
if (error instanceof AxiosError && error.response?.data?.statusCode === 404) {
if (error instanceof AxiosError && [404, 400].includes(error.response?.data?.statusCode)) {
return {
isVerified: false,
recoveryCodes: []
+63 -45
View File
@@ -1,4 +1,4 @@
import { useEffect, 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";
@@ -62,7 +62,9 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
}
}, []);
const verifyMfa = async () => {
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
try {
const { token } = await verifyMfaToken({
@@ -110,14 +112,19 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
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();
}}
/>
</div>
<>
<div className="mb-6 text-center text-lg font-bold text-white">
Your organization requires mobile authenticator 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>
</>
);
}
@@ -147,42 +154,53 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
</p>
</>
)}
<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>
{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
onClick={() => verifyMfa()}
size="sm"
isFullWidth
className="h-14"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
{String(t("mfa.verify"))}
</Button>
<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>
{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>
</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">
No access to both 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">
@@ -58,7 +58,7 @@ export const OrgGenericAuthSection = () => {
});
createNotification({
text: "Successfully updated preferred MFA method",
text: "Successfully updated selected MFA method",
type: "success"
});
} catch (err) {
@@ -90,7 +90,7 @@ export const OrgGenericAuthSection = () => {
Enforce members to authenticate with MFA in order to access the organization
</p>
{currentOrg?.enforceMfa && (
<FormControl label="Required 2FA method" className="mt-3">
<FormControl label="Selected 2FA method" className="mt-3">
<Select
className="min-w-[20rem] border border-mineshaft-500"
onValueChange={handleUpdateSelectedMfa}
@@ -16,7 +16,10 @@ 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 {
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";
@@ -33,6 +36,7 @@ export const MFASection = () => {
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
useGetUserTotpConfiguration();
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
const queryClient = useQueryClient();
const { data: serverDetails } = useFetchServerStatus();
@@ -58,6 +62,26 @@ export const MFASection = () => {
}
};
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;
@@ -67,12 +91,12 @@ export const MFASection = () => {
});
createNotification({
text: "Successfully updated preferred 2FA method",
text: "Successfully updated selected 2FA method",
type: "success"
});
} catch (err) {
createNotification({
text: "Something went wrong while updating preferred 2FA method.",
text: "Something went wrong while updating selected 2FA method.",
type: "error"
});
console.error(err);
@@ -114,7 +138,7 @@ export const MFASection = () => {
return (
<>
<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>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
{user && (
<Checkbox
className="data-[state=checked]:bg-primary"
@@ -132,7 +156,7 @@ export const MFASection = () => {
</Checkbox>
)}
{user?.isMfaEnabled && (
<FormControl label="Preferred 2FA method" className="mt-3">
<FormControl label="Selected 2FA method" className="mt-3">
<Select
className="min-w-[20rem] border border-mineshaft-500"
onValueChange={updateSelectedMfa}
@@ -147,7 +171,7 @@ export const MFASection = () => {
</Select>
</FormControl>
)}
<div className="mt-10 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
<div className="mt-8 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
{isTotpConfigurationLoading ? (
<ContentLoader />
) : (
@@ -158,6 +182,9 @@ export const MFASection = () => {
<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>
@@ -171,13 +198,19 @@ export const MFASection = () => {
)}
</div>
) : (
<div className="mt-6 flex min-w-full justify-center">
<TotpRegistration
onComplete={async () => {
await queryClient.invalidateQueries(userKeys.totpConfiguration);
}}
/>
</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>
)}
@@ -188,7 +221,8 @@ export const MFASection = () => {
/>
<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."
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}