Skip to content

Commit ae841a7

Browse files
committed
feat: add global frontend api
1 parent 14fb5df commit ae841a7

2 files changed

Lines changed: 180 additions & 58 deletions

File tree

custom/TwoFAModal.vue

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
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 && (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">
55
<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>
77
<p>{{ $t('Please enter your authenticator code') }}</p>
88
</div>
99

@@ -23,8 +23,8 @@
2323
/>
2424
</div>
2525

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>
2828
<Button
2929
class="px-4 py-2 rounded border"
3030
@click="onCancel"
@@ -36,7 +36,7 @@
3636

3737

3838

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">
4040
<button
4141
type="button"
4242
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,16 +50,16 @@
5050
<IconShieldOutline class="af-2fa-shield-icon w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
5151
<p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p>
5252
<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>
5454
<p>{{$t('Authenticate yourself using the button below')}}</p>
5555
</div>
5656
<Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
5757
{{$t('Use passkey')}}
5858
</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">
6060
<div class="mb-3 font-normal text-gray-700 dark:text-gray-400">
6161
<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>
6363
</div>
6464
</div>
6565

@@ -80,7 +80,9 @@
8080
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
8181
import { getPasskey } from './utils.js'
8282
import adminforth from '@/adminforth';
83+
import { use2faApi } from './use2faApi';
8384
85+
const twofaApi = use2faApi();
8486
8587
declare global {
8688
interface Window {
@@ -101,6 +103,10 @@
101103
(e: 'closed'): void
102104
}>();
103105
106+
onMounted(() => {
107+
twofaApi.registerAddEventListenerForOTPInput(addEventListenerForOTPInput);
108+
});
109+
104110
async function addEventListenerForOTPInput(){
105111
document.addEventListener('focusin', handleGlobalFocusIn, true);
106112
focusFirstAvailableOtpInput();
@@ -120,32 +126,32 @@
120126
121127
122128
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;
127133
128134
129135
window.adminforthTwoFaModal = {
130136
get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean>) =>
131137
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');
133139
const skipAllowModal = await checkIfSkipAllowModal();
134140
if (skipAllowModal) {
135141
resolve({ code: "123456" }); // dummy code
136142
return;
137143
}
138144
await checkIfUserHasPasskeys();
139145
if (title) {
140-
customDialogTitle.value = title;
146+
twofaApi.setCustomDialogTitle(title);
141147
}
142-
modelShow.value = true;
143-
if (modalMode.value === 'totp') {
148+
twofaApi.setIsOpened(true);
149+
if (twofaApi.modalMode === 'totp') {
144150
await addEventListenerForOTPInput();
145151
}
146-
resolveFn = resolve;
147-
rejectFn = reject;
148-
verifyFn = verifyingCallback ?? null;
152+
twofaApi.registerResolveFn(resolve);
153+
twofaApi.registerRejectFn(reject);
154+
twofaApi.registerVerifyFn(verifyingCallback ?? null);
149155
}),
150156
};
151157
@@ -168,14 +174,14 @@
168174
onCancel();
169175
return null;
170176
}
171-
modelShow.value = false;
177+
twofaApi.setIsOpened(false);
172178
const dataToReturn = {
173179
mode: "passkey",
174180
result: passkeyData
175181
}
176-
customDialogTitle.value = "";
182+
twofaApi.setCustomDialogTitle("");
177183
removeEventListenerForOTPInput();
178-
resolveFn(dataToReturn);
184+
twofaApi.resolveFn?.(dataToReturn);
179185
}
180186
181187
function tagOtpInputs() {
@@ -206,50 +212,50 @@
206212
}
207213
208214
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) {
211217
try {
212-
const ok = await verifyFn(value);
218+
const ok = await twofaApi.verifyFn(value);
213219
if (!ok) {
214-
rejectFn?.(new Error('Invalid code'));
220+
twofaApi.rejectFn?.(new Error('Invalid code'));
215221
return;
216222
}
217223
} catch (err) {
218-
rejectFn?.(err);
224+
twofaApi.rejectFn?.(err);
219225
return;
220226
}
221227
}
222228
223-
modelShow.value = false;
229+
twofaApi.setIsOpened(false);
224230
const dataToReturn = {
225231
mode: "totp",
226232
result: value
227233
}
228-
customDialogTitle.value = "";
234+
twofaApi.setCustomDialogTitle("");
229235
removeEventListenerForOTPInput();
230-
resolveFn(dataToReturn);
236+
twofaApi.resolveFn?.(dataToReturn);
231237
}
232238
233239
234240
function onCancel() {
235-
modelShow.value = false;
241+
twofaApi.setIsOpened(false);
236242
bindValue.value = '';
237243
confirmationResult.value?.clearInput();
238244
removeEventListenerForOTPInput();
239-
rejectFn("Cancel");
245+
twofaApi.rejectFn?.(new Error('Cancel'));
240246
emit('rejected', new Error('cancelled'));
241247
emit('closed');
242248
}
243249
244-
watch(modalMode, async (newMode) => {
250+
watch(() => twofaApi.modalMode, async (newMode) => {
245251
if (newMode === 'totp') {
246252
await addEventListenerForOTPInput();
247253
} else {
248254
removeEventListenerForOTPInput();
249255
}
250256
});
251257
252-
watch(modelShow, async (open) => {
258+
watch(() => twofaApi.isOpened, async (open) => {
253259
if (open) {
254260
await nextTick();
255261
const htmlRef = document.querySelector('html');
@@ -258,7 +264,7 @@
258264
}
259265
260266
// Wait for conditional rendering to complete
261-
if (modalMode.value === 'totp' && !isLoading.value) {
267+
if (twofaApi.modalMode === 'totp' && !isLoading.value) {
262268
await nextTick();
263269
setTimeout(() => {
264270
tagOtpInputs();
@@ -280,29 +286,15 @@
280286
281287
async function checkIfUserHasPasskeys() {
282288
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");
305296
}
297+
isLoading.value = false;
306298
}
307299
308300
function getOtpInputs() {

custom/use2faApi.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { ref } from 'vue';
2+
import { defineStore } from 'pinia'
3+
import { callAdminForthApi } from '@/utils/utils';
4+
5+
export const use2faApi = defineStore('2fa', () => {
6+
const isOpened = ref(false);
7+
const customDialogTitle = ref('');
8+
const resolveFn = ref<((confirmationResult: any) => void) | null>(null);
9+
const verifyFn = ref<null | ((confirmationResult: string) => Promise<boolean> | boolean)>(null);
10+
const rejectFn = ref<((err?: any) => void) | null>(null);
11+
const addEventListenerForOTPInput = ref<null | (() => Promise<void>)>(null);
12+
const doesUserHavePasskeys = ref(false);
13+
const modalMode = ref<"totp" | "passkey">("totp");
14+
15+
function setDoesUserHavePasskeys(value: boolean) {
16+
doesUserHavePasskeys.value = value;
17+
}
18+
19+
function setModalMode(mode: "totp" | "passkey") {
20+
modalMode.value = mode;
21+
}
22+
23+
function registerVerifyFn(fn: (confirmationResult: string) => Promise<boolean> | boolean) {
24+
verifyFn.value = fn;
25+
}
26+
27+
function registerRejectFn(fn: (err?: any) => void) {
28+
rejectFn.value = fn;
29+
}
30+
31+
function registerResolveFn(fn: (confirmationResult: any) => void) {
32+
resolveFn.value = fn;
33+
}
34+
35+
function registerAddEventListenerForOTPInput(fn: () => Promise<void>) {
36+
addEventListenerForOTPInput.value = fn;
37+
}
38+
39+
async function setCustomDialogTitle(title: string) {
40+
customDialogTitle.value = title;
41+
}
42+
43+
async function checkIfSkipAllowModal(){
44+
try {
45+
const response = await callAdminForthApi({
46+
method: "GET",
47+
path: "/plugin/twofa/skip-allow-modal",
48+
});
49+
if ( response.skipAllowed === true ) {
50+
return true;
51+
} else {
52+
return false;
53+
}
54+
} catch (error) {
55+
console.error('Error checking skip allow modal:', error);
56+
return false;
57+
}
58+
}
59+
60+
async function checkIfUserHasPasskeys() {
61+
try {
62+
const response = await callAdminForthApi({
63+
method: 'GET',
64+
path: '/plugin/passkeys/getPasskeys',
65+
});
66+
67+
if (response.ok) {
68+
if (response.data.length >= 1) {
69+
doesUserHavePasskeys.value = true;
70+
modalMode.value = "passkey";
71+
} else {
72+
doesUserHavePasskeys.value = false;
73+
modalMode.value = "totp";
74+
}
75+
}
76+
} catch (error) {
77+
console.error('Error checking passkeys:', error);
78+
doesUserHavePasskeys.value = false;
79+
modalMode.value = "totp";
80+
return false;
81+
}
82+
}
83+
84+
async function get2FaConfirmationResult(title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean> | boolean) {
85+
return new Promise(async (resolve, reject) => {
86+
if (isOpened.value) throw new Error('Modal is already open');
87+
const skipAllowModal = await checkIfSkipAllowModal();
88+
if (skipAllowModal) {
89+
resolve({ code: "123456" }); // dummy code
90+
return;
91+
}
92+
await checkIfUserHasPasskeys();
93+
if (title) {
94+
customDialogTitle.value = title;
95+
}
96+
isOpened.value = true;
97+
if (!doesUserHavePasskeys.value && addEventListenerForOTPInput.value && typeof addEventListenerForOTPInput.value === 'function') {
98+
await addEventListenerForOTPInput.value();
99+
}
100+
resolveFn.value = resolve;
101+
rejectFn.value = reject;
102+
verifyFn.value = verifyingCallback ?? null;
103+
});
104+
}
105+
106+
function setIsOpened(value: boolean) {
107+
isOpened.value = value;
108+
}
109+
110+
return {
111+
isOpened,
112+
customDialogTitle,
113+
get2FaConfirmationResult,
114+
setIsOpened,
115+
setCustomDialogTitle,
116+
checkIfSkipAllowModal,
117+
registerAddEventListenerForOTPInput,
118+
resolveFn,
119+
verifyFn,
120+
rejectFn,
121+
registerVerifyFn,
122+
registerRejectFn,
123+
registerResolveFn,
124+
checkIfUserHasPasskeys,
125+
setDoesUserHavePasskeys,
126+
setModalMode,
127+
doesUserHavePasskeys,
128+
modalMode,
129+
};
130+
});

0 commit comments

Comments
 (0)