@adminforth/two-factors-auth 1.10.6 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +3 -2
- package/custom/TwoFAModal.vue +113 -19
- package/custom/TwoFactorsConfirmation.vue +13 -62
- package/custom/TwoFactorsPasskeysSettings.vue +102 -25
- package/custom/TwoFactorsSetup.vue +1 -1
- package/custom/utils.js +106 -0
- package/dist/custom/TwoFAModal.vue +113 -19
- package/dist/custom/TwoFactorsConfirmation.vue +13 -62
- package/dist/custom/TwoFactorsPasskeysSettings.vue +102 -25
- package/dist/custom/TwoFactorsSetup.vue +1 -1
- package/dist/custom/utils.js +106 -0
- package/dist/index.js +52 -25
- package/index.ts +50 -20
- package/package.json +1 -1
package/custom/utils.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { callAdminForthApi } from '@/utils';
|
|
2
|
+
|
|
3
|
+
export function handlePasskeyAlert(propSuggestionPeriod, router) {
|
|
4
|
+
const currentDate = Date.now();
|
|
5
|
+
window.localStorage.removeItem('suggestionPeriod');
|
|
6
|
+
window.localStorage.setItem('suggestionPeriod', propSuggestionPeriod);
|
|
7
|
+
let suggestionPeriod = window.localStorage.getItem('suggestionPeriod');
|
|
8
|
+
let lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
9
|
+
let suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
10
|
+
if ( !lastSuggestionDate ) {
|
|
11
|
+
window.localStorage.setItem('lastSuggestionDate', currentDate.toString());
|
|
12
|
+
lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
13
|
+
}
|
|
14
|
+
if ( !suggestPasskey ) {
|
|
15
|
+
window.localStorage.setItem('suggestPasskey', 'true');
|
|
16
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
17
|
+
}
|
|
18
|
+
if ( currentDate - parseInt(lastSuggestionDate) > parseInt(suggestionPeriod) ) {
|
|
19
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
20
|
+
if (suggestPasskey !== 'true'){
|
|
21
|
+
if ( suggestPasskey === 'false' || !suggestPasskey ) {
|
|
22
|
+
window.localStorage.setItem('suggestPasskey', 'true');
|
|
23
|
+
} else if ( suggestPasskey !== 'never' ) {
|
|
24
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
29
|
+
|
|
30
|
+
if ( suggestPasskey === 'true' ) {
|
|
31
|
+
adminforth.alert({
|
|
32
|
+
message: 'Do you want to add passkey?',
|
|
33
|
+
variant: 'info',
|
|
34
|
+
buttons: [
|
|
35
|
+
{ value: 'yes', label: 'Add passkey' },
|
|
36
|
+
{ value: 'later', label: 'Later' },
|
|
37
|
+
{ value: 'never', label: 'Never' },
|
|
38
|
+
],
|
|
39
|
+
timeout: 'unlimited'
|
|
40
|
+
}).then((value) => {
|
|
41
|
+
switch (value) {
|
|
42
|
+
case 'yes':
|
|
43
|
+
router.push({ name: 'settings', params: { page: 'passkeys' } });
|
|
44
|
+
break;
|
|
45
|
+
case 'later':
|
|
46
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
47
|
+
break;
|
|
48
|
+
case 'never':
|
|
49
|
+
window.localStorage.setItem('suggestPasskey', 'never');
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getPasskey() {
|
|
60
|
+
const { _options } = await createSignInRequest();
|
|
61
|
+
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
|
|
62
|
+
const credential = await authenticate(options);
|
|
63
|
+
if (!credential) {
|
|
64
|
+
isFetchingPasskey.value = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = JSON.stringify(credential);
|
|
68
|
+
const passkeyOptions = {
|
|
69
|
+
response: result,
|
|
70
|
+
origin: window.location.origin,
|
|
71
|
+
};
|
|
72
|
+
return passkeyOptions;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function createSignInRequest() {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await callAdminForthApi({
|
|
79
|
+
path: `/plugin/passkeys/signInRequest`,
|
|
80
|
+
method: 'POST',
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error creating sign-in request:', error);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (response.ok === true) {
|
|
87
|
+
return { _options: response.data, challengeId: response.challengeId };
|
|
88
|
+
} else {
|
|
89
|
+
adminforth.alert({message: 'Error creating sign-in request.', variant: 'warning'});
|
|
90
|
+
codeError.value = 'Error creating sign-in request.';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function authenticate(options) {
|
|
95
|
+
try {
|
|
96
|
+
const abortController = new AbortController();
|
|
97
|
+
const credential = await navigator.credentials.get({
|
|
98
|
+
publicKey: options,
|
|
99
|
+
signal: abortController.signal,
|
|
100
|
+
});
|
|
101
|
+
return credential;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
adminforth.alert({message: 'Error during authentication', variant: 'warning'});
|
|
104
|
+
codeError.value = 'Error during authentication.';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<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">
|
|
5
5
|
<div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
|
|
6
|
-
{{
|
|
6
|
+
<p> {{ customDialogTitle }} </p>
|
|
7
|
+
<p>{{ $t('Please enter your authenticator code') }}</p>
|
|
7
8
|
</div>
|
|
8
9
|
|
|
9
10
|
<div class="my-4 w-full flex justify-center" ref="otpRoot">
|
|
10
11
|
<v-otp-input
|
|
11
|
-
ref="
|
|
12
|
+
ref="confirmationResult"
|
|
12
13
|
container-class="grid grid-cols-6 gap-3 w-full"
|
|
13
14
|
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"
|
|
14
15
|
:num-inputs="6"
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
/>
|
|
22
23
|
</div>
|
|
23
24
|
|
|
24
|
-
<div class="mt-6 flex justify-center gap-
|
|
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>
|
|
25
27
|
<button
|
|
26
28
|
class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
|
|
27
29
|
@click="onCancel"
|
|
@@ -29,18 +31,60 @@
|
|
|
29
31
|
>{{ $t('Cancel') }}</button>
|
|
30
32
|
</div>
|
|
31
33
|
</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>
|
|
32
67
|
</div>
|
|
33
68
|
</template>
|
|
34
69
|
|
|
35
70
|
<script setup lang="ts">
|
|
36
71
|
import VOtpInput from 'vue3-otp-input';
|
|
37
|
-
import { ref, nextTick, watch } from 'vue';
|
|
72
|
+
import { ref, nextTick, watch, onMounted } from 'vue';
|
|
38
73
|
import { useUserStore } from '@/stores/user';
|
|
39
74
|
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
|
+
|
|
40
81
|
declare global {
|
|
41
82
|
interface Window {
|
|
42
83
|
adminforthTwoFaModal: {
|
|
43
|
-
|
|
84
|
+
get2FaConfirmationResult: (
|
|
85
|
+
verifyingCallback?: (confirmationResult: string) => Promise<boolean>,
|
|
86
|
+
title?: string
|
|
87
|
+
) => Promise<any>;
|
|
44
88
|
};
|
|
45
89
|
}
|
|
46
90
|
}
|
|
@@ -54,15 +98,20 @@
|
|
|
54
98
|
}>();
|
|
55
99
|
|
|
56
100
|
const modelShow = ref(false);
|
|
57
|
-
let resolveFn: ((
|
|
58
|
-
let verifyingCallback: ((
|
|
59
|
-
let verifyFn: 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;
|
|
60
104
|
let rejectFn: ((err?: any) => void) | null = null;
|
|
61
105
|
|
|
106
|
+
|
|
62
107
|
window.adminforthTwoFaModal = {
|
|
63
|
-
|
|
64
|
-
new Promise((resolve, reject) => {
|
|
108
|
+
get2FaConfirmationResult: (verifyingCallback?: (confirmationResult: string) => Promise<boolean>, title?: string) =>
|
|
109
|
+
new Promise(async (resolve, reject) => {
|
|
65
110
|
if (modelShow.value) throw new Error('Modal is already open');
|
|
111
|
+
await checkIfUserHasPasskeys();
|
|
112
|
+
if (title) {
|
|
113
|
+
customDialogTitle.value = title;
|
|
114
|
+
}
|
|
66
115
|
modelShow.value = true;
|
|
67
116
|
resolveFn = resolve;
|
|
68
117
|
rejectFn = reject;
|
|
@@ -73,10 +122,30 @@
|
|
|
73
122
|
const { t } = useI18n();
|
|
74
123
|
const user = useUserStore();
|
|
75
124
|
|
|
76
|
-
const
|
|
125
|
+
const confirmationResult = ref<any>(null);
|
|
77
126
|
const otpRoot = ref<HTMLElement | null>(null);
|
|
78
127
|
const bindValue = ref('');
|
|
128
|
+
const doesUserHavePasskeys = ref(false);
|
|
129
|
+
const modalMode = ref<"totp" | "passkey">("totp");
|
|
130
|
+
const isLoading = ref(false);
|
|
131
|
+
const customDialogTitle = ref("");
|
|
79
132
|
|
|
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
|
+
|
|
80
149
|
function tagOtpInputs() {
|
|
81
150
|
const root = otpRoot.value;
|
|
82
151
|
if (!root) return;
|
|
@@ -96,15 +165,15 @@
|
|
|
96
165
|
event.preventDefault();
|
|
97
166
|
const pastedText = event.clipboardData?.getData('text') || '';
|
|
98
167
|
if (pastedText.length === 6) {
|
|
99
|
-
|
|
168
|
+
confirmationResult.value?.fillInput(pastedText);
|
|
100
169
|
}
|
|
101
170
|
}
|
|
102
171
|
|
|
103
172
|
async function handleOnComplete(value: string) {
|
|
104
|
-
await
|
|
173
|
+
await sendConfirmationResult(value);
|
|
105
174
|
}
|
|
106
175
|
|
|
107
|
-
async function
|
|
176
|
+
async function sendConfirmationResult(value: string) {
|
|
108
177
|
if (!resolveFn) throw new Error('Modal is not initialized properly');
|
|
109
178
|
if (verifyFn) {
|
|
110
179
|
try {
|
|
@@ -120,14 +189,19 @@
|
|
|
120
189
|
}
|
|
121
190
|
|
|
122
191
|
modelShow.value = false;
|
|
123
|
-
|
|
192
|
+
const dataToReturn = {
|
|
193
|
+
mode: "totp",
|
|
194
|
+
result: value
|
|
195
|
+
}
|
|
196
|
+
resolveFn(dataToReturn);
|
|
124
197
|
}
|
|
125
198
|
|
|
126
199
|
|
|
127
200
|
function onCancel() {
|
|
128
201
|
modelShow.value = false;
|
|
129
202
|
bindValue.value = '';
|
|
130
|
-
|
|
203
|
+
confirmationResult.value?.clearInput();
|
|
204
|
+
rejectFn("Cancel");
|
|
131
205
|
emit('rejected', new Error('cancelled'));
|
|
132
206
|
emit('closed');
|
|
133
207
|
}
|
|
@@ -148,10 +222,30 @@
|
|
|
148
222
|
htmlRef.style.overflow = '';
|
|
149
223
|
}
|
|
150
224
|
bindValue.value = '';
|
|
151
|
-
|
|
225
|
+
confirmationResult.value?.clearInput();
|
|
152
226
|
}
|
|
153
227
|
});
|
|
154
228
|
|
|
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
|
+
|
|
155
249
|
</script>
|
|
156
250
|
|
|
157
251
|
<style scoped>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
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">
|
|
12
12
|
<div class="relative p-4 w-full max-w-md max-h-full">
|
|
13
13
|
<!-- Modal content -->
|
|
14
14
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
|
|
45
45
|
<p class="text-4xl font-semibold mb-4">Passkey</p>
|
|
46
46
|
<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">
|
|
48
48
|
Use passkey
|
|
49
49
|
</Button>
|
|
50
50
|
<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 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
71
|
+
<div v-else>
|
|
72
|
+
<Spinner class="w-10 h-10" />
|
|
73
|
+
</div>
|
|
71
74
|
</div>
|
|
72
75
|
</template>
|
|
73
76
|
|
|
@@ -79,13 +82,14 @@
|
|
|
79
82
|
import { useUserStore } from '@/stores/user';
|
|
80
83
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
81
84
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
82
|
-
import { Button, Link } from '@/afcl';
|
|
85
|
+
import { Button, Link, Spinner } from '@/afcl';
|
|
83
86
|
import VOtpInput from "vue3-otp-input";
|
|
84
87
|
import { useI18n } from 'vue-i18n';
|
|
85
88
|
import { useRoute } from 'vue-router'
|
|
86
89
|
import { useRouter } from 'vue-router';
|
|
87
90
|
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
|
|
88
91
|
import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
92
|
+
import { handlePasskeyAlert } from './utils.js';
|
|
89
93
|
|
|
90
94
|
const { t } = useI18n();
|
|
91
95
|
const code = ref(null);
|
|
@@ -94,6 +98,7 @@
|
|
|
94
98
|
const route = useRoute();
|
|
95
99
|
const router = useRouter();
|
|
96
100
|
const codeError = ref(null);
|
|
101
|
+
const isFetchingPasskey = ref(false);
|
|
97
102
|
|
|
98
103
|
onBeforeMount(() => {
|
|
99
104
|
if (localStorage.getItem('isAuthorized') === 'true') {
|
|
@@ -130,6 +135,7 @@
|
|
|
130
135
|
const doesUserHavePasskeys = ref(false);
|
|
131
136
|
const confirmationMode = ref("code");
|
|
132
137
|
const isPasskeysSupported = ref(false);
|
|
138
|
+
const isLoading = ref(true);
|
|
133
139
|
|
|
134
140
|
onMounted(async () => {
|
|
135
141
|
if (localStorage.getItem('isAuthorized') !== 'true') {
|
|
@@ -144,6 +150,7 @@
|
|
|
144
150
|
const rootEl = otpRoot.value;
|
|
145
151
|
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
146
152
|
}
|
|
153
|
+
isLoading.value = false;
|
|
147
154
|
});
|
|
148
155
|
|
|
149
156
|
watch(route, (newRoute) => {
|
|
@@ -230,10 +237,12 @@
|
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
async function usePasskeyButton() {
|
|
240
|
+
isFetchingPasskey.value = true;
|
|
233
241
|
const { _options, challengeId } = await createSignInRequest();
|
|
234
242
|
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
|
|
235
243
|
const credential = await authenticate(options);
|
|
236
244
|
if (!credential) {
|
|
245
|
+
isFetchingPasskey.value = false;
|
|
237
246
|
return;
|
|
238
247
|
}
|
|
239
248
|
const result = JSON.stringify(credential);
|
|
@@ -243,6 +252,7 @@
|
|
|
243
252
|
origin: window.location.origin,
|
|
244
253
|
};
|
|
245
254
|
sendCode('', 'passkey', passkeyOptions);
|
|
255
|
+
isFetchingPasskey.value = false;
|
|
246
256
|
}
|
|
247
257
|
|
|
248
258
|
async function createSignInRequest() {
|
|
@@ -278,65 +288,6 @@
|
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
|
|
281
|
-
</script>
|
|
282
|
-
|
|
283
|
-
<script lang="ts">
|
|
284
|
-
|
|
285
|
-
export function handlePasskeyAlert(propSuggestionPeriod, router) {
|
|
286
|
-
const currentDate = Date.now();
|
|
287
|
-
window.localStorage.removeItem('suggestionPeriod');
|
|
288
|
-
window.localStorage.setItem('suggestionPeriod', propSuggestionPeriod);
|
|
289
|
-
let suggestionPeriod = window.localStorage.getItem('suggestionPeriod');
|
|
290
|
-
let lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
291
|
-
let suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
292
|
-
if ( !lastSuggestionDate ) {
|
|
293
|
-
window.localStorage.setItem('lastSuggestionDate', currentDate.toString());
|
|
294
|
-
lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
295
|
-
}
|
|
296
|
-
if ( !suggestPasskey ) {
|
|
297
|
-
window.localStorage.setItem('suggestPasskey', 'true');
|
|
298
|
-
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
299
|
-
}
|
|
300
|
-
if ( currentDate - parseInt(lastSuggestionDate) > parseInt(suggestionPeriod) ) {
|
|
301
|
-
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
302
|
-
if (suggestPasskey !== 'true'){
|
|
303
|
-
if ( suggestPasskey === 'false' || !suggestPasskey ) {
|
|
304
|
-
window.localStorage.setItem('suggestPasskey', 'true');
|
|
305
|
-
} else if ( suggestPasskey !== 'never' ) {
|
|
306
|
-
window.localStorage.setItem('suggestPasskey', 'false');
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
311
|
-
|
|
312
|
-
if ( suggestPasskey === 'true' ) {
|
|
313
|
-
adminforth.alert({
|
|
314
|
-
message: 'Do you want to add passkey?',
|
|
315
|
-
variant: 'info',
|
|
316
|
-
buttons: [
|
|
317
|
-
{ value: 'yes', label: 'Add passkey' },
|
|
318
|
-
{ value: 'later', label: 'Later' },
|
|
319
|
-
{ value: 'never', label: 'Never' },
|
|
320
|
-
],
|
|
321
|
-
timeout: 'unlimited'
|
|
322
|
-
}).then((value) => {
|
|
323
|
-
switch (value) {
|
|
324
|
-
case 'yes':
|
|
325
|
-
router.push({ name: 'settings', params: { page: 'passkeys' } });
|
|
326
|
-
break;
|
|
327
|
-
case 'later':
|
|
328
|
-
window.localStorage.setItem('suggestPasskey', 'false');
|
|
329
|
-
break;
|
|
330
|
-
case 'never':
|
|
331
|
-
window.localStorage.setItem('suggestPasskey', 'never');
|
|
332
|
-
break;
|
|
333
|
-
default:
|
|
334
|
-
window.localStorage.setItem('suggestPasskey', 'false');
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
291
|
|
|
341
292
|
function getOtpInputs() {
|
|
342
293
|
const root = otpRoot.value;
|