@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/build.log
CHANGED
|
@@ -11,6 +11,7 @@ custom/TwoFactorsSetup.vue
|
|
|
11
11
|
custom/package-lock.json
|
|
12
12
|
custom/package.json
|
|
13
13
|
custom/tsconfig.json
|
|
14
|
+
custom/utils.js
|
|
14
15
|
|
|
15
|
-
sent
|
|
16
|
-
total size is
|
|
16
|
+
sent 68,334 bytes received 172 bytes 137,012.00 bytes/sec
|
|
17
|
+
total size is 67,703 speedup is 0.99
|
package/custom/TwoFAModal.vue
CHANGED
|
@@ -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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-3xl text-
|
|
2
|
+
<div class="text-3xl text-lightBreadcrumbsText dark:text-darkBreadcrumbsText font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
|
|
3
3
|
<p class="flex items-start justify-start leading-none">Passkeys</p>
|
|
4
4
|
<div class="flex flex-col items-end">
|
|
5
5
|
<Table
|
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
class="w-96"
|
|
18
18
|
:buttons="[
|
|
19
19
|
{ label: 'Save', onclick: (dialog) => { renamePasskey(item.id, passkeysNewName); dialog.hide(); } },
|
|
20
|
-
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
|
|
20
|
+
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900 dark:hover:!border-gray-800 dark:focus:!ring-gray-800'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
|
|
21
21
|
]"
|
|
22
22
|
header="Edit Passkey"
|
|
23
23
|
>
|
|
24
24
|
<template #trigger>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
<Tooltip>
|
|
26
|
+
<div
|
|
27
|
+
@click="passkeysNewName = ''"
|
|
28
|
+
class="w-7 h-7 flex items-center justify-center rounded-md text-blue-500 hover:text-blue-700 cursor-pointer"
|
|
29
|
+
>
|
|
30
|
+
<IconPenSolid class="w-5 h-5" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<template #tooltip>
|
|
34
|
+
Rename passkey
|
|
35
|
+
</template>
|
|
36
|
+
</Tooltip>
|
|
31
37
|
</template>
|
|
32
38
|
<div>
|
|
33
39
|
<p>Enter new passkey name:</p>
|
|
@@ -42,17 +48,22 @@
|
|
|
42
48
|
<Dialog
|
|
43
49
|
class="w-96"
|
|
44
50
|
:buttons="[
|
|
45
|
-
{ label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
|
|
46
|
-
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200'}, onclick: (dialog) => dialog.hide() },
|
|
51
|
+
{ label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500 dark:!bg-red-700 dark:hover:!bg-red-600 dark:focus:ring-red-500 dark:!border-red-800'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
|
|
52
|
+
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!text-white dark:!bg-gray-700 dark:hover:!bg-gray-800 dark:!border-gray-600 dark:focus:!ring-gray-800'}, onclick: (dialog) => dialog.hide() },
|
|
47
53
|
]"
|
|
48
54
|
header="Delete Passkey"
|
|
49
55
|
>
|
|
50
56
|
<template #trigger>
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
<Tooltip>
|
|
58
|
+
<div class="w-7 h-7 flex items-center justify-center rounded-md text-red-500 hover:text-red-700 cursor-pointer">
|
|
59
|
+
<IconTrashBinSolid
|
|
60
|
+
class="w-5 h-5"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<template #tooltip>
|
|
64
|
+
Delete passkey
|
|
65
|
+
</template>
|
|
66
|
+
</Tooltip>
|
|
56
67
|
</template>
|
|
57
68
|
<div>
|
|
58
69
|
<p>Are you sure you want to delete this passkey?</p>
|
|
@@ -66,9 +77,9 @@
|
|
|
66
77
|
</template>
|
|
67
78
|
</Table>
|
|
68
79
|
<div class="flex space-x-4 mt-4" v-if="isInitialFinished">
|
|
69
|
-
<ButtonGroup :solidColor="true">
|
|
80
|
+
<ButtonGroup :solidColor="true" v-if="isFetchingPasskey === false">
|
|
70
81
|
<template #button:Profile>
|
|
71
|
-
<div class="flex px-4 py-2" @click="
|
|
82
|
+
<div class="flex px-4 py-2" @click="addPasskeyStartAction()">
|
|
72
83
|
<IconPlusOutline class="w-5 h-5 me-2"/>
|
|
73
84
|
<p>{{ addPasskeyMode === 'platform' ? 'Add Local Passkey' : 'Add External Passkey' }}</p>
|
|
74
85
|
</div>
|
|
@@ -79,6 +90,7 @@
|
|
|
79
90
|
</div>
|
|
80
91
|
</template>
|
|
81
92
|
</ButtonGroup>
|
|
93
|
+
<p v-else class="flex items-center justify-center gap-2 text-base">Processing <Spinner class="w-4 h-4 inline-block" /></p>
|
|
82
94
|
</div>
|
|
83
95
|
<div v-if="isCardsVisible" id="cards-container" class="w-80 mt-2 border-gray-400 p-2 bg-white rounded-lg shadow-md flex flex-col space-y-2">
|
|
84
96
|
<div v-if="isPasskeySupported" class="flex justify-between gap-4" :class="!isPasskeySupported ? 'opacity-50 pointer-events-none' : ''">
|
|
@@ -127,6 +139,29 @@
|
|
|
127
139
|
</div>
|
|
128
140
|
</div>
|
|
129
141
|
</div>
|
|
142
|
+
<Dialog
|
|
143
|
+
ref="confirmDialog"
|
|
144
|
+
class="w-96"
|
|
145
|
+
:click-to-close-outside="false"
|
|
146
|
+
:buttons="[]"
|
|
147
|
+
>
|
|
148
|
+
<div class="flex flex-col">
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
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"
|
|
152
|
+
@click="{ isFetchingPasskey = false; fetchedOptions = null; closeDialog(); }"
|
|
153
|
+
>
|
|
154
|
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
155
|
+
<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"/>
|
|
156
|
+
</svg>
|
|
157
|
+
<span class="sr-only">Close dialog</span>
|
|
158
|
+
</button>
|
|
159
|
+
<div class="flex items-center justify-center space-y-4">
|
|
160
|
+
Now you can add a passkey.
|
|
161
|
+
</div>
|
|
162
|
+
<Button class="mt-4" @click="addPasskeyFinishAction(fetchedOptions); closeDialog();">Add Passkey</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</Dialog>
|
|
130
165
|
</div>
|
|
131
166
|
</template>
|
|
132
167
|
|
|
@@ -135,7 +170,7 @@
|
|
|
135
170
|
import { callAdminForthApi } from '@/utils';
|
|
136
171
|
import adminforth from '@/adminforth';
|
|
137
172
|
import { onMounted, ref, Ref, onBeforeUnmount } from 'vue';
|
|
138
|
-
import { Card, Dialog, ButtonGroup, Tooltip } from '@/afcl'
|
|
173
|
+
import { Card, Dialog, ButtonGroup, Tooltip, Spinner, Button } from '@/afcl'
|
|
139
174
|
import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconCaretDownSolid, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
|
|
140
175
|
import dayjs from 'dayjs';
|
|
141
176
|
import utc from 'dayjs/plugin/utc';
|
|
@@ -152,6 +187,19 @@
|
|
|
152
187
|
const addPasskeyMode: Ref<'platform' | 'cross-platform'> = ref('platform');
|
|
153
188
|
const authenticatorAttachment = ref<'platform' | 'cross-platform' | 'both'>('platform');
|
|
154
189
|
const isInitialFinished = ref(false);
|
|
190
|
+
const isFetchingPasskey = ref(false);
|
|
191
|
+
|
|
192
|
+
const confirmDialog = ref(null);
|
|
193
|
+
|
|
194
|
+
const openDialog = () => {
|
|
195
|
+
confirmDialog.value.open();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const closeDialog = () => {
|
|
199
|
+
confirmDialog.value.close();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const fetchedOptions = ref(null);
|
|
155
203
|
|
|
156
204
|
onMounted(async () => {
|
|
157
205
|
await getPasskeys();
|
|
@@ -179,13 +227,39 @@
|
|
|
179
227
|
}
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
async function
|
|
183
|
-
|
|
230
|
+
async function addPasskeyStartAction() {
|
|
231
|
+
isFetchingPasskey.value = true;
|
|
232
|
+
let confirmationResult;
|
|
233
|
+
try {
|
|
234
|
+
confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(undefined, "To add passkey first verify yourself");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
isFetchingPasskey.value = false;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!confirmationResult) {
|
|
240
|
+
isFetchingPasskey.value = false;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const { options } = await fetchInformationFromTheBackend(confirmationResult);
|
|
244
|
+
if (!options ) {
|
|
245
|
+
isFetchingPasskey.value = false;
|
|
246
|
+
adminforth.alert({message: 'Verification failed.', variant: 'warning'});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
fetchedOptions.value = options;
|
|
250
|
+
openDialog();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function addPasskeyFinishAction(options) {
|
|
184
254
|
const creationResult = await callWebAuthn(options);
|
|
185
255
|
if (!creationResult) {
|
|
256
|
+
isFetchingPasskey.value = false;
|
|
257
|
+
fetchedOptions.value = null;
|
|
186
258
|
return;
|
|
187
259
|
}
|
|
188
|
-
|
|
260
|
+
fetchedOptions.value = null;
|
|
261
|
+
finishRegisteringPasskey(creationResult);
|
|
262
|
+
isFetchingPasskey.value = false;
|
|
189
263
|
}
|
|
190
264
|
|
|
191
265
|
async function getPasskeys() {
|
|
@@ -267,20 +341,24 @@
|
|
|
267
341
|
}
|
|
268
342
|
}
|
|
269
343
|
|
|
270
|
-
async function fetchInformationFromTheBackend() {
|
|
344
|
+
async function fetchInformationFromTheBackend(confirmationResult) {
|
|
271
345
|
let response;
|
|
272
346
|
try {
|
|
273
347
|
response = await callAdminForthApi({
|
|
274
348
|
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
275
349
|
method: 'POST',
|
|
276
350
|
body: {
|
|
277
|
-
mode: addPasskeyMode.value
|
|
351
|
+
mode: addPasskeyMode.value,
|
|
352
|
+
confirmationResult: confirmationResult
|
|
278
353
|
},
|
|
279
354
|
});
|
|
280
355
|
} catch (error) {
|
|
281
356
|
console.error('Error fetching passkeys info:', error);
|
|
282
357
|
return;
|
|
283
358
|
}
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
284
362
|
const _options = response.data;
|
|
285
363
|
const challengeId = response.challengeId;
|
|
286
364
|
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
|
|
@@ -303,7 +381,7 @@
|
|
|
303
381
|
return result;
|
|
304
382
|
}
|
|
305
383
|
|
|
306
|
-
async function finishRegisteringPasskey(credential: any
|
|
384
|
+
async function finishRegisteringPasskey(credential: any) {
|
|
307
385
|
let res
|
|
308
386
|
try {
|
|
309
387
|
res = await callAdminForthApi({
|
|
@@ -312,7 +390,6 @@
|
|
|
312
390
|
body: {
|
|
313
391
|
credential: credential,
|
|
314
392
|
origin: window.location.origin,
|
|
315
|
-
challengeId: challengeId,
|
|
316
393
|
},
|
|
317
394
|
});
|
|
318
395
|
} catch (error) {
|
|
@@ -80,7 +80,7 @@ import VOtpInput from "vue3-otp-input";
|
|
|
80
80
|
import adminforth from '@/adminforth';
|
|
81
81
|
import { useI18n } from 'vue-i18n';
|
|
82
82
|
import { useRoute } from 'vue-router';
|
|
83
|
-
import { handlePasskeyAlert } from './
|
|
83
|
+
import { handlePasskeyAlert } from './utils.js';
|
|
84
84
|
import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
85
85
|
|
|
86
86
|
const { t } = useI18n();
|