@adminforth/two-factors-auth 1.10.7 → 1.11.1

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 CHANGED
@@ -13,5 +13,5 @@ custom/package.json
13
13
  custom/tsconfig.json
14
14
  custom/utils.js
15
15
 
16
- sent 58,612 bytes received 172 bytes 117,568.00 bytes/sec
17
- total size is 57,985 speedup is 0.99
16
+ sent 68,512 bytes received 172 bytes 137,368.00 bytes/sec
17
+ total size is 67,872 speedup is 0.99
@@ -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
- {{ $t('Please enter your authenticator code') }}
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="code"
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-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>
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
- getCode: () => Promise<any>;
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: ((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;
60
104
  let rejectFn: ((err?: any) => void) | null = null;
61
105
 
106
+
62
107
  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) => {
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 code = ref<any>(null);
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
- code.value?.fillInput(pastedText);
168
+ confirmationResult.value?.fillInput(pastedText);
100
169
  }
101
170
  }
102
171
 
103
172
  async function handleOnComplete(value: string) {
104
- await sendCode(value);
173
+ await sendConfirmationResult(value);
105
174
  }
106
175
 
107
- async function sendCode(value: string) {
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
- resolveFn(value);
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
- code.value?.clearInput();
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
- code.value?.clearInput();
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,7 +82,7 @@
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'
@@ -95,6 +98,7 @@
95
98
  const route = useRoute();
96
99
  const router = useRouter();
97
100
  const codeError = ref(null);
101
+ const isFetchingPasskey = ref(false);
98
102
 
99
103
  onBeforeMount(() => {
100
104
  if (localStorage.getItem('isAuthorized') === 'true') {
@@ -131,6 +135,7 @@
131
135
  const doesUserHavePasskeys = ref(false);
132
136
  const confirmationMode = ref("code");
133
137
  const isPasskeysSupported = ref(false);
138
+ const isLoading = ref(true);
134
139
 
135
140
  onMounted(async () => {
136
141
  if (localStorage.getItem('isAuthorized') !== 'true') {
@@ -145,6 +150,7 @@
145
150
  const rootEl = otpRoot.value;
146
151
  rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
147
152
  }
153
+ isLoading.value = false;
148
154
  });
149
155
 
150
156
  watch(route, (newRoute) => {
@@ -231,10 +237,12 @@
231
237
  }
232
238
 
233
239
  async function usePasskeyButton() {
240
+ isFetchingPasskey.value = true;
234
241
  const { _options, challengeId } = await createSignInRequest();
235
242
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
236
243
  const credential = await authenticate(options);
237
244
  if (!credential) {
245
+ isFetchingPasskey.value = false;
238
246
  return;
239
247
  }
240
248
  const result = JSON.stringify(credential);
@@ -244,6 +252,7 @@
244
252
  origin: window.location.origin,
245
253
  };
246
254
  sendCode('', 'passkey', passkeyOptions);
255
+ isFetchingPasskey.value = false;
247
256
  }
248
257
 
249
258
  async function createSignInRequest() {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="text-3xl text-gray-900 font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
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
- <div
26
- @click="passkeysNewName = ''"
27
- class="w-7 h-7 flex items-center justify-center hover:border rounded-md hover:shadow-md text-blue-500 hover:text-blue-700 cursor-pointer"
28
- >
29
- <IconPenSolid class="w-5 h-5" />
30
- </div>
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
- <div class="w-7 h-7 flex items-center justify-center hover:border rounded-md hover:shadow-md text-red-500 hover:text-red-700 cursor-pointer">
52
- <IconTrashBinSolid
53
- class="w-5 h-5"
54
- />
55
- </div>
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="addPasskey()">
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,16 +170,18 @@
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';
142
177
  import timezone from 'dayjs/plugin/timezone';
143
178
  import { useCoreStore } from '@/stores/core';
179
+ import { useUserStore } from '@/stores/user'
144
180
 
145
181
  dayjs.extend(utc);
146
182
  dayjs.extend(timezone);
147
183
  const coreStore = useCoreStore();
184
+ const userStore = useUserStore();
148
185
  const passkeys = ref([]);
149
186
  const isPasskeySupported = ref(false);
150
187
  const passkeysNewName = ref('');
@@ -152,14 +189,29 @@
152
189
  const addPasskeyMode: Ref<'platform' | 'cross-platform'> = ref('platform');
153
190
  const authenticatorAttachment = ref<'platform' | 'cross-platform' | 'both'>('platform');
154
191
  const isInitialFinished = ref(false);
192
+ const isFetchingPasskey = ref(false);
193
+
194
+ const confirmDialog = ref(null);
195
+
196
+ const openDialog = () => {
197
+ confirmDialog.value.open();
198
+ }
199
+
200
+ const closeDialog = () => {
201
+ confirmDialog.value.close();
202
+ }
203
+
204
+ const fetchedOptions = ref(null);
155
205
 
156
206
  onMounted(async () => {
157
- await getPasskeys();
158
- await checkForCompatibility();
159
- if (authenticatorAttachment.value === "cross-platform") {
160
- addPasskeyMode.value = 'cross-platform';
161
- }
162
- isInitialFinished.value = true;
207
+ if (userStore.isAuthorized === true ) {
208
+ await getPasskeys();
209
+ await checkForCompatibility();
210
+ if (authenticatorAttachment.value === "cross-platform") {
211
+ addPasskeyMode.value = 'cross-platform';
212
+ }
213
+ isInitialFinished.value = true;
214
+ }
163
215
  });
164
216
 
165
217
  onMounted(() => {
@@ -179,13 +231,39 @@
179
231
  }
180
232
  }
181
233
 
182
- async function addPasskey() {
183
- const { options, challengeId } = await fetchInformationFromTheBackend();
234
+ async function addPasskeyStartAction() {
235
+ isFetchingPasskey.value = true;
236
+ let confirmationResult;
237
+ try {
238
+ confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(undefined, "To add passkey first verify yourself");
239
+ } catch (e) {
240
+ isFetchingPasskey.value = false;
241
+ return;
242
+ }
243
+ if (!confirmationResult) {
244
+ isFetchingPasskey.value = false;
245
+ return;
246
+ }
247
+ const { options } = await fetchInformationFromTheBackend(confirmationResult);
248
+ if (!options ) {
249
+ isFetchingPasskey.value = false;
250
+ adminforth.alert({message: 'Verification failed.', variant: 'warning'});
251
+ return;
252
+ }
253
+ fetchedOptions.value = options;
254
+ openDialog();
255
+ }
256
+
257
+ async function addPasskeyFinishAction(options) {
184
258
  const creationResult = await callWebAuthn(options);
185
259
  if (!creationResult) {
260
+ isFetchingPasskey.value = false;
261
+ fetchedOptions.value = null;
186
262
  return;
187
263
  }
188
- finishRegisteringPasskey(creationResult, challengeId);
264
+ fetchedOptions.value = null;
265
+ finishRegisteringPasskey(creationResult);
266
+ isFetchingPasskey.value = false;
189
267
  }
190
268
 
191
269
  async function getPasskeys() {
@@ -267,20 +345,24 @@
267
345
  }
268
346
  }
269
347
 
270
- async function fetchInformationFromTheBackend() {
348
+ async function fetchInformationFromTheBackend(confirmationResult) {
271
349
  let response;
272
350
  try {
273
351
  response = await callAdminForthApi({
274
352
  path: `/plugin/passkeys/registerPasskeyRequest`,
275
353
  method: 'POST',
276
354
  body: {
277
- mode: addPasskeyMode.value
355
+ mode: addPasskeyMode.value,
356
+ confirmationResult: confirmationResult
278
357
  },
279
358
  });
280
359
  } catch (error) {
281
360
  console.error('Error fetching passkeys info:', error);
282
361
  return;
283
362
  }
363
+ if (!response.ok) {
364
+ return {};
365
+ }
284
366
  const _options = response.data;
285
367
  const challengeId = response.challengeId;
286
368
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
@@ -303,7 +385,7 @@
303
385
  return result;
304
386
  }
305
387
 
306
- async function finishRegisteringPasskey(credential: any, challengeId: string) {
388
+ async function finishRegisteringPasskey(credential: any) {
307
389
  let res
308
390
  try {
309
391
  res = await callAdminForthApi({
@@ -312,7 +394,6 @@
312
394
  body: {
313
395
  credential: credential,
314
396
  origin: window.location.origin,
315
- challengeId: challengeId,
316
397
  },
317
398
  });
318
399
  } catch (error) {
package/custom/utils.js CHANGED
@@ -1,4 +1,6 @@
1
- export function handlePasskeyAlert(propSuggestionPeriod, router) {
1
+ import { callAdminForthApi } from '@/utils';
2
+
3
+ export function handlePasskeyAlert(propSuggestionPeriod, router) {
2
4
  const currentDate = Date.now();
3
5
  window.localStorage.removeItem('suggestionPeriod');
4
6
  window.localStorage.setItem('suggestionPeriod', propSuggestionPeriod);
@@ -52,4 +54,53 @@
52
54
  }
53
55
  });
54
56
  }
55
- }
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
+ }