Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
147 changes: 37 additions & 110 deletions custom/TwoFAModal.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<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"
v-show ="modelShow && (isLoading === false)">
<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">
v-show ="twofaApi.isOpened && (isLoading === false)">
<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">
<div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
<p> {{ customDialogTitle }} </p>
<p> {{ twofaApi.customDialogTitle }} </p>
<p>{{ $t('Please enter your authenticator code') }}</p>
</div>

Expand All @@ -23,20 +23,19 @@
/>
</div>

<div class="flex items-center w-full" :class="doesUserHavePasskeys ? 'justify-between' : 'justify-center' ">
<p v-if="doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'passkey'" >{{$t('use passkey')}}</p>
<div class="flex items-center w-full" :class="twofaApi.doesUserHavePasskeys ? 'justify-between' : 'justify-center' ">
<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>
<Button
class="px-4 py-2 rounded border"
@click="onCancel"
:disabled="inProgress"
>{{ $t('Cancel') }}</Button>
</div>
</div>
</div>



<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">
<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">
<button
type="button"
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"
Expand All @@ -50,16 +49,16 @@
<IconShieldOutline class="af-2fa-shield-icon w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
<p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p>
<div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200">
<p class="mb-2">{{customDialogTitle}} </p>
<p class="mb-2">{{twofaApi.customDialogTitle}} </p>
<p>{{$t('Authenticate yourself using the button below')}}</p>
</div>
<Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
{{$t('Use passkey')}}
</Button>
<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">
<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">
<div class="mb-3 font-normal text-gray-700 dark:text-gray-400">
<p>{{$t('Have issues with passkey?')}}</p>
<p class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'totp'" >{{$t('use TOTP')}}</p>
<p class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="twofaApi.setModalMode('totp')" >{{$t('use TOTP')}}</p>
</div>
</div>

Expand All @@ -73,14 +72,13 @@

import VOtpInput from 'vue3-otp-input';
import { ref, nextTick, watch, onMounted } from 'vue';
import { useUserStore } from '@/stores/user';
import { useI18n } from 'vue-i18n';
import { callAdminForthApi } from '@/utils';
import { Link, Button } from '@/afcl';
import { Button } from '@/afcl';
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
import { getPasskey } from './utils.js'
import adminforth from '@/adminforth';
import { use2faApi } from './use2faApi';

const twofaApi = use2faApi();
const isFetchingPasskey = ref(false);

declare global {
interface Window {
Expand All @@ -101,6 +99,10 @@
(e: 'closed'): void
}>();

onMounted(() => {
twofaApi.registerAddEventListenerForOTPInput(addEventListenerForOTPInput);
});

async function addEventListenerForOTPInput(){
document.addEventListener('focusin', handleGlobalFocusIn, true);
focusFirstAvailableOtpInput();
Expand All @@ -118,64 +120,35 @@
// Abort any in-flight WebAuthn request when leaving the component
}


const modelShow = ref(false);
let resolveFn: ((confirmationResult: any) => void) | null = null;
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
let rejectFn: ((err?: any) => void) | null = null;


window.adminforthTwoFaModal = {
get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean>) =>
new Promise(async (resolve, reject) => {
if (modelShow.value) throw new Error('Modal is already open');
const skipAllowModal = await checkIfSkipAllowModal();
if (skipAllowModal) {
resolve({ code: "123456" }); // dummy code
return;
}
await checkIfUserHasPasskeys();
if (title) {
customDialogTitle.value = title;
}
modelShow.value = true;
if (modalMode.value === 'totp') {
await addEventListenerForOTPInput();
}
resolveFn = resolve;
rejectFn = reject;
verifyFn = verifyingCallback ?? null;
}),
get2FaConfirmationResult: twofaApi.get2FaConfirmationResult
};

const { t } = useI18n();
const user = useUserStore();

const confirmationResult = ref<any>(null);
const otpRoot = ref<HTMLElement | null>(null);
const bindValue = ref('');
const doesUserHavePasskeys = ref(false);
const modalMode = ref<"totp" | "passkey">("totp");
const isLoading = ref(false);
const customDialogTitle = ref("");

async function usePasskeyButtonClick() {
let passkeyData;
isFetchingPasskey.value = true;
try {
passkeyData = await getPasskey();
} catch (error) {
onCancel();
return null;
} finally {
isFetchingPasskey.value = false;
}
modelShow.value = false;
twofaApi.setIsOpened(false);
const dataToReturn = {
mode: "passkey",
result: passkeyData
}
customDialogTitle.value = "";
twofaApi.setCustomDialogTitle("");
removeEventListenerForOTPInput();
resolveFn(dataToReturn);
twofaApi.resolveFn?.(dataToReturn);
}

function tagOtpInputs() {
Expand Down Expand Up @@ -206,50 +179,50 @@
}

async function sendConfirmationResult(value: string) {
if (!resolveFn) throw new Error('Modal is not initialized properly');
if (verifyFn) {
if (!twofaApi.resolveFn) throw new Error('Modal is not initialized properly');
if (twofaApi.verifyFn) {
try {
const ok = await verifyFn(value);
const ok = await twofaApi.verifyFn(value);
if (!ok) {
rejectFn?.(new Error('Invalid code'));
twofaApi.rejectFn?.(new Error('Invalid code'));
return;
}
} catch (err) {
rejectFn?.(err);
twofaApi.rejectFn?.(err);
return;
}
}

modelShow.value = false;
twofaApi.setIsOpened(false);
const dataToReturn = {
mode: "totp",
result: value
}
customDialogTitle.value = "";
twofaApi.setCustomDialogTitle("");
removeEventListenerForOTPInput();
resolveFn(dataToReturn);
twofaApi.resolveFn?.(dataToReturn);
}


function onCancel() {
modelShow.value = false;
twofaApi.setIsOpened(false);
bindValue.value = '';
confirmationResult.value?.clearInput();
removeEventListenerForOTPInput();
rejectFn("Cancel");
twofaApi.rejectFn?.(new Error('Cancel'));
emit('rejected', new Error('cancelled'));
emit('closed');
}

watch(modalMode, async (newMode) => {
watch(() => twofaApi.modalMode, async (newMode) => {
if (newMode === 'totp') {
await addEventListenerForOTPInput();
} else {
removeEventListenerForOTPInput();
}
});

watch(modelShow, async (open) => {
watch(() => twofaApi.isOpened, async (open) => {
if (open) {
await nextTick();
const htmlRef = document.querySelector('html');
Expand All @@ -258,7 +231,7 @@
}

// Wait for conditional rendering to complete
if (modalMode.value === 'totp' && !isLoading.value) {
if (twofaApi.modalMode === 'totp' && !isLoading.value) {
await nextTick();
setTimeout(() => {
tagOtpInputs();
Expand All @@ -278,33 +251,6 @@
}
});

async function checkIfUserHasPasskeys() {
isLoading.value = true;
try {
const response = await callAdminForthApi({
method: 'GET',
path: '/plugin/passkeys/getPasskeys',
});

if (response.ok) {
if (response.data.length >= 1) {
doesUserHavePasskeys.value = true;
modalMode.value = "passkey";
} else {
doesUserHavePasskeys.value = false;
modalMode.value = "totp";
}
}
} catch (error) {
console.error('Error checking passkeys:', error);
// Fallback to TOTP if there's an error
doesUserHavePasskeys.value = false;
modalMode.value = "totp";
} finally {
isLoading.value = false;
}
}

function getOtpInputs() {
const root = otpRoot.value;
if (!root) return [];
Expand Down Expand Up @@ -344,25 +290,6 @@
}
});
}


async function checkIfSkipAllowModal(){
try {
const response = await callAdminForthApi({
method: "GET",
path: "/plugin/twofa/skip-allow-modal",
});
if ( response.skipAllowed === true ) {
return true;
} else {
return false;
}
} catch (error) {
console.error('Error checking skip allow modal:', error);
return false;
}
}

</script>

<style scoped>
Expand Down
Loading