2fa
This commit is contained in:
@@ -1,12 +1,36 @@
|
||||
import { Input, Checkbox, Button, Link } from "@heroui/react";
|
||||
import {
|
||||
Input,
|
||||
Checkbox,
|
||||
Button,
|
||||
Link,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@heroui/react";
|
||||
import { IconMail, IconLock } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import http, { login } from "../http";
|
||||
import TwoFactorAuthenticationModule from "./TwoFactorAuthenticationModule";
|
||||
|
||||
export const checkTwoFactorStatus = async (email: string) => {
|
||||
try {
|
||||
const answer = await http.post("/User/has-2fa", {
|
||||
email: email,
|
||||
});
|
||||
return answer.data.enabled as boolean;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
const validateFields = () => {
|
||||
if (!email || !password) {
|
||||
@@ -16,9 +40,22 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
const checkFor2FA = async () => {
|
||||
if (!validateFields()) return;
|
||||
checkTwoFactorStatus(email)
|
||||
.then((answer) => {
|
||||
if (answer) {
|
||||
onOpen();
|
||||
} else {
|
||||
handleLogin();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong! Please try again.");
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
const loginRequest = {
|
||||
email,
|
||||
password,
|
||||
@@ -79,7 +116,7 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<Button color="primary" className="w-full" onPress={handleLogin}>
|
||||
<Button color="primary" className="w-full" onPress={checkFor2FA}>
|
||||
Login
|
||||
</Button>
|
||||
<div className="flex flex-row gap-2 w-full justify-center *:my-auto">
|
||||
@@ -89,6 +126,18 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Modal size="lg" isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent>
|
||||
<ModalHeader>Two-Factor Authentication</ModalHeader>
|
||||
<ModalBody>
|
||||
<TwoFactorAuthenticationModule
|
||||
email={email}
|
||||
onTwoFactorSuccess={handleLogin}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter></ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
215
AceJobAgency.client/src/components/Manage2FAView.tsx
Normal file
215
AceJobAgency.client/src/components/Manage2FAView.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Image, Input } from "@heroui/react";
|
||||
import TwoFactorAuthenticationModule from "./TwoFactorAuthenticationModule";
|
||||
import { checkTwoFactorStatus } from "./LoginView";
|
||||
import http from "../http";
|
||||
import { toast } from "react-toastify";
|
||||
import { UserProfile } from "../models/user-profile";
|
||||
|
||||
export default function Manage2FAView({
|
||||
onClose,
|
||||
userProfile,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
userProfile: UserProfile;
|
||||
}) {
|
||||
const [setup2FAStepperCount, setSetup2FAStepperCount] = useState(0);
|
||||
const [disable2FAStepperCount, setDisable2FAStepperCount] = useState(0);
|
||||
const [isTwoFactorEnabled, setIsTwoFactorEnabled] = useState(false);
|
||||
const [setupQRBase64, setSetupQRBase64] = useState("");
|
||||
const [setupBase32Secret, setSetupBase32Secret] = useState("");
|
||||
const [userPassword, setUserPassword] = useState("");
|
||||
|
||||
const disableTwoFactor = async () => {
|
||||
http
|
||||
.post("/User/disable-2fa", {
|
||||
id: userProfile.id,
|
||||
})
|
||||
.then(() => {
|
||||
setDisable2FAStepperCount(2);
|
||||
});
|
||||
};
|
||||
|
||||
const verifyAccount = () => {
|
||||
if (userPassword.length === 0) {
|
||||
return;
|
||||
}
|
||||
http
|
||||
.post("/User/login", {
|
||||
verify: true,
|
||||
email: userProfile.email,
|
||||
password: userPassword,
|
||||
})
|
||||
.then(() => {
|
||||
disableTwoFactor();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Invalid password");
|
||||
});
|
||||
};
|
||||
|
||||
const enableTwoFactor = () => {
|
||||
http.post("/User/enable-2fa").then((response) => {
|
||||
setSetupQRBase64(response.data.qrCodeUrl);
|
||||
setSetupBase32Secret(response.data.secret);
|
||||
setSetup2FAStepperCount(1);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkTwoFactorStatus(userProfile.email)
|
||||
.then((answer) => {
|
||||
setIsTwoFactorEnabled(answer);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{userProfile && (
|
||||
<>
|
||||
{!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
|
||||
className="shadow-medium"
|
||||
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={userProfile.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={() => {
|
||||
setDisable2FAStepperCount(1);
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{disable2FAStepperCount === 1 && (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<p>Let's verify that it's you.</p>
|
||||
<Input
|
||||
type="password"
|
||||
label="Password"
|
||||
value={userPassword}
|
||||
onValueChange={setUserPassword}
|
||||
/>
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<Button
|
||||
onPress={() => {
|
||||
verifyAccount();
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{disable2FAStepperCount === 2 && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import http from "../http";
|
||||
import { toast } from "react-toastify";
|
||||
import { InputOtp } from "@heroui/react";
|
||||
|
||||
export default function TwoFactorsAuthenticationModule({
|
||||
email,
|
||||
onTwoFactorSuccess,
|
||||
}: {
|
||||
email: string;
|
||||
onTwoFactorSuccess: () => void;
|
||||
}) {
|
||||
const [twoFactorToken, setTwoFactorToken] = useState("");
|
||||
const [twoFactorVerifying, setTwoFactorVerifying] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!(twoFactorToken.length == 6 && !twoFactorVerifying)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTwoFactorVerifying(true);
|
||||
http
|
||||
.post("/User/verify-2fa", {
|
||||
email: email,
|
||||
token: twoFactorToken,
|
||||
})
|
||||
.then(() => {
|
||||
onTwoFactorSuccess();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setTwoFactorToken("");
|
||||
setTwoFactorVerifying(false);
|
||||
});
|
||||
}, [twoFactorToken]);
|
||||
return (
|
||||
<div className="text-center flex flex-col gap-4 w-full *:mx-auto">
|
||||
<InputOtp
|
||||
length={6}
|
||||
value={twoFactorToken}
|
||||
onValueChange={setTwoFactorToken}
|
||||
size="lg"
|
||||
isDisabled={twoFactorVerifying}
|
||||
/>
|
||||
<p className="text-sm opacity-50">
|
||||
Please enter the 6 digits passcode from your authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,11 +19,22 @@ import remarkGfm from "remark-gfm";
|
||||
import { IconDownload, IconEdit, IconUpload } from "@tabler/icons-react";
|
||||
import { toast } from "react-toastify";
|
||||
import ChangePasswordView from "../components/ChangePasswordView";
|
||||
import Manage2FAView from "../components/Manage2FAView";
|
||||
|
||||
export default function MemberPage() {
|
||||
const accessToken = getAccessToken();
|
||||
const navigate = useNavigate();
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const {
|
||||
isOpen: changePasswordModalIsOpen,
|
||||
onOpen: changePasswordModalOnOpen,
|
||||
onOpenChange: changePasswordModalOnOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: twoFactorAuthModalIsOpen,
|
||||
onOpen: twoFactorAuthModalOnOpen,
|
||||
onOpenChange: twoFactorAuthModalOnOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
|
||||
@@ -185,13 +196,29 @@ export default function MemberPage() {
|
||||
<Button variant="light" color="danger" onPress={logout}>
|
||||
Log out
|
||||
</Button>
|
||||
<Button variant="light" color="primary" onPress={onOpen}>
|
||||
Change password
|
||||
</Button>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
variant="light"
|
||||
color="primary"
|
||||
onPress={twoFactorAuthModalOnOpen}
|
||||
>
|
||||
Manage 2FA
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="primary"
|
||||
onPress={changePasswordModalOnOpen}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<Modal
|
||||
isOpen={changePasswordModalIsOpen}
|
||||
onOpenChange={changePasswordModalOnOpenChange}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
@@ -204,6 +231,28 @@ export default function MemberPage() {
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={twoFactorAuthModalIsOpen}
|
||||
onOpenChange={twoFactorAuthModalOnOpen}
|
||||
>
|
||||
<ModalContent>
|
||||
{(on2FAClose) => (
|
||||
<>
|
||||
<ModalHeader />
|
||||
<ModalBody>
|
||||
{userProfile && (
|
||||
<Manage2FAView
|
||||
onClose={on2FAClose}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user