Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fad1636
fix: allow to resolve multiple 2fa calls for the verifyAuto calls
yaroslav8765 Apr 28, 2026
1a2c3e1
fix: improve verifyAuto multiple calls handling and security
yaroslav8765 Apr 28, 2026
298dcdd
fix: fix security problems of the verifyAuto call
yaroslav8765 Apr 29, 2026
bc6e07d
feat: enhance TwoFAModal and TwoFactorsPasskeysSettings for improved …
SerVitasik Apr 29, 2026
a9545a3
Merge branch 'main' of https://github.com/devforth/adminforth-two-fac…
SerVitasik Apr 29, 2026
30f5fc0
fix: update error messages and translations in 2FA components
kulikp1 Apr 29, 2026
0654780
fix: update translation logic for multiple actions in 2FA modal
kulikp1 Apr 30, 2026
a4e5d31
fix: update adminforth verion
yaroslav8765 Apr 30, 2026
8b78beb
Merge pull request #24 from devforth/feature/AdminForth/1539/check-tr…
SerVitasik May 1, 2026
80748d4
fix: update adminforth verion
SerVitasik May 1, 2026
dcf0bea
fix: improve error messaging in TwoFAModal for better user feedback
kulikp1 May 5, 2026
742d560
Merge branch 'main' into feature/AdminForth/1540/show-proper-error-me…
kulikp1 May 5, 2026
500becc
Merge pull request #25 from devforth/feature/AdminForth/1540/show-pro…
SerVitasik May 5, 2026
3efb1e7
feat: enhance TwoFactorsConfirmation modal with improved layout and u…
kulikp1 May 6, 2026
2ef7b76
Merge pull request #26 from devforth/feature/AdminForth/1565/improve-…
SerVitasik May 7, 2026
d66a81e
fix: change toast type to error for passkey authentication failures
kulikp1 May 7, 2026
3a70ea0
fix: streamline error handling for passkey authentication by removing…
kulikp1 May 7, 2026
ddc7710
Merge branch 'main' into feature/AdminForth/1566/change-toast-type-to…
kulikp1 May 7, 2026
89b51cb
fix: enhance waitForResponse method with timeout handling and error r…
NoOne7135 May 7, 2026
fe59bc3
fix: dont show success toast when user resolves 2fa modal successfully
yaroslav8765 May 8, 2026
4590cf8
Merge branch 'main' of https://github.com/devforth/adminforth-two-fac…
yaroslav8765 May 8, 2026
88fd9f2
fix: update error handling for authentication to provide more specifi…
kulikp1 May 8, 2026
d3e01d5
fix: enhance error handling for authentication by adding a generic er…
kulikp1 May 8, 2026
234c580
fix: enhance error handling for authentication by adding a generic er…
kulikp1 May 8, 2026
a8a1fb4
return comment
kulikp1 May 8, 2026
e15cc21
fix: remove redundant error handling for NotAllowedError in OTP input…
kulikp1 May 8, 2026
362db37
Merge pull request #27 from devforth/feature/AdminForth/1566/change-t…
SerVitasik May 8, 2026
2024252
fix: update error response for failed verification to include status …
kulikp1 May 15, 2026
91f945f
fix: update response status handling for verification failure to use …
kulikp1 May 15, 2026
84e093c
fix: simplify error response for verification failure by removing red…
kulikp1 May 15, 2026
43a505c
fix: enhance error handling for passkey creation with specific messag…
kulikp1 May 15, 2026
651adef
fix: add new case for error
kulikp1 May 15, 2026
520f7fd
fix: update error message for verification failure to specify wrong o…
kulikp1 May 15, 2026
cdc90b0
fix: change alert variant to 'danger' for timeout or not allowed oper…
kulikp1 May 15, 2026
e411aaa
fix: update adminforth dependency version to ^2.66.0 in package.json …
kulikp1 May 22, 2026
cc25ab8
Merge pull request #30 from devforth/feature/AdminForth/1611/display-…
kulikp1 May 22, 2026
ceee634
fix: improve error handling and validation for 2FA confirmation process
NoOne7135 May 25, 2026
3d2c6cb
fix: update response status message for wrong or expired TOTP code
NoOne7135 May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 153 additions & 82 deletions custom/TwoFAModal.vue

Large diffs are not rendered by default.

173 changes: 102 additions & 71 deletions custom/TwoFactorsConfirmation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,83 @@
'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
}: {}"
>

<div v-if="isLoading===false" id="authentication-modal" tabindex="-1" class="af-two-factors-confirmation overflow-y-auto overflow-x-hidden z-50 min-w-[00px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="af-login-popup relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" :class="codeError ? 'rounded-b-none' : ''">
<div class="p-8 w-full max-w-md max-h-full custom-auth-wrapper" >
<div v-if="confirmationMode === 'code'" class="af-totp-confirmation">
<div id="mfaCode-label" class="mx-4">{{$t('Please enter your authenticator code')}} </div>
<div class="mt-4 w-full flex flex-col gap-4 justify-center" ref="otpRoot">
<v-otp-input
ref="code"
container-class="grid grid-cols-6 gap-3 w-full"
input-classes="bg-gray-50 text-center justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
:num-inputs="6"
inputType="number"
inputmode="numeric"
:should-auto-focus="true"
:should-focus-order="true"
v-model:value="bindValue"
@on-complete="handleOnComplete"
/>
<div class="flex items-center justify-between w-full">
<Link v-if="confirmationMode === 'code' && doesUserHavePasskeys" :to="{ hash: '#passkey' }" class="w-max underline hover:no-underline hover:cursor-pointer text-lightPrimary whitespace-nowrap">{{$t('Use passkey')}}</Link>
<Link
v-if="confirmationMode === 'code'"
to="/login"
class="w-max"
>
{{$t('Back to login')}}
</Link>
</div>
</div>
</div>
<div v-else class="af-passkey-confirmation flex flex-col items-center justify-center py-4 gap-6">
<IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
<p class="text-4xl font-semibold mb-4">{{$t('Passkey')}}</p>
<p class="mb-2 max-w-[300px]">{{$t('When you are ready, authenticate using the button below')}}</p>
<Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
{{$t('Use passkey')}}
</Button>
<div v-if="confirmationMode === 'passkey'" class="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">
<p class="mb-3 font-normal text-gray-700 dark:text-gray-400">
{{$t('Have issues with passkey?')}}
<div v-if="doesUserHavePasskeys" class="flex justify-start cursor-pointer gap-2" >
<Link v-if="confirmationMode === 'passkey'" :to="{ hash: '#code' }" class="underline hover:no-underline text-lightPrimary whitespace-nowrap">{{$t('use TOTP')}}</Link>
<p> {{$t('or')}}</p>
<Link
to="/login"
class="w-full"
>
{{$t('back to login')}}
</Link>
</div>
</p>
</div>
</div>
<div v-if="isLoading===false" id="authentication-modal" class="af-two-factors-confirmation flex items-center justify-center w-full p-4">
<div class="relative w-full max-w-md">

<div class="af-login-popup relative bg-white dark:bg-gray-700 rounded-lg shadow p-6" :class="codeError ? 'rounded-b-none' : ''">

<div class="af-2fa-header flex flex-col items-center justify-center gap-3 mb-6">
<div class="af-2fa-icon-wrap w-14 h-14 shrink-0 flex items-center justify-center rounded-full bg-lightPrimary dark:bg-darkPrimary">
<IconShieldOutline class="af-2fa-shield-icon w-7 h-7 text-white" />
</div>
<div class="af-2fa-title-wrap text-center">
<p class="text-xl font-medium text-gray-900 dark:text-white">
{{ confirmationMode === 'code' ? $t('Two-factor Auth') : $t('Passkey') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ confirmationMode === 'code'
? $t('Please enter your authenticator code')
: $t('When you are ready, authenticate using the button below')
}}
</p>
</div>
</div>

<div v-if="confirmationMode === 'code'" class="af-2fa-otp-root flex flex-col items-center gap-6" ref="otpRoot">
<v-otp-input
ref="code"
container-class="grid grid-cols-6 gap-3"
input-classes="bg-gray-50 text-center flex justify-center otp-input border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
:num-inputs="6"
inputType="number"
inputmode="numeric"
:should-auto-focus="true"
v-model:value="bindValue"
@on-complete="handleOnComplete"
/>

<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
<p>
{{$t('Having trouble?')}}
<button v-if="doesUserHavePasskeys" type="button"
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
@click="router.push({ hash: '#passkey' })">
{{$t('Use passkey instead')}}
</button>
<span v-if="doesUserHavePasskeys" class="mx-1">{{$t('or')}}</span>
<Link to="/login" class="hover-link">
{{$t('Back to login')}}
</Link>
</p>
</div>
</div>

<div v-else class="af-2fa-passkey-root flex flex-col gap-6">
<Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="af-2fa-passkey-btn w-full flex items-center justify-center gap-2">
<IconShieldOutline class="w-4 h-4" />
{{$t('Use passkey to verify')}}
</Button>

<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
<p>
{{$t('Having trouble?')}}
<button type="button"
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
@click="router.push({ hash: '#code' })">
{{$t('Use TOTP instead')}}
</button>
<span class="mx-1">{{$t('or')}}</span>
<Link to="/login" class="hover-link">
{{$t('Back to login')}}
</Link>
</p>
</div>
</div>
</div>
<div
v-if="codeError"
class="af-two-factors-confirmation-error relative top-full left-0 bg-red-100 text-red-700 text-sm px-2 py-2 rounded-b-lg shadow"
>
<p class="pl-6">{{ codeError }} </p>

<div v-if="codeError" class="af-two-factors-confirmation-error relative bg-red-100 text-red-700 text-xs px-4 py-2 rounded-b-lg shadow border-t border-red-200 text-center">
{{ codeError }}
</div>
<div v-else class="h-[36px] opacity-0">

</div>
</div>
</div>
<div v-else>
Expand Down Expand Up @@ -299,7 +309,7 @@
}
});
} catch (error) {
console.error('Error checking if user has passkeys:', error);
console.error(t('Error checking if user has passkeys:', error));
}
}

Expand All @@ -319,7 +329,7 @@
options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
} catch (e) {
console.error('Error parsing request options:', e);
adminforth.alert({message: t('Error initiating passkey authentication.'), variant: 'warning'});
adminforth.alert({message: t('Error initiating passkey authentication.'), variant: 'danger'});
return;
}
const credential = await authenticate(options);
Expand All @@ -345,13 +355,13 @@
method: 'POST',
});
} catch (error) {
console.error('Error creating sign-in request:', error);
console.error(t('Error creating sign-in request:', error));
return;
}
if (response.ok === true) {
return { _options: response.data, challengeId: response.challengeId };
} else {
adminforth.alert({message: t('Error creating sign-in request.'), variant: 'warning'});
adminforth.alert({message: t('Error creating sign-in request.'), variant: 'danger'});
codeError.value = 'Error creating sign-in request.';
}
}
Expand All @@ -378,7 +388,7 @@
});
return credential;
} catch (error) {
console.error('Error during authentication:', error);
console.error(t('Error during authentication:', error));
// Handle specific concurrent/pending request error cases gracefully
const name = (error && (error.name || error.constructor?.name)) || '';
const message = (error && error.message) || '';
Expand All @@ -390,7 +400,7 @@
codeError.value = t('A previous passkey attempt was still pending. Please try again.');
return null;
} else if (name === 'NotAllowedError') {
adminforth.alert({ message: t('The operation either timed out or was not allowed'), variant: 'warning' });
adminforth.alert({ message: t('The operation either timed out or was not allowed'), variant: 'danger' });
codeError.value = t('The operation either timed out or was not allowed.');
return null;
} else {
Expand Down Expand Up @@ -477,5 +487,26 @@
@apply ml-4;
}
}
}
}

:deep(.otp-input-container) {
display: flex;
gap: 0.75rem;
}

.hover-link {
text-decoration: none !important;
display: inline-block;
width: fit-content;
margin: 0 auto;

&:hover {
text-decoration: underline !important;
}
}

button.hover-link {
font-family: inherit;
font-size: inherit;
}
</style>
Loading