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+ export 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 ;
0 commit comments