@adminforth/two-factors-auth 2.1.0 → 2.1.2

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 69,802 bytes received 172 bytes 139,948.00 bytes/sec
17
- total size is 69,162 speedup is 0.99
16
+ sent 74,931 bytes received 172 bytes 150,206.00 bytes/sec
17
+ total size is 74,291 speedup is 0.99
@@ -97,6 +97,25 @@
97
97
  (e: 'closed'): void
98
98
  }>();
99
99
 
100
+ function removeListeners() {
101
+ window.removeEventListener('paste', handlePaste);
102
+ document.removeEventListener('focusin', handleGlobalFocusIn, true);
103
+ const rootEl = otpRoot.value;
104
+ rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
105
+ }
106
+
107
+ async function addListeners() {
108
+ document.addEventListener('focusin', handleGlobalFocusIn, true);
109
+
110
+ // Wait for DOM to be ready and OTP inputs to be rendered
111
+ await nextTick();
112
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for v-otp-input to render
113
+
114
+ focusFirstAvailableOtpInput();
115
+ const rootEl = otpRoot.value;
116
+ rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
117
+ }
118
+
100
119
  const modelShow = ref(false);
101
120
  let resolveFn: ((confirmationResult: string) => void) | null = null;
102
121
  let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
@@ -113,6 +132,7 @@
113
132
  customDialogTitle.value = title;
114
133
  }
115
134
  modelShow.value = true;
135
+ await addListeners();
116
136
  resolveFn = resolve;
117
137
  rejectFn = reject;
118
138
  verifyFn = verifyingCallback ?? null;
@@ -144,6 +164,7 @@
144
164
  result: passkeyData
145
165
  }
146
166
  customDialogTitle.value = "";
167
+ removeListeners();
147
168
  resolveFn(dataToReturn);
148
169
  }
149
170
 
@@ -195,6 +216,7 @@
195
216
  result: value
196
217
  }
197
218
  customDialogTitle.value = "";
219
+ removeListeners();
198
220
  resolveFn(dataToReturn);
199
221
  }
200
222
 
@@ -203,11 +225,22 @@
203
225
  modelShow.value = false;
204
226
  bindValue.value = '';
205
227
  confirmationResult.value?.clearInput();
228
+ removeListeners();
206
229
  rejectFn("Cancel");
207
230
  emit('rejected', new Error('cancelled'));
208
231
  emit('closed');
209
232
  }
210
233
 
234
+ watch(modalMode, async (newMode) => {
235
+ if (newMode === 'totp' && modelShow.value && !isLoading.value) {
236
+ await nextTick();
237
+ setTimeout(() => {
238
+ tagOtpInputs();
239
+ focusFirstAvailableOtpInput();
240
+ }, 100);
241
+ }
242
+ });
243
+
211
244
  watch(modelShow, async (open) => {
212
245
  if (open) {
213
246
  await nextTick();
@@ -215,8 +248,17 @@
215
248
  if (htmlRef) {
216
249
  htmlRef.style.overflow = 'hidden';
217
250
  }
218
- tagOtpInputs();
219
- window.addEventListener('paste', handlePaste);
251
+
252
+ // Wait for conditional rendering to complete
253
+ if (modalMode.value === 'totp' && !isLoading.value) {
254
+ await nextTick();
255
+ setTimeout(() => {
256
+ tagOtpInputs();
257
+ window.addEventListener('paste', handlePaste);
258
+ }, 100);
259
+ } else {
260
+ window.addEventListener('paste', handlePaste);
261
+ }
220
262
  } else {
221
263
  window.removeEventListener('paste', handlePaste);
222
264
  const htmlRef = document.querySelector('html');
@@ -248,7 +290,47 @@
248
290
  });
249
291
  }
250
292
 
251
- </script>
293
+ function getOtpInputs() {
294
+ const root = otpRoot.value;
295
+ if (!root) return [];
296
+ return Array.from(root.querySelectorAll('input.otp-input'));
297
+ }
298
+
299
+ function focusFirstAvailableOtpInput() {
300
+ const inputs = getOtpInputs();
301
+ if (!inputs.length) {
302
+ // Retry after a short delay if inputs aren't ready yet
303
+ setTimeout(() => focusFirstAvailableOtpInput(), 50);
304
+ return;
305
+ }
306
+ const firstEmpty = inputs.find((i) => !(i as HTMLInputElement).value);
307
+ ((firstEmpty || inputs[0]) as HTMLInputElement).focus();
308
+ }
309
+
310
+ function handleGlobalFocusIn(event) {
311
+ const inputs = getOtpInputs();
312
+ if (!inputs.length) return;
313
+ const target = event.target;
314
+ if (!target) return;
315
+ if (!inputs.includes(target)) {
316
+ requestAnimationFrame(() => {
317
+ focusFirstAvailableOtpInput();
318
+ });
319
+ }
320
+ }
321
+
322
+ function handleFocusOut() {
323
+ requestAnimationFrame(() => {
324
+ const inputs = getOtpInputs();
325
+ if (!inputs.length) return;
326
+ const active = document.activeElement;
327
+ if (!active || !inputs.includes(active)) {
328
+ focusFirstAvailableOtpInput();
329
+ }
330
+ });
331
+ }
332
+
333
+ </script>
252
334
 
253
335
  <style scoped>
254
336
  :deep(.otp-input-container) {
@@ -98,6 +98,9 @@
98
98
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
99
99
  import ErrorMessage from '@/components/ErrorMessage.vue';
100
100
  import { handlePasskeyAlert } from './utils.js';
101
+
102
+ // Global provided by AdminForth runtime
103
+ declare const adminforth: any;
101
104
 
102
105
  const { t } = useI18n();
103
106
  const code = ref(null);
@@ -108,6 +111,29 @@
108
111
  const codeError = ref(null);
109
112
  const isFetchingPasskey = ref(false);
110
113
 
114
+ // Shared WebAuthn state to prevent concurrent navigator.credentials.get calls
115
+ const webAuthn = {
116
+ controller: null as AbortController | null,
117
+ inFlight: false,
118
+ };
119
+
120
+ function cancelPendingWebAuthn(reason?: string) {
121
+ try {
122
+ if (webAuthn.controller) {
123
+ // Abort any pending WebAuthn request
124
+ webAuthn.controller.abort();
125
+ }
126
+ } catch (e) {
127
+ // no-op
128
+ } finally {
129
+ webAuthn.controller = null;
130
+ webAuthn.inFlight = false;
131
+ if (reason) {
132
+ console.debug('[WebAuthn] Aborted pending request:', reason);
133
+ }
134
+ }
135
+ }
136
+
111
137
  onBeforeMount(() => {
112
138
  if (localStorage.getItem('isAuthorized') === 'true') {
113
139
  coreStore.fetchMenuAndResource();
@@ -147,6 +173,8 @@
147
173
 
148
174
  onMounted(async () => {
149
175
  if (localStorage.getItem('isAuthorized') !== 'true') {
176
+ // Safety: ensure no pending WebAuthn request survives SPA navigation
177
+ cancelPendingWebAuthn('mount-init');
150
178
  await nextTick();
151
179
  await isCMAAvailable();
152
180
  tagOtpInputs();
@@ -167,8 +195,10 @@
167
195
  watch(route, (newRoute) => {
168
196
  codeError.value = null;
169
197
  if ( newRoute.hash === '#passkey' ) {
198
+ cancelPendingWebAuthn('switch-to-passkey');
170
199
  confirmationMode.value = 'passkey';
171
200
  } else if ( newRoute.hash === '#code' ) {
201
+ cancelPendingWebAuthn('switch-to-code');
172
202
  confirmationMode.value = 'code';
173
203
  }
174
204
  });
@@ -188,6 +218,8 @@
188
218
  document.removeEventListener('focusin', handleGlobalFocusIn, true);
189
219
  const rootEl = otpRoot.value;
190
220
  rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
221
+ // Abort any in-flight WebAuthn request when leaving the component
222
+ cancelPendingWebAuthn('component-unmount');
191
223
  });
192
224
 
193
225
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
@@ -257,8 +289,16 @@
257
289
  }
258
290
 
259
291
  async function usePasskeyButton() {
292
+ // Cancel any stray pending requests from earlier flows
293
+ cancelPendingWebAuthn('button-pressed');
294
+ codeError.value = null;
260
295
  isFetchingPasskey.value = true;
261
- const { _options, challengeId } = await createSignInRequest();
296
+ const signIn = await createSignInRequest();
297
+ if (!signIn) {
298
+ isFetchingPasskey.value = false;
299
+ return;
300
+ }
301
+ const { _options, challengeId } = signIn;
262
302
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
263
303
  const credential = await authenticate(options);
264
304
  if (!credential) {
@@ -300,25 +340,52 @@
300
340
  async function authenticate(options) {
301
341
  console.log("Authenticating with options:", options);
302
342
  try {
343
+ // Guard: prevent concurrent navigator.credentials.get calls
344
+ if (webAuthn.inFlight) {
345
+ console.warn('[WebAuthn] A request is already in flight. Aborting previous and retrying.');
346
+ cancelPendingWebAuthn('pre-auth-guard');
347
+ }
348
+
303
349
  const abortController = new AbortController();
350
+ webAuthn.controller = abortController;
351
+ webAuthn.inFlight = true;
352
+
304
353
  const credential = await navigator.credentials.get({
305
354
  publicKey: options,
306
355
  signal: abortController.signal,
356
+ // mediation can be set if using conditional UI, omitted here intentionally
307
357
  });
308
358
  console.log("Credential obtained:", credential);
309
359
  return credential;
310
360
  } catch (error) {
311
361
  console.error('Error during authentication:', error);
362
+ // Handle specific concurrent/pending request error cases gracefully
363
+ const name = (error && (error.name || error.constructor?.name)) || '';
364
+ const message = (error && error.message) || '';
365
+ if (name === 'AbortError') {
366
+ // Aborted intentionally; no user-facing error needed
367
+ return null;
368
+ }
369
+ if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) {
370
+ adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' });
371
+ codeError.value = t('A previous passkey attempt was still pending. Please try again.');
372
+ return null;
373
+ }
312
374
  adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'});
313
375
  codeError.value = 'Error during authentication.';
314
376
  }
377
+ finally {
378
+ // Clear in-flight state regardless of outcome
379
+ webAuthn.inFlight = false;
380
+ webAuthn.controller = null;
381
+ }
315
382
  }
316
383
 
317
384
 
318
- function getOtpInputs() {
385
+ function getOtpInputs(): HTMLInputElement[] {
319
386
  const root = otpRoot.value;
320
387
  if (!root) return [];
321
- return Array.from(root.querySelectorAll('input.otp-input'));
388
+ return Array.from(root.querySelectorAll('input.otp-input')) as HTMLInputElement[];
322
389
  }
323
390
 
324
391
  function focusFirstAvailableOtpInput() {
@@ -331,7 +398,7 @@
331
398
  function handleGlobalFocusIn(event) {
332
399
  const inputs = getOtpInputs();
333
400
  if (!inputs.length) return;
334
- const target = event.target;
401
+ const target = event.target as HTMLInputElement | null;
335
402
  if (!target) return;
336
403
  if (!inputs.includes(target)) {
337
404
  requestAnimationFrame(() => {
@@ -344,8 +411,9 @@
344
411
  requestAnimationFrame(() => {
345
412
  const inputs = getOtpInputs();
346
413
  if (!inputs.length) return;
347
- const active = document.activeElement;
348
- if (!active || !inputs.includes(active)) {
414
+ const active = document.activeElement as Element | null;
415
+ const activeInput = active instanceof HTMLInputElement ? active : null;
416
+ if (!activeInput || !inputs.includes(activeInput)) {
349
417
  focusFirstAvailableOtpInput();
350
418
  }
351
419
  });
@@ -144,6 +144,7 @@
144
144
  class="w-96"
145
145
  :click-to-close-outside="false"
146
146
  :buttons="[]"
147
+ :closable="false"
147
148
  >
148
149
  <div class="flex flex-col">
149
150
  <button
@@ -97,6 +97,25 @@
97
97
  (e: 'closed'): void
98
98
  }>();
99
99
 
100
+ function removeListeners() {
101
+ window.removeEventListener('paste', handlePaste);
102
+ document.removeEventListener('focusin', handleGlobalFocusIn, true);
103
+ const rootEl = otpRoot.value;
104
+ rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
105
+ }
106
+
107
+ async function addListeners() {
108
+ document.addEventListener('focusin', handleGlobalFocusIn, true);
109
+
110
+ // Wait for DOM to be ready and OTP inputs to be rendered
111
+ await nextTick();
112
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for v-otp-input to render
113
+
114
+ focusFirstAvailableOtpInput();
115
+ const rootEl = otpRoot.value;
116
+ rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
117
+ }
118
+
100
119
  const modelShow = ref(false);
101
120
  let resolveFn: ((confirmationResult: string) => void) | null = null;
102
121
  let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
@@ -113,6 +132,7 @@
113
132
  customDialogTitle.value = title;
114
133
  }
115
134
  modelShow.value = true;
135
+ await addListeners();
116
136
  resolveFn = resolve;
117
137
  rejectFn = reject;
118
138
  verifyFn = verifyingCallback ?? null;
@@ -144,6 +164,7 @@
144
164
  result: passkeyData
145
165
  }
146
166
  customDialogTitle.value = "";
167
+ removeListeners();
147
168
  resolveFn(dataToReturn);
148
169
  }
149
170
 
@@ -195,6 +216,7 @@
195
216
  result: value
196
217
  }
197
218
  customDialogTitle.value = "";
219
+ removeListeners();
198
220
  resolveFn(dataToReturn);
199
221
  }
200
222
 
@@ -203,11 +225,22 @@
203
225
  modelShow.value = false;
204
226
  bindValue.value = '';
205
227
  confirmationResult.value?.clearInput();
228
+ removeListeners();
206
229
  rejectFn("Cancel");
207
230
  emit('rejected', new Error('cancelled'));
208
231
  emit('closed');
209
232
  }
210
233
 
234
+ watch(modalMode, async (newMode) => {
235
+ if (newMode === 'totp' && modelShow.value && !isLoading.value) {
236
+ await nextTick();
237
+ setTimeout(() => {
238
+ tagOtpInputs();
239
+ focusFirstAvailableOtpInput();
240
+ }, 100);
241
+ }
242
+ });
243
+
211
244
  watch(modelShow, async (open) => {
212
245
  if (open) {
213
246
  await nextTick();
@@ -215,8 +248,17 @@
215
248
  if (htmlRef) {
216
249
  htmlRef.style.overflow = 'hidden';
217
250
  }
218
- tagOtpInputs();
219
- window.addEventListener('paste', handlePaste);
251
+
252
+ // Wait for conditional rendering to complete
253
+ if (modalMode.value === 'totp' && !isLoading.value) {
254
+ await nextTick();
255
+ setTimeout(() => {
256
+ tagOtpInputs();
257
+ window.addEventListener('paste', handlePaste);
258
+ }, 100);
259
+ } else {
260
+ window.addEventListener('paste', handlePaste);
261
+ }
220
262
  } else {
221
263
  window.removeEventListener('paste', handlePaste);
222
264
  const htmlRef = document.querySelector('html');
@@ -248,7 +290,47 @@
248
290
  });
249
291
  }
250
292
 
251
- </script>
293
+ function getOtpInputs() {
294
+ const root = otpRoot.value;
295
+ if (!root) return [];
296
+ return Array.from(root.querySelectorAll('input.otp-input'));
297
+ }
298
+
299
+ function focusFirstAvailableOtpInput() {
300
+ const inputs = getOtpInputs();
301
+ if (!inputs.length) {
302
+ // Retry after a short delay if inputs aren't ready yet
303
+ setTimeout(() => focusFirstAvailableOtpInput(), 50);
304
+ return;
305
+ }
306
+ const firstEmpty = inputs.find((i) => !(i as HTMLInputElement).value);
307
+ ((firstEmpty || inputs[0]) as HTMLInputElement).focus();
308
+ }
309
+
310
+ function handleGlobalFocusIn(event) {
311
+ const inputs = getOtpInputs();
312
+ if (!inputs.length) return;
313
+ const target = event.target;
314
+ if (!target) return;
315
+ if (!inputs.includes(target)) {
316
+ requestAnimationFrame(() => {
317
+ focusFirstAvailableOtpInput();
318
+ });
319
+ }
320
+ }
321
+
322
+ function handleFocusOut() {
323
+ requestAnimationFrame(() => {
324
+ const inputs = getOtpInputs();
325
+ if (!inputs.length) return;
326
+ const active = document.activeElement;
327
+ if (!active || !inputs.includes(active)) {
328
+ focusFirstAvailableOtpInput();
329
+ }
330
+ });
331
+ }
332
+
333
+ </script>
252
334
 
253
335
  <style scoped>
254
336
  :deep(.otp-input-container) {
@@ -98,6 +98,9 @@
98
98
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
99
99
  import ErrorMessage from '@/components/ErrorMessage.vue';
100
100
  import { handlePasskeyAlert } from './utils.js';
101
+
102
+ // Global provided by AdminForth runtime
103
+ declare const adminforth: any;
101
104
 
102
105
  const { t } = useI18n();
103
106
  const code = ref(null);
@@ -108,6 +111,29 @@
108
111
  const codeError = ref(null);
109
112
  const isFetchingPasskey = ref(false);
110
113
 
114
+ // Shared WebAuthn state to prevent concurrent navigator.credentials.get calls
115
+ const webAuthn = {
116
+ controller: null as AbortController | null,
117
+ inFlight: false,
118
+ };
119
+
120
+ function cancelPendingWebAuthn(reason?: string) {
121
+ try {
122
+ if (webAuthn.controller) {
123
+ // Abort any pending WebAuthn request
124
+ webAuthn.controller.abort();
125
+ }
126
+ } catch (e) {
127
+ // no-op
128
+ } finally {
129
+ webAuthn.controller = null;
130
+ webAuthn.inFlight = false;
131
+ if (reason) {
132
+ console.debug('[WebAuthn] Aborted pending request:', reason);
133
+ }
134
+ }
135
+ }
136
+
111
137
  onBeforeMount(() => {
112
138
  if (localStorage.getItem('isAuthorized') === 'true') {
113
139
  coreStore.fetchMenuAndResource();
@@ -147,6 +173,8 @@
147
173
 
148
174
  onMounted(async () => {
149
175
  if (localStorage.getItem('isAuthorized') !== 'true') {
176
+ // Safety: ensure no pending WebAuthn request survives SPA navigation
177
+ cancelPendingWebAuthn('mount-init');
150
178
  await nextTick();
151
179
  await isCMAAvailable();
152
180
  tagOtpInputs();
@@ -167,8 +195,10 @@
167
195
  watch(route, (newRoute) => {
168
196
  codeError.value = null;
169
197
  if ( newRoute.hash === '#passkey' ) {
198
+ cancelPendingWebAuthn('switch-to-passkey');
170
199
  confirmationMode.value = 'passkey';
171
200
  } else if ( newRoute.hash === '#code' ) {
201
+ cancelPendingWebAuthn('switch-to-code');
172
202
  confirmationMode.value = 'code';
173
203
  }
174
204
  });
@@ -188,6 +218,8 @@
188
218
  document.removeEventListener('focusin', handleGlobalFocusIn, true);
189
219
  const rootEl = otpRoot.value;
190
220
  rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
221
+ // Abort any in-flight WebAuthn request when leaving the component
222
+ cancelPendingWebAuthn('component-unmount');
191
223
  });
192
224
 
193
225
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
@@ -257,8 +289,16 @@
257
289
  }
258
290
 
259
291
  async function usePasskeyButton() {
292
+ // Cancel any stray pending requests from earlier flows
293
+ cancelPendingWebAuthn('button-pressed');
294
+ codeError.value = null;
260
295
  isFetchingPasskey.value = true;
261
- const { _options, challengeId } = await createSignInRequest();
296
+ const signIn = await createSignInRequest();
297
+ if (!signIn) {
298
+ isFetchingPasskey.value = false;
299
+ return;
300
+ }
301
+ const { _options, challengeId } = signIn;
262
302
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
263
303
  const credential = await authenticate(options);
264
304
  if (!credential) {
@@ -300,25 +340,52 @@
300
340
  async function authenticate(options) {
301
341
  console.log("Authenticating with options:", options);
302
342
  try {
343
+ // Guard: prevent concurrent navigator.credentials.get calls
344
+ if (webAuthn.inFlight) {
345
+ console.warn('[WebAuthn] A request is already in flight. Aborting previous and retrying.');
346
+ cancelPendingWebAuthn('pre-auth-guard');
347
+ }
348
+
303
349
  const abortController = new AbortController();
350
+ webAuthn.controller = abortController;
351
+ webAuthn.inFlight = true;
352
+
304
353
  const credential = await navigator.credentials.get({
305
354
  publicKey: options,
306
355
  signal: abortController.signal,
356
+ // mediation can be set if using conditional UI, omitted here intentionally
307
357
  });
308
358
  console.log("Credential obtained:", credential);
309
359
  return credential;
310
360
  } catch (error) {
311
361
  console.error('Error during authentication:', error);
362
+ // Handle specific concurrent/pending request error cases gracefully
363
+ const name = (error && (error.name || error.constructor?.name)) || '';
364
+ const message = (error && error.message) || '';
365
+ if (name === 'AbortError') {
366
+ // Aborted intentionally; no user-facing error needed
367
+ return null;
368
+ }
369
+ if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) {
370
+ adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' });
371
+ codeError.value = t('A previous passkey attempt was still pending. Please try again.');
372
+ return null;
373
+ }
312
374
  adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'});
313
375
  codeError.value = 'Error during authentication.';
314
376
  }
377
+ finally {
378
+ // Clear in-flight state regardless of outcome
379
+ webAuthn.inFlight = false;
380
+ webAuthn.controller = null;
381
+ }
315
382
  }
316
383
 
317
384
 
318
- function getOtpInputs() {
385
+ function getOtpInputs(): HTMLInputElement[] {
319
386
  const root = otpRoot.value;
320
387
  if (!root) return [];
321
- return Array.from(root.querySelectorAll('input.otp-input'));
388
+ return Array.from(root.querySelectorAll('input.otp-input')) as HTMLInputElement[];
322
389
  }
323
390
 
324
391
  function focusFirstAvailableOtpInput() {
@@ -331,7 +398,7 @@
331
398
  function handleGlobalFocusIn(event) {
332
399
  const inputs = getOtpInputs();
333
400
  if (!inputs.length) return;
334
- const target = event.target;
401
+ const target = event.target as HTMLInputElement | null;
335
402
  if (!target) return;
336
403
  if (!inputs.includes(target)) {
337
404
  requestAnimationFrame(() => {
@@ -344,8 +411,9 @@
344
411
  requestAnimationFrame(() => {
345
412
  const inputs = getOtpInputs();
346
413
  if (!inputs.length) return;
347
- const active = document.activeElement;
348
- if (!active || !inputs.includes(active)) {
414
+ const active = document.activeElement as Element | null;
415
+ const activeInput = active instanceof HTMLInputElement ? active : null;
416
+ if (!activeInput || !inputs.includes(activeInput)) {
349
417
  focusFirstAvailableOtpInput();
350
418
  }
351
419
  });
@@ -144,6 +144,7 @@
144
144
  class="w-96"
145
145
  :click-to-close-outside="false"
146
146
  :buttons="[]"
147
+ :closable="false"
147
148
  >
148
149
  <div class="flex flex-col">
149
150
  <button
package/dist/index.js CHANGED
@@ -343,21 +343,15 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
343
343
  let verified = null;
344
344
  if (body.usePasskey && this.options.passkeys) {
345
345
  // passkeys are enabled and user wants to use them
346
- console.log("Using passkey for 2FA verification");
347
346
  const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyLoginTemporaryJWT` });
348
- console.log("Passkey cookies:", passkeysCookies);
349
347
  if (!passkeysCookies) {
350
348
  return { error: 'Passkey token is required' };
351
349
  }
352
- console.log("Verifying passkey cookies");
353
350
  const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
354
351
  if (!decodedPasskeysCookies) {
355
352
  return { error: 'Invalid passkey' };
356
353
  }
357
- console.log("Passkey cookies decoded:", decodedPasskeysCookies);
358
- console.log("Verifying passkey response");
359
354
  const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
360
- console.log("Passkey response verification result:", res);
361
355
  if (res.ok && res.passkeyConfirmed) {
362
356
  verified = true;
363
357
  }
package/index.ts CHANGED
@@ -383,21 +383,15 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
383
383
  let verified = null;
384
384
  if (body.usePasskey && this.options.passkeys) {
385
385
  // passkeys are enabled and user wants to use them
386
- console.log("Using passkey for 2FA verification");
387
386
  const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`});
388
- console.log("Passkey cookies:", passkeysCookies);
389
387
  if (!passkeysCookies) {
390
388
  return { error: 'Passkey token is required' };
391
389
  }
392
- console.log("Verifying passkey cookies");
393
390
  const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
394
391
  if (!decodedPasskeysCookies) {
395
392
  return { error: 'Invalid passkey' };
396
393
  }
397
- console.log("Passkey cookies decoded:", decodedPasskeysCookies);
398
- console.log("Verifying passkey response");
399
394
  const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
400
- console.log("Passkey response verification result:", res);
401
395
  if (res.ok && res.passkeyConfirmed) {
402
396
  verified = true;
403
397
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",