Skip to content

Commit aa9d609

Browse files
Dawaic6janet-barbie
authored andcommitted
two factor authentication on fn
1 parent 708e519 commit aa9d609

File tree

10 files changed

+395
-39
lines changed

10 files changed

+395
-39
lines changed

src/components/Twofactorpopup.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
interface TwoFactorPopupProps {
3+
isOpen: boolean;
4+
error: string;
5+
loading: boolean;
6+
input: string[];
7+
handleInput: (index: number, e: React.ChangeEvent<HTMLInputElement>) => void;
8+
verifyOtp: () => void;
9+
}
10+
const TwoFactorPopup: React.FC<TwoFactorPopupProps> = ({
11+
isOpen,
12+
error,
13+
loading,
14+
input,
15+
handleInput,
16+
verifyOtp,
17+
}) => {
18+
if (!isOpen) return null;
19+
return (
20+
<div className="popup">
21+
<h2>Enter your 2FA code</h2>
22+
{error && <p className="error">{error}</p>}
23+
<div className="otp-inputs">
24+
{input.map((value, index) => (
25+
<input
26+
key={index}
27+
type="text"
28+
maxLength={1}
29+
value={value}
30+
onChange={(e) => handleInput(index, e)}
31+
className="otp-input"
32+
/>
33+
))}
34+
</div>
35+
<button onClick={verifyOtp} disabled={loading}>
36+
{loading ? "Verifying..." : "Verify OTP"}
37+
</button>
38+
</div>
39+
);
40+
};
41+
export default TwoFactorPopup;

src/components/verify2fa.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { useState } from "react";
2+
import { useMutation } from "@apollo/client";
3+
import LOGIN_MUTATION from './LoginMutation';
4+
import VerifyOneTimeCode from './2faMutation';
5+
import TwoFactorPopup from "./Twofactorpopup";
6+
import { useNavigate, useSearchParams } from "react-router-dom";
7+
import { toast } from "react-toastify";
8+
import React from "react";
9+
10+
const LoginWith2FA = () => {
11+
const [isOpen, setIsOpen] = useState(false); // For 2FA popup
12+
const [error, setError] = useState("");
13+
const [loading, setLoading] = useState(false);
14+
const [otpInput, setOtpInput] = useState(Array(6).fill("")); // 6-digit OTP
15+
const [loginUser, setLoginUser] = useState<{ user: { id: string; role: string; } } | null>(null);
16+
const navigate = useNavigate();
17+
const [searchParams] = useSearchParams();
18+
19+
// Move handleError above useMutation hooks
20+
const handleError = (err: any) => {
21+
console.log(err.message);
22+
if (err.networkError) {
23+
toast.error("There was a problem contacting the server");
24+
} else if (err.message.toLowerCase() !== "invalid credentials") {
25+
toast.error("Please wait to be added to a program or cohort");
26+
} else {
27+
setError("Invalid credentials");
28+
}
29+
};
30+
31+
const [login] = useMutation(LOGIN_MUTATION, {
32+
onCompleted: (data) => {
33+
if (data.loginUser.requiresOtp) {
34+
setLoginUser({ user: { id: data.loginUser.id, role: data.loginUser.role } }); // Save user data for post-2FA actions
35+
setIsOpen(true);
36+
} else {
37+
// Call handlePostLogin with just the role
38+
handlePostLogin({ user: { role: data.loginUser.role } });
39+
}
40+
},
41+
onError: handleError,
42+
});
43+
44+
45+
const [verifyOtp] = useMutation(VerifyOneTimeCode, {
46+
onCompleted: (data) => {
47+
if (data.verifyOtp.success) {
48+
if (loginUser) {
49+
handlePostLogin(loginUser); // Continue login flow after OTP verification
50+
setIsOpen(false); // Close the popup
51+
} else {
52+
setError("User not found for post-login.");
53+
}
54+
} else {
55+
setError("Invalid OTP. Please try again.");
56+
}
57+
},
58+
onError: handleError,
59+
});
60+
61+
const handleLogin = async () => {
62+
setLoading(true);
63+
try {
64+
await login({ variables: { loginInput: { /* Include necessary loginInput fields here */ } } });
65+
} finally {
66+
setLoading(false);
67+
}
68+
};
69+
70+
const handlePostLogin = (user: { user: { role: string; } }) => {
71+
toast.success("Login successful");
72+
const redirect = searchParams.get("redirect");
73+
const role = user.user.role;
74+
if (redirect) {
75+
navigate(redirect);
76+
} else {
77+
switch (role) {
78+
case "superAdmin":
79+
navigate("/organizations");
80+
break;
81+
case "admin":
82+
navigate("/trainees");
83+
break;
84+
case "coordinator":
85+
navigate("/trainees");
86+
break;
87+
case "manager":
88+
navigate("/dashboard");
89+
break;
90+
case "ttl":
91+
navigate("/ttl-trainees");
92+
break;
93+
default:
94+
navigate("/performance");
95+
break;
96+
}
97+
}
98+
};
99+
100+
const handleOtpInput = (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
101+
const updatedInput = [...otpInput];
102+
updatedInput[index] = e.target.value;
103+
setOtpInput(updatedInput);
104+
};
105+
106+
const handleOtpVerification = () => {
107+
if (loginUser) {
108+
verifyOtp({ variables: { otp: otpInput.join(""), userId: loginUser.user.id } });
109+
} else {
110+
setError("User not found for OTP verification.");
111+
}
112+
};
113+
114+
return (
115+
<div>
116+
<button onClick={handleLogin}>Login</button>
117+
118+
<TwoFactorPopup
119+
isOpen={isOpen}
120+
error={error}
121+
loading={loading}
122+
input={otpInput}
123+
handleInput={handleOtpInput}
124+
verifyOtp={handleOtpVerification}
125+
/>
126+
</div>
127+
);
128+
};
129+
130+
export default LoginWith2FA;

src/containers/DashRoutes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const ViewTraineeRatings = React.lazy(
4141
const TtlTraineeDashboard = React.lazy(
4242
() => import('../pages/ttlTraineeDashboard'),
4343
);
44+
const LoginWith2fa = React.lazy(
45+
() => import('../pages/LoginWith2fa'),
46+
);
4447

4548
const TraineeRatingDashboard = React.lazy(
4649
() => import('../pages/TraineeRatingDashboard'),
@@ -157,6 +160,7 @@ function DashRoutes() {
157160
<Route path="/team-cards" element={<ManagersCards />} />
158161
<Route path="/teams/cards" element={<CoordinatorCards />} />
159162
<Route path="/ttl-trainees" element={<TtlTraineeDashboard />} />
163+
160164
</Routes>
161165
</Suspense>
162166
</main>

src/containers/Routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import ProtectedRoutes from '../ProtectedRoute';
4141
import RemoveTokenPage from '../utils/RemoveTokenPage';
4242
import PrivateRoute from '../utils/PrivateRoute'
4343
import CalendarConfirmation from '../components/CalendarConfirmation';
44+
import TwoFactorPage from '../pages/LoginWith2fa';
4445

4546
function MainRoutes() {
4647
return (
@@ -127,6 +128,7 @@ function MainRoutes() {
127128
</ProtectedRoutes>
128129
}
129130
/>
131+
<Route path="/users/LoginWith2fa" element={<TwoFactorPage/>}/>
130132
<Route
131133
path="/pricing"
132134
element={

src/pages/LoginWith2fa.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useMutation, gql } from '@apollo/client';
3+
import { useLocation } from 'react-router-dom';
4+
5+
// Define the GraphQL mutation
6+
const LOGIN_WITH_TWO_FACTOR_AUTHENTICATION = gql`
7+
mutation LoginWithTwoFactorAuthentication($email: String!, $otp: String!, $twoWayVerificationToken: String!) {
8+
loginWithTwoFactorAuthentication(email: $email, otp: $otp, twoWayVerificationToken: $twoWayVerificationToken) {
9+
message
10+
token
11+
user {
12+
email
13+
}
14+
}
15+
}
16+
`;
17+
18+
const TwoFactorPage: React.FC = () => {
19+
const [input, setInput] = useState<string[]>(Array(6).fill(''));
20+
const [error, setError] = useState('');
21+
const [loading, setLoading] = useState(false);
22+
23+
const location = useLocation();
24+
const { email, TwoWayVerificationToken } = location.state || {};
25+
26+
useEffect(() => {
27+
alert("Two-Way Verification Token:".concat(TwoWayVerificationToken));
28+
alert("Email:".concat(email));
29+
}, [TwoWayVerificationToken, email]);
30+
31+
const [loginWithTwoFactorAuthentication] = useMutation(LOGIN_WITH_TWO_FACTOR_AUTHENTICATION);
32+
33+
const handleInput = (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
34+
const newInput = [...input];
35+
newInput[index] = e.target.value;
36+
setInput(newInput);
37+
38+
if (e.target.value && index < input.length - 1) {
39+
const nextInput = document.getElementById(`otp-input-${index + 1}`);
40+
nextInput?.focus();
41+
}
42+
};
43+
44+
const verifyOtp = async () => {
45+
setLoading(true);
46+
setError('');
47+
48+
const otp = input.join('');
49+
50+
try {
51+
const { data } = await loginWithTwoFactorAuthentication({
52+
variables: { email, otp, twoWayVerificationToken: TwoWayVerificationToken },
53+
});
54+
55+
if (data?.loginWithTwoFactorAuthentication?.token) {
56+
alert(`Success: ${data.loginWithTwoFactorAuthentication.message}`);
57+
// Continue with authenticated flow or store token if needed
58+
} else {
59+
setError('Invalid OTP. Please try again.');
60+
alert('Error: Invalid OTP. Please try again.');
61+
}
62+
} catch (err) {
63+
setError('An error occurred during verification. Please try again.');
64+
alert('An error occurred during verification. Please try again.');
65+
} finally {
66+
setLoading(false);
67+
}
68+
};
69+
70+
return (
71+
<div className="two-factor-page flex flex-col items-center justify-center min-h-screen bg-gray-100">
72+
<h2 className="text-2xl font-semibold mb-4">Enter your 2FA code</h2>
73+
{error && <p className="text-red-500 mb-2">{error}</p>}
74+
<div className="otp-inputs flex space-x-2 mb-4">
75+
{input.map((value, index) => (
76+
<input
77+
key={index}
78+
id={`otp-input-${index}`}
79+
type="text"
80+
maxLength={1}
81+
value={value}
82+
onChange={(e) => handleInput(index, e)}
83+
className="otp-input w-12 h-12 text-center text-lg border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
84+
/>
85+
))}
86+
</div>
87+
<button
88+
onClick={verifyOtp}
89+
disabled={loading}
90+
className={`px-4 py-2 bg-blue-500 text-white rounded ${loading ? 'opacity-50' : ''}`}
91+
>
92+
{loading ? 'Verifying...' : 'Verify OTP'}
93+
</button>
94+
</div>
95+
);
96+
};
97+
98+
export default TwoFactorPage;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { gql } from '@apollo/client';
2+
3+
export const EnableTwoFactorAuth = gql`
4+
mutation EnableTwoFactorAuth($email: String!) {
5+
enableTwoFactorAuth(email: $email)
6+
}
7+
`;
8+
9+
10+
export const LOGIN_WITH_TWO_FACTOR_AUTHENTICATION = gql`
11+
mutation LoginWithTwoFactorAuthentication(
12+
$id: String
13+
$email: String
14+
$otp: String!
15+
$TwoWayVerificationToken: String!
16+
) {
17+
loginWithTwoFactorAuthentication(
18+
id: $id
19+
email: $email
20+
otp: $otp
21+
TwoWayVerificationToken: $TwoWayVerificationToken
22+
) {
23+
token
24+
user {
25+
id
26+
email
27+
role
28+
}
29+
message
30+
}
31+
}
32+
`;
33+
34+
export const DisableTwoFactorAuth = gql`
35+
mutation DisableTwoFactorAuth($email: String!) {
36+
disableTwoFactorAuth(email: $email)
37+
}
38+
`;

0 commit comments

Comments
 (0)