11import QRCode from 'qrcode' ;
2- import { ref , onUnmounted } from 'vue' ;
2+ import { useToast } from 'vue-toastification' ;
3+
4+ const toast = useToast ( ) ;
35
4- // TOTP configuration
56const TOTP_CONFIG = {
67 digits : 6 ,
7- timeStep : 30 , // seconds
8+ timeStep : 60 ,
89} ;
910
1011export function useOneTimeQrCode ( ) {
@@ -14,16 +15,10 @@ export function useOneTimeQrCode() {
1415 const isLoading = ref < boolean > ( false ) ;
1516 let intervalId : NodeJS . Timeout | null = null ;
1617
17- /**
18- * Convert string to Uint8Array
19- */
2018 const stringToBytes = ( str : string ) : Uint8Array => {
2119 return new TextEncoder ( ) . encode ( str ) ;
2220 } ;
2321
24- /**
25- * Generate HMAC using Web Crypto API
26- */
2722 const generateHMAC = async ( key : Uint8Array , message : Uint8Array ) : Promise < ArrayBuffer > => {
2823 const cryptoKey = await crypto . subtle . importKey (
2924 'raw' ,
@@ -46,7 +41,6 @@ export function useOneTimeQrCode() {
4641 const userSecret = secret || `LabSyncro-${ userId } -${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } ` ;
4742 const counter = Math . floor ( Date . now ( ) / 1000 / TOTP_CONFIG . timeStep ) ;
4843
49- // Convert counter to 8-byte buffer
5044 const counterBytes = new Uint8Array ( 8 ) ;
5145 let tempCounter = counter ;
5246 for ( let i = counterBytes . length - 1 ; i >= 0 ; i -- ) {
@@ -67,13 +61,11 @@ export function useOneTimeQrCode() {
6761 ( hmacArray [ offset + 2 ] << 8 ) |
6862 hmacArray [ offset + 3 ] ;
6963
70- // Get last n digits
7164 code = code % Math . pow ( 10 , TOTP_CONFIG . digits ) ;
72-
73- // Pad with leading zeros if needed
74- return code . toString ( ) . padStart ( TOTP_CONFIG . digits , '0' ) ;
65+
66+ return code . toString ( ) . padStart ( TOTP_CONFIG . digits , "0" ) ;
7567 } catch ( error ) {
76- console . error ( 'Error generating token:' , error ) ;
68+ toast . error ( `Lỗi khi tạo mã QR: ${ error } ` ) ;
7769 throw error ;
7870 }
7971 } ;
@@ -90,7 +82,7 @@ export function useOneTimeQrCode() {
9082 const currentToken = await generateToken ( userId , secret ) ;
9183 return token === currentToken ;
9284 } catch ( error ) {
93- console . error ( 'Error verifying token:' , error ) ;
85+ toast . error ( `Lỗi khi xác thực mã QR: ${ error } ` ) ;
9486 return false ;
9587 }
9688 } ;
@@ -102,55 +94,67 @@ export function useOneTimeQrCode() {
10294 */
10395 const generateQrCode = async ( userId : string , extraData ?: Record < string , any > ) => {
10496 try {
105- isLoading . value = true ;
97+ if ( intervalId ) {
98+ clearInterval ( intervalId ) ;
99+ intervalId = null ;
100+ }
106101
107- // Generate token
102+ isLoading . value = true ;
108103 const token = await generateToken ( userId ) ;
109104
110- // Calculate token expiry time
111- const now = Math . floor ( Date . now ( ) / 1000 ) ;
112- const step = TOTP_CONFIG . timeStep ;
113- expiryTimestamp . value = ( Math . floor ( now / step ) + 1 ) * step * 1000 ;
105+ const now = Date . now ( ) ;
106+ const newExpiryTimestamp = now + ( TOTP_CONFIG . timeStep * 1000 ) ;
114107
115- // Create data object to be encoded in QR code
116108 const qrData = JSON . stringify ( {
117109 token,
118110 userId,
119- timestamp : Date . now ( ) ,
120- expiry : expiryTimestamp . value ,
111+ timestamp : now ,
112+ expiry : newExpiryTimestamp ,
121113 ...extraData ,
122114 } ) ;
123115
124- // Generate QR code as data URL
125116 qrDataUrl . value = await QRCode . toDataURL ( qrData ) ;
117+ expiryTimestamp . value = newExpiryTimestamp ;
118+ timeLeft . value = TOTP_CONFIG . timeStep ;
126119
127- // Start the countdown
128- startCountdown ( ) ;
129120 } catch ( error ) {
130- console . error ( 'Error generating QR code:' , error ) ;
121+ toast . error ( `Lỗi khi tạo mã QR: ${ error } ` ) ;
122+ qrDataUrl . value = '' ;
123+ expiryTimestamp . value = 0 ;
124+ timeLeft . value = 0 ;
131125 } finally {
132126 isLoading . value = false ;
133127 }
134128 } ;
135129
136- /**
137- * Start countdown for token expiry
138- */
139130 const startCountdown = ( ) => {
140- // Clear any existing interval
141- if ( intervalId ) clearInterval ( intervalId ) ;
142-
143- // Update time left on interval
144- intervalId = setInterval ( ( ) => {
145- const now = Date . now ( ) ;
146- if ( now >= expiryTimestamp . value ) {
147- // Token expired, regenerate
148- timeLeft . value = 0 ;
149- if ( intervalId ) clearInterval ( intervalId ) ;
150- } else {
151- timeLeft . value = Math . floor ( ( expiryTimestamp . value - now ) / 1000 ) ;
131+ if ( intervalId ) {
132+ clearInterval ( intervalId ) ;
133+ intervalId = null ;
134+ }
135+
136+ if ( expiryTimestamp . value > Date . now ( ) ) {
137+ const initialTimeLeft = Math . max ( 0 , Math . floor ( ( expiryTimestamp . value - Date . now ( ) ) / 1000 ) ) ;
138+ if ( timeLeft . value !== initialTimeLeft ) {
139+ timeLeft . value = initialTimeLeft ;
152140 }
153- } , 1000 ) ;
141+
142+ intervalId = setInterval ( ( ) => {
143+ const now = Date . now ( ) ;
144+ if ( now >= expiryTimestamp . value ) {
145+ timeLeft . value = 0 ;
146+ clearInterval ( intervalId ! ) ;
147+ intervalId = null ;
148+ } else {
149+ const newTimeLeft = Math . max ( 0 , Math . floor ( ( expiryTimestamp . value - now ) / 1000 ) ) ;
150+ if ( timeLeft . value !== newTimeLeft ) {
151+ timeLeft . value = newTimeLeft ;
152+ }
153+ }
154+ } , 1000 ) ;
155+ } else {
156+ timeLeft . value = 0 ;
157+ }
154158 } ;
155159
156160 /**
@@ -160,43 +164,43 @@ export function useOneTimeQrCode() {
160164 */
161165 const verifyScannedQrCode = async ( scannedQrData : string ) : Promise < { userId : string ; extraData ?: any } | null > => {
162166 try {
163- // Parse the QR data
164167 const qrData = JSON . parse ( scannedQrData ) ;
165168 const { token, userId, timestamp, expiry, ...extraData } = qrData ;
166169
167- // Check if token has expired
168170 if ( Date . now ( ) > expiry ) {
169- console . error ( 'Token has expired' ) ;
171+ toast . error ( "Mã QR đã hết hạn" ) ;
170172 return null ;
171173 }
172-
173- // Verify the token
174+
174175 const isValid = await verifyToken ( token , userId ) ;
175176 if ( ! isValid ) {
176- console . error ( 'Invalid token' ) ;
177+ toast . error ( "Mã QR không hợp lệ hoặc đã hết hạn" ) ;
177178 return null ;
178179 }
179-
180- // Token is valid, return user ID and any extra data
180+
181181 return { userId, extraData } ;
182182 } catch ( error ) {
183- console . error ( 'Error verifying QR code:' , error ) ;
183+ toast . error ( `Lỗi khi xác thực mã QR: ${ error } ` ) ;
184184 return null ;
185185 }
186186 } ;
187187
188- /**
189- * Clean up resources when component unmounts
190- */
191188 const cleanUp = ( ) => {
192- if ( intervalId ) clearInterval ( intervalId ) ;
189+ if ( intervalId ) {
190+ clearInterval ( intervalId ) ;
191+ intervalId = null ;
192+ }
193193 qrDataUrl . value = '' ;
194194 expiryTimestamp . value = 0 ;
195195 timeLeft . value = 0 ;
196+ isLoading . value = false ;
196197 } ;
197198
198199 onUnmounted ( ( ) => {
199- cleanUp ( ) ;
200+ if ( intervalId ) {
201+ clearInterval ( intervalId ) ;
202+ intervalId = null ;
203+ }
200204 } ) ;
201205
202206 return {
@@ -207,6 +211,7 @@ export function useOneTimeQrCode() {
207211 verifyToken,
208212 generateQrCode,
209213 verifyScannedQrCode,
214+ startCountdown,
210215 cleanUp,
211216 } ;
212217}
0 commit comments