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