From 19198537af50d1f46a60adc7377799e54688b159 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Thu, 15 Aug 2024 01:16:09 +0800 Subject: [PATCH] Enable / Disable 2FA --- client/src/components/SignInModule.tsx | 61 +----- .../TwoFactorAuthenticationModule.tsx | 59 ++++++ .../TwoFactorsAuthenticationSetupModule.tsx | 173 ++++++++++++++++++ client/src/components/UpdateAccountModule.tsx | 64 ++++++- client/src/utilities.ts | 12 ++ server/routes/users.js | 21 +++ 6 files changed, 329 insertions(+), 61 deletions(-) create mode 100644 client/src/components/TwoFactorAuthenticationModule.tsx create mode 100644 client/src/components/TwoFactorsAuthenticationSetupModule.tsx diff --git a/client/src/components/SignInModule.tsx b/client/src/components/SignInModule.tsx index 8ab1e8e..8af9d70 100644 --- a/client/src/components/SignInModule.tsx +++ b/client/src/components/SignInModule.tsx @@ -13,11 +13,11 @@ import config from "../config"; import NextUIFormikInput from "./NextUIFormikInput"; import { useNavigate } from "react-router-dom"; import { ChevronLeftIcon } from "../icons"; -import { popErrorToast, popToast } from "../utilities"; +import { checkTwoFactorStatus, popErrorToast, popToast } from "../utilities"; import { retrieveUserInformation } from "../security/users"; import instance from "../security/http"; -import { useEffect, useRef, useState } from "react"; -import AuthCode, { AuthCodeRef } from "react-auth-code-input"; +import { useState } from "react"; +import TwoFactorAuthenticationModule from "./TwoFactorAuthenticationModule"; const validationSchema = Yup.object({ email: Yup.string() @@ -42,10 +42,6 @@ export default function SignInModule() { const [twoFactorModal, setTwoFactorModal] = useState(false); const [userLoginInformation, setUserLoginInformation] = useState(); - const [twoFactorToken, setTwoFactorToken] = useState(""); - const [twoFactorVerifying, setTwoFactorVerifying] = useState(false); - - const AuthInputRef = useRef(null); const initialValues = { email: "", @@ -85,10 +81,9 @@ export default function SignInModule() { const handleSubmit = (values: any): void => { setUserLoginInformation(values); - instance - .post("/users/has-2fa", { email: values.email }) + checkTwoFactorStatus(values.email) .then((answer) => { - if (answer.data.enabled) { + if (answer) { setTwoFactorModal(true); } else { proceedWithLogin(values); @@ -99,32 +94,6 @@ export default function SignInModule() { }); }; - useEffect(() => { - if (!(twoFactorToken.length == 6 && !twoFactorVerifying)) { - return; - } - - setTwoFactorVerifying(true); - instance - .post("/users/verify-2fa", { - email: userLoginInformation.email, - token: twoFactorToken, - }) - .then(() => { - proceedWithLogin(); - }) - .catch((error) => { - popErrorToast(error); - AuthInputRef.current?.clear(); - setTwoFactorToken(""); - }) - .finally(() => { - AuthInputRef.current?.clear(); - setTwoFactorToken(""); - setTwoFactorVerifying(false); - }); - }, [twoFactorToken]); - return (
@@ -190,22 +159,12 @@ export default function SignInModule() { Two-Factor Authentication -
- { - setTwoFactorToken(value); - }} + {userLoginInformation && ( + -

- Please enter the 6 digits passcode from your authenticator app. -

-
+ )}
diff --git a/client/src/components/TwoFactorAuthenticationModule.tsx b/client/src/components/TwoFactorAuthenticationModule.tsx new file mode 100644 index 0000000..9118994 --- /dev/null +++ b/client/src/components/TwoFactorAuthenticationModule.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from "react"; +import AuthCode, { AuthCodeRef } from "react-auth-code-input"; +import instance from "../security/http"; +import { popErrorToast } from "../utilities"; + +export default function TwoFactorsAuthenticationModule({ + email, + onTwoFactorSuccess, +}: { + email: string; + onTwoFactorSuccess: () => void; +}) { + const AuthInputRef = useRef(null); + const [twoFactorToken, setTwoFactorToken] = useState(""); + const [twoFactorVerifying, setTwoFactorVerifying] = useState(false); + useEffect(() => { + if (!(twoFactorToken.length == 6 && !twoFactorVerifying)) { + return; + } + + setTwoFactorVerifying(true); + instance + .post("/users/verify-2fa", { + email: email, + token: twoFactorToken, + }) + .then(() => { + onTwoFactorSuccess(); + }) + .catch((error) => { + popErrorToast(error); + AuthInputRef.current?.clear(); + setTwoFactorToken(""); + }) + .finally(() => { + AuthInputRef.current?.clear(); + setTwoFactorToken(""); + setTwoFactorVerifying(false); + }); + }, [twoFactorToken]); + return ( +
+ { + setTwoFactorToken(value); + }} + /> +

+ Please enter the 6 digits passcode from your authenticator app. +

+
+ ); +} diff --git a/client/src/components/TwoFactorsAuthenticationSetupModule.tsx b/client/src/components/TwoFactorsAuthenticationSetupModule.tsx new file mode 100644 index 0000000..6cb4837 --- /dev/null +++ b/client/src/components/TwoFactorsAuthenticationSetupModule.tsx @@ -0,0 +1,173 @@ +import { retrieveUserInformation } from "../security/users"; +import { useEffect, useState } from "react"; +import { Button, Image, Input } from "@nextui-org/react"; +import instance from "../security/http"; +import { checkTwoFactorStatus } from "../utilities"; +import TwoFactorAuthenticationModule from "./TwoFactorAuthenticationModule"; + +export default function TwoFactorsAuthenticationSetupModule({ + onClose, +}: { + onClose: () => void; +}) { + const [userInformation, setUserInformation] = useState(); + const [setup2FAStepperCount, setSetup2FAStepperCount] = useState(0); + const [disable2FAStepperCount, setDisable2FAStepperCount] = useState(0); + const [isTwoFactorEnabled, setIsTwoFactorEnabled] = useState(false); + const [setupQRBase64, setSetupQRBase64] = useState(""); + const [setupBase32Secret, setSetupBase32Secret] = useState(""); + + const disableTwoFactor = async () => { + instance + .post("/users/disable-2fa", { + id: userInformation.id, + }) + .then(() => { + setDisable2FAStepperCount(1); + }); + }; + + const enableTwoFactor = () => { + instance + .post("/users/enable-2fa/" + userInformation.id) + .then((response) => { + setSetupQRBase64(response.data.data.qrCodeUrl); + setSetupBase32Secret(response.data.data.secret); + setSetup2FAStepperCount(1); + }); + }; + + const testTwoFactor = () => {}; + + useEffect(() => { + retrieveUserInformation().then((response) => { + setUserInformation(response); + checkTwoFactorStatus(response.email).then((answer) => { + setIsTwoFactorEnabled(answer); + }); + }); + }, []); + + return ( +
+ {userInformation && ( + <> + {!isTwoFactorEnabled && ( +
+ {setup2FAStepperCount === 0 && ( +
+

+ This setup will guide you through the enabling of + Two-Factors Authorization (2FA). +

+ +
+ )} + {setup2FAStepperCount === 1 && ( +
+

+ Please scan the QR code below using an authenticator app of + your choice. +

+ {setupQRBase64 && ( + 2FA SETUP QR + )} +

Or alternatively, manually enter the secret in the app:

+ +
+ +
+
+ )} + {setup2FAStepperCount === 2 && ( +
+

Let's give it a try.

+ { + setSetup2FAStepperCount(3); + }} + /> +
+ + +
+
+ )} + {setup2FAStepperCount === 3 && ( +
+

+ All set! You will be asked to provide the passcode next time + you log in. +

+
+ +
+
+ )} +
+ )} + {isTwoFactorEnabled && ( +
+ {disable2FAStepperCount === 0 && ( +
+

+ Are you sure you want to disable Two-Factors Authorization + (2FA)? +

+
+ + +
+
+ )} + {disable2FAStepperCount === 1 && ( +
+

2FA has been disabled.

+
+ +
+
+ )} +
+ )} + + )} +
+ ); +} diff --git a/client/src/components/UpdateAccountModule.tsx b/client/src/components/UpdateAccountModule.tsx index 7695af6..05a8b1f 100644 --- a/client/src/components/UpdateAccountModule.tsx +++ b/client/src/components/UpdateAccountModule.tsx @@ -18,15 +18,17 @@ import { Form, Formik } from "formik"; import NextUIFormikInput from "./NextUIFormikInput"; import { useNavigate } from "react-router-dom"; import UserProfilePicture from "./UserProfilePicture"; -import { popErrorToast, popToast } from "../utilities"; +import { checkTwoFactorStatus, popErrorToast, popToast } from "../utilities"; import instance from "../security/http"; import axios from "axios"; import NextUIFormikSelect from "./NextUIFormikSelect"; +import TwoFactorsAuthenticationSetupModule from "./TwoFactorsAuthenticationSetupModule"; export default function UpdateAccountModule() { const navigate = useNavigate(); const [userInformation, setUserInformation] = useState(); const [townCouncils, setTownCouncils] = useState([]); + const [is2FAEnabled, setIs2FAEnabled] = useState(false); const { isOpen: isArchiveDialogOpen, @@ -40,6 +42,12 @@ export default function UpdateAccountModule() { onOpenChange: onResetPasswordOpenChange, } = useDisclosure(); + const { + isOpen: isTwoFactorsAuthenticationOpen, + onOpen: onTwoFactorsAuthenticationOpen, + onOpenChange: onTwoFactorsAuthenticationOpenChange, + } = useDisclosure(); + useEffect(() => { retrieveUserInformation() .then((response) => { @@ -49,6 +57,9 @@ export default function UpdateAccountModule() { .then((values) => { setTownCouncils(JSON.parse(values.data).townCouncils); }); + checkTwoFactorStatus(response.email).then((answer) => { + setIs2FAEnabled(answer); + }); }) .catch(() => { navigate("/signin"); @@ -266,7 +277,7 @@ export default function UpdateAccountModule() { } className="rounded-xl -m-2 *:px-4" > - +

Danger zone

@@ -275,17 +286,16 @@ export default function UpdateAccountModule() {

+ -
@@ -379,6 +389,40 @@ export default function UpdateAccountModule() { }} + + {/* Two-Factors Authorization Modal */} + + + {(onClose) => { + return ( + <> + + Set up Two-Factors Authentication + + +
+ { + checkTwoFactorStatus(userInformation.email) + .then((answer) => { + setIs2FAEnabled(answer); + }) + .finally(() => { + onClose(); + }); + }} + /> +
+
+ + ); + }} +
+
)}
diff --git a/client/src/utilities.ts b/client/src/utilities.ts index 5c714f6..d164cae 100644 --- a/client/src/utilities.ts +++ b/client/src/utilities.ts @@ -1,6 +1,7 @@ import { AxiosError } from "axios"; import toast from "react-hot-toast"; import exportFromJSON, { ExportType } from "export-from-json"; +import instance from "./security/http"; export function getTimeOfDay(): number { const currentHour = new Date().getHours(); @@ -48,3 +49,14 @@ export const exportData = ( ) => { exportFromJSON({ data, fileName, exportType }); }; + +export const checkTwoFactorStatus = async (email: string) => { + try { + const answer = await instance.post("/users/has-2fa", { + email: email, + }); + return answer.data.enabled; + } catch { + return false; + } +}; diff --git a/server/routes/users.js b/server/routes/users.js index c804718..2e3b6ab 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -546,6 +546,27 @@ router.post("/enable-2fa/:id", async (req, res) => { }); }); +router.post("/disable-2fa/", async (req, res) => { + let id = req.body.id; + const user = User.findByPk(id); + + if (!user) { + return res.status(404).send("User not found"); + } + + await User.update( + { secret: null }, + { + where: { id: id }, + } + ); + + return res.json({ + status: "success", + message: "2FA disabled", + }); +}); + router.post("/has-2fa", async (req, res) => { let email = req.body.email; const user = await User.findOne({