Skip to content

Commit 97582d7

Browse files
Dawaic6janet-barbie
authored andcommitted
two factor authentication on fn
1 parent 31852e3 commit 97582d7

File tree

8 files changed

+882
-404
lines changed

8 files changed

+882
-404
lines changed

src/containers/DashRoutes.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const ViewTraineeRatings = React.lazy(
4242
const TtlTraineeDashboard = React.lazy(
4343
() => import('../pages/ttlTraineeDashboard'),
4444
);
45+
const LoginWith2fa = React.lazy(
46+
() => import('../pages/LoginWith2fa'),
47+
);
4548

4649
const TraineeRatingDashboard = React.lazy(
4750
() => import('../pages/TraineeRatingDashboard'),

src/containers/Routes.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import RemoveTokenPage from '../utils/RemoveTokenPage';
4242
import PrivateRoute from '../utils/PrivateRoute'
4343
import CalendarConfirmation from '../components/CalendarConfirmation';
4444
import NotFound from '../components/NotFoundPage';
45+
import TwoFactorPage from '../pages/LoginWith2fa';
4546

4647
function MainRoutes() {
4748
return (
@@ -121,13 +122,14 @@ function MainRoutes() {
121122
<Route
122123
path="/users/login"
123124
element={
124-
<ProtectedRoutes>
125+
<ProtectedRoutes>
125126
<Suspense fallback={<Skeleton />}>
126127
<Adminlogin />
127128
</Suspense>
128-
</ProtectedRoutes>
129+
</ProtectedRoutes>
129130
}
130131
/>
132+
<Route path="/users/LoginWith2fa" element={<TwoFactorPage/>}/>
131133
<Route
132134
path="/pricing"
133135
element={

src/pages/LoginWith2fa.tsx

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/* eslint-disable */
2+
import React, { useState, useEffect, useCallback, useContext } from 'react';
3+
import { useMutation, gql, useApolloClient } from '@apollo/client';
4+
import { useLocation, useNavigate } from 'react-router-dom';
5+
import { UserContext } from '../hook/useAuth';
6+
import { toast } from 'react-toastify';
7+
import { useTranslation } from 'react-i18next';
8+
9+
interface Profile {
10+
id: string;
11+
firstName: string;
12+
lastName: string;
13+
name: string | null;
14+
address: string | null;
15+
city: string | null;
16+
country: string | null;
17+
phoneNumber: string | null;
18+
biography: string | null;
19+
avatar: string | null;
20+
cover: string | null;
21+
__typename: 'Profile';
22+
}
23+
24+
interface User {
25+
id: string;
26+
role: string;
27+
email: string;
28+
profile: Profile;
29+
__typename: 'User';
30+
}
31+
32+
interface LoginResponse {
33+
loginWithTwoFactorAuthentication: {
34+
token: string;
35+
user: User;
36+
message: string;
37+
__typename: 'LoginResponse';
38+
};
39+
}
40+
41+
const LOGIN_WITH_2FA = gql`
42+
mutation LoginWithTwoFactorAuthentication(
43+
$email: String!
44+
$otp: String!
45+
$TwoWayVerificationToken: String!
46+
) {
47+
loginWithTwoFactorAuthentication(
48+
email: $email
49+
otp: $otp
50+
TwoWayVerificationToken: $TwoWayVerificationToken
51+
) {
52+
token
53+
user {
54+
id
55+
role
56+
email
57+
profile {
58+
id
59+
firstName
60+
lastName
61+
name
62+
address
63+
city
64+
country
65+
phoneNumber
66+
biography
67+
avatar
68+
cover
69+
}
70+
}
71+
message
72+
}
73+
}
74+
`;
75+
76+
const TwoFactorPage: React.FC = () => {
77+
const [input, setInput] = useState<string[]>(Array(6).fill(''));
78+
const [error, setError] = useState('');
79+
const [loading, setLoading] = useState(false);
80+
const [isDark, setIsDark] = useState(false);
81+
const { login } = useContext(UserContext);
82+
const client = useApolloClient();
83+
const { t } = useTranslation();
84+
85+
const location = useLocation();
86+
const navigate = useNavigate();
87+
const { email, TwoWayVerificationToken } = location.state || {};
88+
useEffect(() => {
89+
// Update document class and localStorage when theme changes
90+
if (isDark) {
91+
document.documentElement.classList.add('dark');
92+
localStorage.setItem('theme', 'dark');
93+
} else {
94+
document.documentElement.classList.remove('dark');
95+
localStorage.setItem('theme', 'light');
96+
}
97+
}, [isDark]);
98+
99+
useEffect(() => {
100+
if (!email || !TwoWayVerificationToken) {
101+
navigate('/login');
102+
}
103+
}, [email, TwoWayVerificationToken, navigate]);
104+
105+
const [loginWithTwoFactorAuthentication] = useMutation<LoginResponse>(LOGIN_WITH_2FA, {
106+
onCompleted: async (data) => {
107+
const response = data.loginWithTwoFactorAuthentication;
108+
try {
109+
localStorage.setItem('authToken', response.token);
110+
localStorage.setItem('user', JSON.stringify(response.user));
111+
await login(response);
112+
await client.resetStore();
113+
toast.success(response.message);
114+
115+
const rolePaths: Record<string, string> = {
116+
superAdmin: '/organizations',
117+
admin: '/trainees',
118+
coordinator: '/trainees',
119+
manager: '/dashboard',
120+
ttl: '/ttl-trainees',
121+
trainee: '/performance'
122+
};
123+
124+
const redirectPath = rolePaths[response.user.role] || '/dashboard';
125+
navigate(redirectPath, { replace: true });
126+
} catch (error) {
127+
128+
toast.error(('Login Error'));
129+
}
130+
},
131+
onError: (error) => {
132+
const errorMessage = error.message || ('Verification Failed');
133+
setError(errorMessage);
134+
toast.error(errorMessage);
135+
setInput(Array(6).fill(''));
136+
}
137+
});
138+
139+
const verifyOtp = async (currentInput = input) => {
140+
if (currentInput.some(val => !val)) {
141+
setError(('Please Enter All Digits'));
142+
return;
143+
}
144+
145+
setLoading(true);
146+
setError('');
147+
148+
try {
149+
await loginWithTwoFactorAuthentication({
150+
variables: {
151+
email,
152+
otp: currentInput.join(''),
153+
TwoWayVerificationToken
154+
}
155+
});
156+
} finally {
157+
setLoading(false);
158+
}
159+
};
160+
161+
const handleInput = useCallback((index: number, value: string) => {
162+
if (!/^\d*$/.test(value)) return;
163+
164+
const newInput = [...input];
165+
newInput[index] = value;
166+
setInput(newInput);
167+
168+
if (value && index < input.length - 1) {
169+
const nextInput = document.getElementById(`otp-input-${index + 1}`) as HTMLInputElement;
170+
nextInput?.focus();
171+
}
172+
173+
if (value && index === input.length - 1) {
174+
const allFilled = newInput.every(val => val !== '');
175+
if (allFilled) {
176+
verifyOtp(newInput);
177+
}
178+
}
179+
}, [input]);
180+
181+
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
182+
if (e.key === 'Backspace' && !input[index] && index > 0) {
183+
const prevInput = document.getElementById(`otp-input-${index - 1}`) as HTMLInputElement;
184+
prevInput?.focus();
185+
}
186+
};
187+
188+
const handlePaste = (e: React.ClipboardEvent) => {
189+
e.preventDefault();
190+
const pastedData = e.clipboardData.getData('text').trim();
191+
192+
if (!/^\d+$/.test(pastedData)) {
193+
setError(('Only Numbers Can Be Pasted'));
194+
return;
195+
}
196+
197+
const digits = pastedData.slice(0, 6).split('');
198+
const newInput = [...digits, ...Array(6 - digits.length).fill('')];
199+
200+
setInput(newInput);
201+
202+
if (digits.length < 6) {
203+
const nextEmptyIndex = digits.length;
204+
const nextInput = document.getElementById(`otp-input-${nextEmptyIndex}`) as HTMLInputElement;
205+
nextInput?.focus();
206+
} else {
207+
verifyOtp(newInput);
208+
}
209+
};
210+
211+
const toggleTheme = () => {
212+
setIsDark(!isDark);
213+
};
214+
215+
return (
216+
<div className="flex flex-col items-center justify-center min-h-screen transition-colors duration-200 bg-gray-100 dark:bg-gray-900">
217+
<div className="p-8 transition-colors duration-200 bg-white rounded-lg shadow-md dark:bg-gray-800 w-96">
218+
<h2 className="mb-6 text-2xl font-semibold text-center text-gray-800 dark:text-gray-100">
219+
{('Verification Required')}
220+
</h2>
221+
222+
<p className="mb-6 text-sm text-center text-gray-600 dark:text-gray-400">
223+
{('Enter Verification Code')}<br/>
224+
<span className="font-medium">{email}</span>
225+
</p>
226+
227+
{error && (
228+
<div className="p-3 mb-4 text-sm text-red-500 bg-red-100 rounded dark:bg-red-900/30">
229+
{error}
230+
</div>
231+
)}
232+
233+
<form onSubmit={(e) => {
234+
e.preventDefault();
235+
verifyOtp();
236+
}}>
237+
<div className="flex justify-center mb-6 space-x-2">
238+
{input.map((value, index) => (
239+
<input
240+
key={index}
241+
id={`otp-input-${index}`}
242+
type="text"
243+
inputMode="numeric"
244+
maxLength={1}
245+
value={value}
246+
onChange={(e) => handleInput(index, e.target.value)}
247+
onKeyDown={(e) => handleKeyDown(index, e)}
248+
onPaste={index === 0 ? handlePaste : undefined}
249+
className="w-12 h-12 text-lg font-semibold text-center text-gray-800 transition-colors bg-white border rounded dark:text-gray-100 dark:border-gray-600 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
250+
disabled={loading}
251+
autoComplete="one-time-code"
252+
required
253+
/>
254+
))}
255+
</div>
256+
257+
<button
258+
type="submit"
259+
disabled={loading || input.some(val => !val)}
260+
className="w-full py-3 text-white transition-colors bg-primary rounded hover:bg-blue-600 disabled:bg-primary dark:disabled:bg-primary disabled:cursor-not-allowed"
261+
>
262+
{loading ? (
263+
<span className="flex items-center justify-center">
264+
{('Verifying')}
265+
</span>
266+
) : (
267+
('VerifyCode')
268+
)}
269+
</button>
270+
</form>
271+
272+
273+
</div>
274+
</div>
275+
);
276+
};
277+
278+
export default TwoFactorPage;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 LoginWithTwoFactorAuthentication= gql`
11+
mutation LoginWithTwoFactorAuthentication($email: String!, $otp: String!, $twoWayVerificationToken: String!) {
12+
loginWithTwoFactorAuthentication(email: $email, otp: $otp, TwoWayVerificationToken: $twoWayVerificationToken) {
13+
message
14+
token
15+
user {
16+
email
17+
}
18+
}
19+
}
20+
`;
21+
22+
export const DisableTwoFactorAuth = gql`
23+
mutation DisableTwoFactorAuth($email: String!) {
24+
disableTwoFactorAuth(email: $email)
25+
}
26+
`;

0 commit comments

Comments
 (0)