|
1 | 1 | <template> |
2 | 2 | <div class="af-two-factors-modal fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 top-0 bottom-0 left-0 right-0" |
3 | | - v-show ="modelShow && (isLoading === false)"> |
4 | | - <div v-if="modalMode === 'totp'" class="af-two-factor-modal-totp flex flex-col items-center relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md"> |
| 3 | + v-show ="twofaApi.isOpened && (isLoading === false)"> |
| 4 | + <div v-if="twofaApi.modalMode === 'totp'" class="af-two-factor-modal-totp flex flex-col items-center relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md"> |
5 | 5 | <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center"> |
6 | | - <p> {{ customDialogTitle }} </p> |
| 6 | + <p> {{ twofaApi.customDialogTitle }} </p> |
7 | 7 | <p>{{ $t('Please enter your authenticator code') }}</p> |
8 | 8 | </div> |
9 | 9 |
|
|
23 | 23 | /> |
24 | 24 | </div> |
25 | 25 |
|
26 | | - <div class="flex items-center w-full" :class="doesUserHavePasskeys ? 'justify-between' : 'justify-center' "> |
27 | | - <p v-if="doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'passkey'" >{{$t('use passkey')}}</p> |
| 26 | + <div class="flex items-center w-full" :class="twofaApi.doesUserHavePasskeys ? 'justify-between' : 'justify-center' "> |
| 27 | + <p v-if="twofaApi.doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="twofaApi.setModalMode('passkey')" >{{$t('use passkey')}}</p> |
28 | 28 | <Button |
29 | 29 | class="px-4 py-2 rounded border" |
30 | 30 | @click="onCancel" |
|
36 | 36 |
|
37 | 37 |
|
38 | 38 |
|
39 | | - <div v-else-if="modalMode === 'passkey'" class="af-two-factor-modal-passkeys flex flex-col items-center justify-center py-4 gap-6 relative bg-white dark:bg-gray-700 rounded-lg shadow p-6"> |
| 39 | + <div v-else-if="twofaApi.modalMode === 'passkey'" class="af-two-factor-modal-passkeys flex flex-col items-center justify-center py-4 gap-6 relative bg-white dark:bg-gray-700 rounded-lg shadow p-6"> |
40 | 40 | <button |
41 | 41 | type="button" |
42 | 42 | class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover" |
|
50 | 50 | <IconShieldOutline class="af-2fa-shield-icon w-16 h-16 text-lightPrimary dark:text-darkPrimary"/> |
51 | 51 | <p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p> |
52 | 52 | <div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200"> |
53 | | - <p class="mb-2">{{customDialogTitle}} </p> |
| 53 | + <p class="mb-2">{{twofaApi.customDialogTitle}} </p> |
54 | 54 | <p>{{$t('Authenticate yourself using the button below')}}</p> |
55 | 55 | </div> |
56 | 56 | <Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16"> |
57 | 57 | {{$t('Use passkey')}} |
58 | 58 | </Button> |
59 | | - <div v-if="modalMode === 'passkey'" class="af-2fa-passkey-issues-card max-w-sm px-6 pt-3 w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700"> |
| 59 | + <div v-if="twofaApi.modalMode === 'passkey'" class="af-2fa-passkey-issues-card max-w-sm px-6 pt-3 w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700"> |
60 | 60 | <div class="mb-3 font-normal text-gray-700 dark:text-gray-400"> |
61 | 61 | <p>{{$t('Have issues with passkey?')}}</p> |
62 | | - <p class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'totp'" >{{$t('use TOTP')}}</p> |
| 62 | + <p class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="twofaApi.setModalMode('totp')" >{{$t('use TOTP')}}</p> |
63 | 63 | </div> |
64 | 64 | </div> |
65 | 65 |
|
|
80 | 80 | import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite'; |
81 | 81 | import { getPasskey } from './utils.js' |
82 | 82 | import adminforth from '@/adminforth'; |
| 83 | + import { use2faApi } from './use2faApi'; |
83 | 84 |
|
| 85 | + const twofaApi = use2faApi(); |
84 | 86 |
|
85 | 87 | declare global { |
86 | 88 | interface Window { |
|
101 | 103 | (e: 'closed'): void |
102 | 104 | }>(); |
103 | 105 |
|
| 106 | + onMounted(() => { |
| 107 | + twofaApi.registerAddEventListenerForOTPInput(addEventListenerForOTPInput); |
| 108 | + }); |
| 109 | +
|
104 | 110 | async function addEventListenerForOTPInput(){ |
105 | 111 | document.addEventListener('focusin', handleGlobalFocusIn, true); |
106 | 112 | focusFirstAvailableOtpInput(); |
|
120 | 126 |
|
121 | 127 |
|
122 | 128 | const modelShow = ref(false); |
123 | | - let resolveFn: ((confirmationResult: any) => void) | null = null; |
124 | | - let verifyingCallback: ((confirmationResult: string) => boolean) | null = null; |
125 | | - let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null; |
126 | | - let rejectFn: ((err?: any) => void) | null = null; |
| 129 | + // let resolveFn: ((confirmationResult: any) => void) | null = null; |
| 130 | + // let verifyingCallback: ((confirmationResult: string) => boolean) | null = null; |
| 131 | + // let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null; |
| 132 | + // let rejectFn: ((err?: any) => void) | null = null; |
127 | 133 |
|
128 | 134 |
|
129 | 135 | window.adminforthTwoFaModal = { |
130 | 136 | get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean>) => |
131 | 137 | new Promise(async (resolve, reject) => { |
132 | | - if (modelShow.value) throw new Error('Modal is already open'); |
| 138 | + if (twofaApi.isOpened) throw new Error('Modal is already open'); |
133 | 139 | const skipAllowModal = await checkIfSkipAllowModal(); |
134 | 140 | if (skipAllowModal) { |
135 | 141 | resolve({ code: "123456" }); // dummy code |
136 | 142 | return; |
137 | 143 | } |
138 | 144 | await checkIfUserHasPasskeys(); |
139 | 145 | if (title) { |
140 | | - customDialogTitle.value = title; |
| 146 | + twofaApi.setCustomDialogTitle(title); |
141 | 147 | } |
142 | | - modelShow.value = true; |
143 | | - if (modalMode.value === 'totp') { |
| 148 | + twofaApi.setIsOpened(true); |
| 149 | + if (twofaApi.modalMode === 'totp') { |
144 | 150 | await addEventListenerForOTPInput(); |
145 | 151 | } |
146 | | - resolveFn = resolve; |
147 | | - rejectFn = reject; |
148 | | - verifyFn = verifyingCallback ?? null; |
| 152 | + twofaApi.registerResolveFn(resolve); |
| 153 | + twofaApi.registerRejectFn(reject); |
| 154 | + twofaApi.registerVerifyFn(verifyingCallback ?? null); |
149 | 155 | }), |
150 | 156 | }; |
151 | 157 | |
|
168 | 174 | onCancel(); |
169 | 175 | return null; |
170 | 176 | } |
171 | | - modelShow.value = false; |
| 177 | + twofaApi.setIsOpened(false); |
172 | 178 | const dataToReturn = { |
173 | 179 | mode: "passkey", |
174 | 180 | result: passkeyData |
175 | 181 | } |
176 | | - customDialogTitle.value = ""; |
| 182 | + twofaApi.setCustomDialogTitle(""); |
177 | 183 | removeEventListenerForOTPInput(); |
178 | | - resolveFn(dataToReturn); |
| 184 | + twofaApi.resolveFn?.(dataToReturn); |
179 | 185 | } |
180 | 186 |
|
181 | 187 | function tagOtpInputs() { |
|
206 | 212 | } |
207 | 213 | |
208 | 214 | async function sendConfirmationResult(value: string) { |
209 | | - if (!resolveFn) throw new Error('Modal is not initialized properly'); |
210 | | - if (verifyFn) { |
| 215 | + if (!twofaApi.resolveFn) throw new Error('Modal is not initialized properly'); |
| 216 | + if (twofaApi.verifyFn) { |
211 | 217 | try { |
212 | | - const ok = await verifyFn(value); |
| 218 | + const ok = await twofaApi.verifyFn(value); |
213 | 219 | if (!ok) { |
214 | | - rejectFn?.(new Error('Invalid code')); |
| 220 | + twofaApi.rejectFn?.(new Error('Invalid code')); |
215 | 221 | return; |
216 | 222 | } |
217 | 223 | } catch (err) { |
218 | | - rejectFn?.(err); |
| 224 | + twofaApi.rejectFn?.(err); |
219 | 225 | return; |
220 | 226 | } |
221 | 227 | } |
222 | 228 |
|
223 | | - modelShow.value = false; |
| 229 | + twofaApi.setIsOpened(false); |
224 | 230 | const dataToReturn = { |
225 | 231 | mode: "totp", |
226 | 232 | result: value |
227 | 233 | } |
228 | | - customDialogTitle.value = ""; |
| 234 | + twofaApi.setCustomDialogTitle(""); |
229 | 235 | removeEventListenerForOTPInput(); |
230 | | - resolveFn(dataToReturn); |
| 236 | + twofaApi.resolveFn?.(dataToReturn); |
231 | 237 | } |
232 | 238 | |
233 | 239 | |
234 | 240 | function onCancel() { |
235 | | - modelShow.value = false; |
| 241 | + twofaApi.setIsOpened(false); |
236 | 242 | bindValue.value = ''; |
237 | 243 | confirmationResult.value?.clearInput(); |
238 | 244 | removeEventListenerForOTPInput(); |
239 | | - rejectFn("Cancel"); |
| 245 | + twofaApi.rejectFn?.(new Error('Cancel')); |
240 | 246 | emit('rejected', new Error('cancelled')); |
241 | 247 | emit('closed'); |
242 | 248 | } |
243 | 249 |
|
244 | | - watch(modalMode, async (newMode) => { |
| 250 | + watch(() => twofaApi.modalMode, async (newMode) => { |
245 | 251 | if (newMode === 'totp') { |
246 | 252 | await addEventListenerForOTPInput(); |
247 | 253 | } else { |
248 | 254 | removeEventListenerForOTPInput(); |
249 | 255 | } |
250 | 256 | }); |
251 | 257 |
|
252 | | - watch(modelShow, async (open) => { |
| 258 | + watch(() => twofaApi.isOpened, async (open) => { |
253 | 259 | if (open) { |
254 | 260 | await nextTick(); |
255 | 261 | const htmlRef = document.querySelector('html'); |
|
258 | 264 | } |
259 | 265 | |
260 | 266 | // Wait for conditional rendering to complete |
261 | | - if (modalMode.value === 'totp' && !isLoading.value) { |
| 267 | + if (twofaApi.modalMode === 'totp' && !isLoading.value) { |
262 | 268 | await nextTick(); |
263 | 269 | setTimeout(() => { |
264 | 270 | tagOtpInputs(); |
|
280 | 286 |
|
281 | 287 | async function checkIfUserHasPasskeys() { |
282 | 288 | isLoading.value = true; |
283 | | - try { |
284 | | - const response = await callAdminForthApi({ |
285 | | - method: 'GET', |
286 | | - path: '/plugin/passkeys/getPasskeys', |
287 | | - }); |
288 | | - |
289 | | - if (response.ok) { |
290 | | - if (response.data.length >= 1) { |
291 | | - doesUserHavePasskeys.value = true; |
292 | | - modalMode.value = "passkey"; |
293 | | - } else { |
294 | | - doesUserHavePasskeys.value = false; |
295 | | - modalMode.value = "totp"; |
296 | | - } |
297 | | - } |
298 | | - } catch (error) { |
299 | | - console.error('Error checking passkeys:', error); |
300 | | - // Fallback to TOTP if there's an error |
301 | | - doesUserHavePasskeys.value = false; |
302 | | - modalMode.value = "totp"; |
303 | | - } finally { |
304 | | - isLoading.value = false; |
| 289 | + const hasPasskeys = await twofaApi.checkIfUserHasPasskeys(); |
| 290 | + if (hasPasskeys) { |
| 291 | + twofaApi.setDoesUserHavePasskeys(true); |
| 292 | + twofaApi.setModalMode("passkey"); |
| 293 | + } else { |
| 294 | + twofaApi.setDoesUserHavePasskeys(false); |
| 295 | + twofaApi.setModalMode("totp"); |
305 | 296 | } |
| 297 | + isLoading.value = false; |
306 | 298 | } |
307 | 299 |
|
308 | 300 | function getOtpInputs() { |
|
0 commit comments