|
68 | 68 | </template> |
69 | 69 |
|
70 | 70 | <script setup lang="ts"> |
| 71 | +
|
71 | 72 | import VOtpInput from 'vue3-otp-input'; |
72 | 73 | import { ref, nextTick, watch, onMounted } from 'vue'; |
73 | 74 | import { useUserStore } from '@/stores/user'; |
|
98 | 99 | (e: 'closed'): void |
99 | 100 | }>(); |
100 | 101 |
|
101 | | - function removeListeners() { |
| 102 | + async function addEventListenerForOTPInput(){ |
| 103 | + document.addEventListener('focusin', handleGlobalFocusIn, true); |
| 104 | + focusFirstAvailableOtpInput(); |
| 105 | + isLoading.value = false; |
| 106 | + await nextTick(); |
| 107 | + const rootEl = otpRoot.value; |
| 108 | + rootEl && rootEl.addEventListener('focusout', handleFocusOut, true); |
| 109 | + } |
| 110 | +
|
| 111 | + function removeEventListenerForOTPInput() { |
102 | 112 | window.removeEventListener('paste', handlePaste); |
103 | 113 | document.removeEventListener('focusin', handleGlobalFocusIn, true); |
104 | 114 | const rootEl = otpRoot.value; |
105 | 115 | rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true); |
| 116 | + // Abort any in-flight WebAuthn request when leaving the component |
106 | 117 | } |
107 | 118 |
|
108 | | - async function addListeners() { |
109 | | - document.addEventListener('focusin', handleGlobalFocusIn, true); |
110 | | - |
111 | | - // Wait for DOM to be ready and OTP inputs to be rendered |
112 | | - await nextTick(); |
113 | | - await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for v-otp-input to render |
114 | | - |
115 | | - focusFirstAvailableOtpInput(); |
116 | | - const rootEl = otpRoot.value; |
117 | | - rootEl && rootEl.addEventListener('focusout', handleFocusOut, true); |
118 | | - } |
119 | 119 |
|
120 | 120 | const modelShow = ref(false); |
121 | | - let resolveFn: ((confirmationResult: string) => void) | null = null; |
| 121 | + let resolveFn: ((confirmationResult: any) => void) | null = null; |
122 | 122 | let verifyingCallback: ((confirmationResult: string) => boolean) | null = null; |
123 | 123 | let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null; |
124 | 124 | let rejectFn: ((err?: any) => void) | null = null; |
|
133 | 133 | customDialogTitle.value = title; |
134 | 134 | } |
135 | 135 | modelShow.value = true; |
136 | | - await addListeners(); |
| 136 | + if (modalMode.value === 'totp') { |
| 137 | + await addEventListenerForOTPInput(); |
| 138 | + } |
137 | 139 | resolveFn = resolve; |
138 | 140 | rejectFn = reject; |
139 | 141 | verifyFn = verifyingCallback ?? null; |
|
163 | 165 | return null; |
164 | 166 | } else if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) { |
165 | 167 | adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' }); |
| 168 | + onCancel(); |
166 | 169 | return null; |
167 | 170 | } else if (name === 'NotAllowedError') { |
168 | 171 | adminforth.alert({ message: `The operation either timed out or was not allowed`, variant: 'warning' }); |
| 172 | + onCancel(); |
169 | 173 | return null; |
170 | 174 | } else { |
171 | 175 | adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'}); |
| 176 | + onCancel(); |
172 | 177 | return null; |
173 | 178 | } |
174 | | - onCancel(); |
175 | 179 | } |
176 | 180 | modelShow.value = false; |
177 | 181 | const dataToReturn = { |
178 | 182 | mode: "passkey", |
179 | 183 | result: passkeyData |
180 | 184 | } |
181 | 185 | customDialogTitle.value = ""; |
182 | | - removeListeners(); |
| 186 | + removeEventListenerForOTPInput(); |
183 | 187 | resolveFn(dataToReturn); |
184 | 188 | } |
185 | 189 |
|
|
231 | 235 | result: value |
232 | 236 | } |
233 | 237 | customDialogTitle.value = ""; |
234 | | - removeListeners(); |
| 238 | + removeEventListenerForOTPInput(); |
235 | 239 | resolveFn(dataToReturn); |
236 | 240 | } |
237 | 241 | |
|
240 | 244 | modelShow.value = false; |
241 | 245 | bindValue.value = ''; |
242 | 246 | confirmationResult.value?.clearInput(); |
243 | | - removeListeners(); |
| 247 | + removeEventListenerForOTPInput(); |
244 | 248 | rejectFn("Cancel"); |
245 | 249 | emit('rejected', new Error('cancelled')); |
246 | 250 | emit('closed'); |
247 | 251 | } |
248 | 252 |
|
249 | 253 | watch(modalMode, async (newMode) => { |
250 | | - if (newMode === 'totp' && modelShow.value && !isLoading.value) { |
251 | | - await nextTick(); |
252 | | - setTimeout(() => { |
253 | | - tagOtpInputs(); |
254 | | - focusFirstAvailableOtpInput(); |
255 | | - }, 100); |
| 254 | + if (newMode === 'totp') { |
| 255 | + await addEventListenerForOTPInput(); |
| 256 | + } else { |
| 257 | + removeEventListenerForOTPInput(); |
256 | 258 | } |
257 | 259 | }); |
258 | 260 |
|
|
287 | 289 |
|
288 | 290 | async function checkIfUserHasPasskeys() { |
289 | 291 | isLoading.value = true; |
290 | | - callAdminForthApi({ |
291 | | - method: 'GET', |
292 | | - path: '/plugin/passkeys/getPasskeys', |
293 | | - }).then((response) => { |
| 292 | + try { |
| 293 | + const response = await callAdminForthApi({ |
| 294 | + method: 'GET', |
| 295 | + path: '/plugin/passkeys/getPasskeys', |
| 296 | + }); |
| 297 | + |
294 | 298 | if (response.ok) { |
295 | 299 | if (response.data.length >= 1) { |
296 | 300 | doesUserHavePasskeys.value = true; |
297 | 301 | modalMode.value = "passkey"; |
298 | | - isLoading.value = false; |
299 | 302 | } else { |
300 | 303 | doesUserHavePasskeys.value = false; |
301 | 304 | modalMode.value = "totp"; |
302 | | - isLoading.value = false; |
303 | 305 | } |
304 | 306 | } |
305 | | - }); |
| 307 | + } catch (error) { |
| 308 | + console.error('Error checking passkeys:', error); |
| 309 | + // Fallback to TOTP if there's an error |
| 310 | + doesUserHavePasskeys.value = false; |
| 311 | + modalMode.value = "totp"; |
| 312 | + } finally { |
| 313 | + isLoading.value = false; |
| 314 | + } |
306 | 315 | } |
307 | 316 |
|
308 | 317 | function getOtpInputs() { |
|
0 commit comments