diff --git a/client/package.json b/client/package.json index 081b026..ea03a0c 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "framer-motion": "^11.2.10", "openai": "^4.53.2", "react": "^18.2.0", + "react-auth-code-input": "^3.2.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", diff --git a/client/src/components/SignInModule.tsx b/client/src/components/SignInModule.tsx index e106220..8ab1e8e 100644 --- a/client/src/components/SignInModule.tsx +++ b/client/src/components/SignInModule.tsx @@ -1,13 +1,23 @@ -import { Button, Link } from "@nextui-org/react"; +import { + Button, + Link, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from "@nextui-org/react"; import { Formik, Form } from "formik"; import * as Yup from "yup"; import config from "../config"; import NextUIFormikInput from "./NextUIFormikInput"; import { useNavigate } from "react-router-dom"; import { ChevronLeftIcon } from "../icons"; -import { popErrorToast } from "../utilities"; +import { 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"; const validationSchema = Yup.object({ email: Yup.string() @@ -30,14 +40,28 @@ const validationSchema = Yup.object({ export default function SignInModule() { const navigate = useNavigate(); + 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: "", password: "", }; - const handleSubmit = (values: any): void => { + const proceedWithLogin = (values?: any) => { + if (!values) { + values = userLoginInformation; + } + console.log("proceeding with login: " + values.email); instance - .post(config.serverAddress + "/users/login", values) + .post(config.serverAddress + "/users/login", { + email: values.email, + password: values.password, + }) .then((response) => { console.log("logging in"); localStorage.setItem("accessToken", response.data.accessToken); @@ -55,9 +79,52 @@ export default function SignInModule() { }) .catch((error) => { popErrorToast(error); + setTwoFactorModal(false); }); }; + const handleSubmit = (values: any): void => { + setUserLoginInformation(values); + instance + .post("/users/has-2fa", { email: values.email }) + .then((answer) => { + if (answer.data.enabled) { + setTwoFactorModal(true); + } else { + proceedWithLogin(values); + } + }) + .catch(() => { + popToast("User not found!", 2); + }); + }; + + 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 (
@@ -119,6 +186,30 @@ export default function SignInModule() { Sign up
+ + + Two-Factor Authentication + +
+ { + setTwoFactorToken(value); + }} + /> +

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

+
+
+ +
+
); } diff --git a/server/routes/users.js b/server/routes/users.js index f0e43ab..5b3d9f1 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -544,10 +544,28 @@ router.post("/enable-2fa/:id", async (req, res) => { }); }); -router.post("/verify-2fa", async (req, res) => { - const { id, token } = req.body; +router.post("/has-2fa", async (req, res) => { + let email = req.body.email; + const user = await User.findOne({ + where: { email: email }, + }); - const user = await User.findByPk(id); + if (!user) { + return res.status(404).send("User not found"); + } + + return res.json({ + status: "success", + enabled: user.secret ? true : false, + }); +}); + +router.post("/verify-2fa", async (req, res) => { + const { email, token } = req.body; + + const user = await User.findOne({ + where: { email: email }, + }); if (!user) { console.log("User not found");