Enable / Disable 2FA
This commit is contained in:
@@ -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<any>();
|
||||
const [twoFactorToken, setTwoFactorToken] = useState("");
|
||||
const [twoFactorVerifying, setTwoFactorVerifying] = useState(false);
|
||||
|
||||
const AuthInputRef = useRef<AuthCodeRef>(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 (
|
||||
<div className="flex flex-col gap-16">
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -190,22 +159,12 @@ export default function SignInModule() {
|
||||
<ModalContent>
|
||||
<ModalHeader>Two-Factor Authentication</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="text-center flex flex-col gap-4">
|
||||
<AuthCode
|
||||
containerClassName="flex flex-row gap-4 w-full justify-center"
|
||||
inputClassName="w-16 h-16 text-4xl text-center rounded-lg my-2 bg-neutral-100 dark:bg-neutral-800 border-2 border-neutral-200 dark:border-neutral-700 "
|
||||
length={6}
|
||||
allowedCharacters="numeric"
|
||||
ref={AuthInputRef}
|
||||
disabled={twoFactorVerifying}
|
||||
onChange={(value) => {
|
||||
setTwoFactorToken(value);
|
||||
}}
|
||||
{userLoginInformation && (
|
||||
<TwoFactorAuthenticationModule
|
||||
email={userLoginInformation.email}
|
||||
onTwoFactorSuccess={proceedWithLogin}
|
||||
/>
|
||||
<p className="text-md opacity-50">
|
||||
Please enter the 6 digits passcode from your authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter></ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
59
client/src/components/TwoFactorAuthenticationModule.tsx
Normal file
59
client/src/components/TwoFactorAuthenticationModule.tsx
Normal file
@@ -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<AuthCodeRef>(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 (
|
||||
<div className="text-center flex flex-col gap-4">
|
||||
<AuthCode
|
||||
containerClassName="flex flex-row gap-4 w-full justify-center"
|
||||
inputClassName="w-16 h-16 text-4xl text-center rounded-lg my-2 bg-neutral-100 dark:bg-neutral-800 border-2 border-neutral-200 dark:border-neutral-700 "
|
||||
length={6}
|
||||
allowedCharacters="numeric"
|
||||
ref={AuthInputRef}
|
||||
disabled={twoFactorVerifying}
|
||||
onChange={(value) => {
|
||||
setTwoFactorToken(value);
|
||||
}}
|
||||
/>
|
||||
<p className="text-md opacity-50">
|
||||
Please enter the 6 digits passcode from your authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
173
client/src/components/TwoFactorsAuthenticationSetupModule.tsx
Normal file
173
client/src/components/TwoFactorsAuthenticationSetupModule.tsx
Normal file
@@ -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<any>();
|
||||
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 (
|
||||
<div>
|
||||
{userInformation && (
|
||||
<>
|
||||
{!isTwoFactorEnabled && (
|
||||
<div>
|
||||
{setup2FAStepperCount === 0 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
This setup will guide you through the enabling of
|
||||
Two-Factors Authorization (2FA).
|
||||
</p>
|
||||
<Button
|
||||
onPress={() => {
|
||||
enableTwoFactor();
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{setup2FAStepperCount === 1 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
Please scan the QR code below using an authenticator app of
|
||||
your choice.
|
||||
</p>
|
||||
{setupQRBase64 && (
|
||||
<Image src={setupQRBase64} alt="2FA SETUP QR" />
|
||||
)}
|
||||
<p>Or alternatively, manually enter the secret in the app:</p>
|
||||
<Input value={setupBase32Secret} readOnly />
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<Button
|
||||
onPress={() => {
|
||||
setSetup2FAStepperCount(2);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{setup2FAStepperCount === 2 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>Let's give it a try.</p>
|
||||
<TwoFactorAuthenticationModule
|
||||
email={userInformation.email}
|
||||
onTwoFactorSuccess={() => {
|
||||
setSetup2FAStepperCount(3);
|
||||
}}
|
||||
/>
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
setSetup2FAStepperCount(3);
|
||||
}}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
enableTwoFactor();
|
||||
}}
|
||||
>
|
||||
Setup again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{setup2FAStepperCount === 3 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
All set! You will be asked to provide the passcode next time
|
||||
you log in.
|
||||
</p>
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<Button onPress={onClose}>Finish</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isTwoFactorEnabled && (
|
||||
<div>
|
||||
{disable2FAStepperCount === 0 && (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<p>
|
||||
Are you sure you want to disable Two-Factors Authorization
|
||||
(2FA)?
|
||||
</p>
|
||||
<div className="flex flex-row gap-2 w-full justify-end">
|
||||
<Button
|
||||
variant="light"
|
||||
color="danger"
|
||||
onPress={() => {
|
||||
disableTwoFactor();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{disable2FAStepperCount === 1 && (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<p>2FA has been disabled.</p>
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<Button onPress={onClose}>Finish</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<any>();
|
||||
const [townCouncils, setTownCouncils] = useState<string[]>([]);
|
||||
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"
|
||||
>
|
||||
<Card className="flex flex-row justify-between *:my-auto bg-primary-50 dark:bg-primary-950 p-4 my-2">
|
||||
<Card className="flex flex-col gap-4 justify-between *:my-auto bg-primary-50 dark:bg-primary-950 p-4 my-2">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg">Danger zone</p>
|
||||
<p className="opacity-50">
|
||||
@@ -275,17 +286,16 @@ export default function UpdateAccountModule() {
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Button
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={onResetPasswordOpen}
|
||||
color={is2FAEnabled ? "danger" : "secondary"}
|
||||
onPress={onTwoFactorsAuthenticationOpen}
|
||||
>
|
||||
{is2FAEnabled ? "Disable" : "Enable"} Two-Factors
|
||||
Authentication
|
||||
</Button>
|
||||
<Button color="danger" onPress={onResetPasswordOpen}>
|
||||
Reset your password
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
onPress={onArchiveDialogOpen}
|
||||
>
|
||||
<Button color="danger" onPress={onArchiveDialogOpen}>
|
||||
Archive this account
|
||||
</Button>
|
||||
</div>
|
||||
@@ -379,6 +389,40 @@ export default function UpdateAccountModule() {
|
||||
}}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Two-Factors Authorization Modal */}
|
||||
<Modal
|
||||
isOpen={isTwoFactorsAuthenticationOpen}
|
||||
onOpenChange={onTwoFactorsAuthenticationOpenChange}
|
||||
size="xl"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => {
|
||||
return (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Set up Two-Factors Authentication
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="w-full h-full pb-4">
|
||||
<TwoFactorsAuthenticationSetupModule
|
||||
onClose={() => {
|
||||
checkTwoFactorStatus(userInformation.email)
|
||||
.then((answer) => {
|
||||
setIs2FAEnabled(answer);
|
||||
})
|
||||
.finally(() => {
|
||||
onClose();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user