diff --git a/custom/TwoFAModal.vue b/custom/TwoFAModal.vue index bb5e4e3..9f8cd8f 100644 --- a/custom/TwoFAModal.vue +++ b/custom/TwoFAModal.vue @@ -1,45 +1,55 @@ @@ -83,13 +112,15 @@ import websocket from '@/websocket'; import type { AdminUser } from '@/types/Common'; + type TwoFaConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record }; + declare global { interface Window { adminforthTwoFaModal: { get2FaConfirmationResult: ( title?: string, verifyingCallback?: (confirmationResult: string) => Promise - ) => Promise; + ) => Promise; }; } } @@ -100,40 +131,65 @@ const { alert } = useAdminforth(); - let currentSessionId: string | null = null; + const isAwaiting2FAResult = ref(false); + let allowAddNewSessions = true; + const ALLOW_NEW_SESSIONS_PERIOD = 1000; + const sessionsIdsToResolve = ref([]); + + watch(isAwaiting2FAResult, (awaiting) => { + if (awaiting) { + allowAddNewSessions = true; + setTimeout(() => { + if (isAwaiting2FAResult.value) { + allowAddNewSessions = false; + } + }, ALLOW_NEW_SESSIONS_PERIOD); + } + }); + watch( props, () => { if (props.adminUser) { websocket.unsubscribeByPrefix(`/user2fa/`); websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => { - currentSessionId = data.sessionId; + if (!allowAddNewSessions) { + alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'}); + return; + } + sessionsIdsToResolve.value.push(data.sessionId); let confirmationResult; + if (isAwaiting2FAResult.value) { + return; + } try { + isAwaiting2FAResult.value = true; confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(); } catch (error) { - console.error('Error during 2FA confirmation:', error); + console.error(t('Error during 2FA confirmation:', error)); } + isAwaiting2FAResult.value = false; try { const response = await callAdminForthApi({ method: "POST", path: "/plugin/passkeys/resolveVerifyAuto", - body: { confirmationResult, sessionId: data.sessionId } + body: { confirmationResult, sessionsIds: sessionsIdsToResolve.value } }); if (!response.ok && response.error === 'No session ID or confirmation result'){ - alert({message: 'Verification session finished or cancelled.', variant: 'warning'}); + alert({message: t('Verification session finished or cancelled.'), variant: 'warning'}); } else if (!response.ok) { - alert({message: 'Verification failed', variant: 'danger'}); + alert({message: response.error || t('Verification failed'), variant: 'danger'}); } else if (response.ok) { - alert({message: 'Verification successful', variant: 'success'}); + // alert({message: 'Verification successful', variant: 'success'}); } + sessionsIdsToResolve.value = []; } catch (error) { - console.error('Error resolving automatic 2FA verification:', error); + console.error(t('Error resolving automatic 2FA verification:', error)); } - currentSessionId = null; + allowAddNewSessions = true; }); websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => { - if (currentSessionId === data.sessionId && rejectFn && modelShow.value) { + if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) { onCancel(); - currentSessionId = null; + sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId); } }); } @@ -174,7 +230,7 @@ window.adminforthTwoFaModal = { get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise) => new Promise(async (resolve, reject) => { - if (modelShow.value) throw new Error('Modal is already open'); + if (modelShow.value) throw new Error(t('Modal is already open')); const skipAllowModal = await checkIfSkipAllowModal(); if (skipAllowModal) { resolve({ code: "123456" }); // dummy code @@ -251,12 +307,12 @@ } async function sendConfirmationResult(value: string) { - if (!resolveFn) throw new Error('Modal is not initialized properly'); + if (!resolveFn) throw new Error(t('Modal is not initialized properly')); if (verifyFn) { try { const ok = await verifyFn(value); if (!ok) { - rejectFn?.(new Error('Invalid code')); + rejectFn?.(new Error(t('Invalid code'))); return; } } catch (err) { @@ -341,7 +397,7 @@ } } } catch (error) { - console.error('Error checking passkeys:', error); + console.error(t('Error checking passkeys:', error)); // Fallback to TOTP if there's an error doesUserHavePasskeys.value = false; modalMode.value = "totp"; @@ -403,7 +459,7 @@ return false; } } catch (error) { - console.error('Error checking skip allow modal:', error); + console.error(t('Error checking skip allow modal:', error)); return false; } } diff --git a/custom/TwoFactorsConfirmation.vue b/custom/TwoFactorsConfirmation.vue index 1331b86..6ee075f 100644 --- a/custom/TwoFactorsConfirmation.vue +++ b/custom/TwoFactorsConfirmation.vue @@ -7,73 +7,83 @@ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken' }: {}" > - -
-
- -