Skip to content

Commit abef7b9

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth-two-factors-auth
2 parents 6840c22 + 8245694 commit abef7b9

File tree

5 files changed

+330
-69
lines changed

5 files changed

+330
-69
lines changed

custom/TwoFAModal.vue

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<template>
22
<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">
4-
<div class="relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
3+
v-show ="modelShow && (isLoading === false)">
4+
<div v-if="modalMode === 'totp'" class="af-two-factor-modal-totp relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
55
<div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
6-
{{ $t('Please enter your authenticator code') }}
6+
<p> {{ customDialogTitle }} </p>
7+
<p>{{ $t('Please enter your authenticator code') }}</p>
78
</div>
89

910
<div class="my-4 w-full flex justify-center" ref="otpRoot">
1011
<v-otp-input
11-
ref="code"
12+
ref="confirmationResult"
1213
container-class="grid grid-cols-6 gap-3 w-full"
1314
input-classes="bg-gray-50 text-center flex 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"
1415
:num-inputs="6"
@@ -21,26 +22,69 @@
2122
/>
2223
</div>
2324

24-
<div class="mt-6 flex justify-center gap-3">
25+
<div class="mt-6 flex justify-center items-center gap-32 w-full">
26+
<p v-if="doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'passkey'" >use passkey</p>
2527
<button
2628
class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
2729
@click="onCancel"
2830
:disabled="inProgress"
2931
>{{ $t('Cancel') }}</button>
3032
</div>
3133
</div>
34+
35+
36+
37+
<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">
38+
<button
39+
type="button"
40+
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"
41+
@click="onCancel"
42+
>
43+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
44+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
45+
</svg>
46+
<span class="sr-only">Close modal</span>
47+
</button>
48+
<IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
49+
<p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">Passkey</p>
50+
<div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200">
51+
<p class="mb-2">{{customDialogTitle}} </p>
52+
<p>Authenticate yourself using the button below</p>
53+
</div>
54+
<Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
55+
Use passkey
56+
</Button>
57+
<div v-if="modalMode === '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">
58+
<div class="mb-3 font-normal text-gray-700 dark:text-gray-400">
59+
<p> Have issues with passkey? </p>
60+
<p class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'totp'" >use TOTP</p>
61+
</div>
62+
</div>
63+
64+
65+
66+
</div>
3267
</div>
3368
</template>
3469

3570
<script setup lang="ts">
3671
import VOtpInput from 'vue3-otp-input';
37-
import { ref, nextTick, watch } from 'vue';
72+
import { ref, nextTick, watch, onMounted } from 'vue';
3873
import { useUserStore } from '@/stores/user';
3974
import { useI18n } from 'vue-i18n';
75+
import { callAdminForthApi } from '@/utils';
76+
import { Link, Button } from '@/afcl';
77+
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
78+
import { getPasskey } from './utils.js'
79+
80+
4081
declare global {
4182
interface Window {
4283
adminforthTwoFaModal: {
43-
getCode: () => Promise<any>;
84+
get2FaConfirmationResult: (
85+
verifyingCallback?: (confirmationResult: string) => Promise<boolean>,
86+
title?: string
87+
) => Promise<any>;
4488
};
4589
}
4690
}
@@ -54,15 +98,20 @@
5498
}>();
5599
56100
const modelShow = ref(false);
57-
let resolveFn: ((code: string) => void) | null = null;
58-
let verifyingCallback: ((code: string) => boolean) | null = null;
59-
let verifyFn: null | ((code: string) => Promise<boolean> | boolean) = null;
101+
let resolveFn: ((confirmationResult: string) => void) | null = null;
102+
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
103+
let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
60104
let rejectFn: ((err?: any) => void) | null = null;
61105
106+
62107
window.adminforthTwoFaModal = {
63-
getCode: (verifyingCallback?: (code: string) => Promise<boolean>) =>
64-
new Promise((resolve, reject) => {
108+
get2FaConfirmationResult: (verifyingCallback?: (confirmationResult: string) => Promise<boolean>, title?: string) =>
109+
new Promise(async (resolve, reject) => {
65110
if (modelShow.value) throw new Error('Modal is already open');
111+
await checkIfUserHasPasskeys();
112+
if (title) {
113+
customDialogTitle.value = title;
114+
}
66115
modelShow.value = true;
67116
resolveFn = resolve;
68117
rejectFn = reject;
@@ -73,10 +122,30 @@
73122
const { t } = useI18n();
74123
const user = useUserStore();
75124
76-
const code = ref<any>(null);
125+
const confirmationResult = ref<any>(null);
77126
const otpRoot = ref<HTMLElement | null>(null);
78127
const bindValue = ref('');
128+
const doesUserHavePasskeys = ref(false);
129+
const modalMode = ref<"totp" | "passkey">("totp");
130+
const isLoading = ref(false);
131+
const customDialogTitle = ref("");
79132
133+
async function usePasskeyButtonClick() {
134+
let passkeyData;
135+
try {
136+
passkeyData = await getPasskey();
137+
} catch (e) {
138+
adminforth.alert({message: 'Failed to get passkey', variant: 'danger'});
139+
onCancel();
140+
}
141+
modelShow.value = false;
142+
const dataToReturn = {
143+
mode: "passkey",
144+
result: passkeyData
145+
}
146+
resolveFn(dataToReturn);
147+
}
148+
80149
function tagOtpInputs() {
81150
const root = otpRoot.value;
82151
if (!root) return;
@@ -96,15 +165,15 @@
96165
event.preventDefault();
97166
const pastedText = event.clipboardData?.getData('text') || '';
98167
if (pastedText.length === 6) {
99-
code.value?.fillInput(pastedText);
168+
confirmationResult.value?.fillInput(pastedText);
100169
}
101170
}
102171
103172
async function handleOnComplete(value: string) {
104-
await sendCode(value);
173+
await sendConfirmationResult(value);
105174
}
106175
107-
async function sendCode(value: string) {
176+
async function sendConfirmationResult(value: string) {
108177
if (!resolveFn) throw new Error('Modal is not initialized properly');
109178
if (verifyFn) {
110179
try {
@@ -120,14 +189,19 @@
120189
}
121190
122191
modelShow.value = false;
123-
resolveFn(value);
192+
const dataToReturn = {
193+
mode: "totp",
194+
result: value
195+
}
196+
resolveFn(dataToReturn);
124197
}
125198
126199
127200
function onCancel() {
128201
modelShow.value = false;
129202
bindValue.value = '';
130-
code.value?.clearInput();
203+
confirmationResult.value?.clearInput();
204+
rejectFn("Cancel");
131205
emit('rejected', new Error('cancelled'));
132206
emit('closed');
133207
}
@@ -148,10 +222,30 @@
148222
htmlRef.style.overflow = '';
149223
}
150224
bindValue.value = '';
151-
code.value?.clearInput();
225+
confirmationResult.value?.clearInput();
152226
}
153227
});
154228
229+
async function checkIfUserHasPasskeys() {
230+
isLoading.value = true;
231+
callAdminForthApi({
232+
method: 'GET',
233+
path: '/plugin/passkeys/getPasskeys',
234+
}).then((response) => {
235+
if (response.ok) {
236+
if (response.data.length >= 1) {
237+
doesUserHavePasskeys.value = true;
238+
modalMode.value = "passkey";
239+
isLoading.value = false;
240+
} else {
241+
doesUserHavePasskeys.value = false;
242+
modalMode.value = "totp";
243+
isLoading.value = false;
244+
}
245+
}
246+
});
247+
}
248+
155249
</script>
156250

157251
<style scoped>

custom/TwoFactorsConfirmation.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
}: {}"
99
>
1010

11-
<div 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">
11+
<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">
1212
<div class="relative p-4 w-full max-w-md max-h-full">
1313
<!-- Modal content -->
1414
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
@@ -44,7 +44,7 @@
4444
<IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
4545
<p class="text-4xl font-semibold mb-4">Passkey</p>
4646
<p class="mb-2 max-w-[300px]">When you are ready, authenticate using the button below</p>
47-
<Button @click="usePasskeyButton" class="w-full mx-16">
47+
<Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
4848
Use passkey
4949
</Button>
5050
<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">
@@ -68,6 +68,9 @@
6868
</div>
6969
</div>
7070
</div>
71+
<div v-else>
72+
<Spinner class="w-10 h-10" />
73+
</div>
7174
</div>
7275
</template>
7376

@@ -79,7 +82,7 @@
7982
import { useUserStore } from '@/stores/user';
8083
import { callAdminForthApi, loadFile } from '@/utils';
8184
import { showErrorTost } from '@/composables/useFrontendApi';
82-
import { Button, Link } from '@/afcl';
85+
import { Button, Link, Spinner } from '@/afcl';
8386
import VOtpInput from "vue3-otp-input";
8487
import { useI18n } from 'vue-i18n';
8588
import { useRoute } from 'vue-router'
@@ -95,6 +98,7 @@
9598
const route = useRoute();
9699
const router = useRouter();
97100
const codeError = ref(null);
101+
const isFetchingPasskey = ref(false);
98102
99103
onBeforeMount(() => {
100104
if (localStorage.getItem('isAuthorized') === 'true') {
@@ -131,6 +135,7 @@
131135
const doesUserHavePasskeys = ref(false);
132136
const confirmationMode = ref("code");
133137
const isPasskeysSupported = ref(false);
138+
const isLoading = ref(true);
134139
135140
onMounted(async () => {
136141
if (localStorage.getItem('isAuthorized') !== 'true') {
@@ -145,6 +150,7 @@
145150
const rootEl = otpRoot.value;
146151
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
147152
}
153+
isLoading.value = false;
148154
});
149155
150156
watch(route, (newRoute) => {
@@ -231,10 +237,12 @@
231237
}
232238
233239
async function usePasskeyButton() {
240+
isFetchingPasskey.value = true;
234241
const { _options, challengeId } = await createSignInRequest();
235242
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
236243
const credential = await authenticate(options);
237244
if (!credential) {
245+
isFetchingPasskey.value = false;
238246
return;
239247
}
240248
const result = JSON.stringify(credential);
@@ -244,6 +252,7 @@
244252
origin: window.location.origin,
245253
};
246254
sendCode('', 'passkey', passkeyOptions);
255+
isFetchingPasskey.value = false;
247256
}
248257
249258
async function createSignInRequest() {

0 commit comments

Comments
 (0)