@adminforth/two-factors-auth 2.0.6 → 2.1.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 69,822 bytes received 172 bytes 139,988.00 bytes/sec
17
- total size is 69,191 speedup is 0.99
16
+ sent 71,446 bytes received 172 bytes 143,236.00 bytes/sec
17
+ total size is 70,806 speedup is 0.99
@@ -24,11 +24,11 @@
24
24
 
25
25
  <div class="mt-6 flex justify-center items-center gap-32 w-full">
26
26
  <p v-if="doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'passkey'" >use passkey</p>
27
- <button
28
- class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ <Button
28
+ class="px-4 py-2 rounded border"
29
29
  @click="onCancel"
30
30
  :disabled="inProgress"
31
- >{{ $t('Cancel') }}</button>
31
+ >{{ $t('Cancel') }}</Button>
32
32
  </div>
33
33
  </div>
34
34
 
@@ -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) {
@@ -150,11 +150,8 @@
150
150
  await nextTick();
151
151
  await isCMAAvailable();
152
152
  tagOtpInputs();
153
- console.log("Checking if device supports passkeys:", isPasskeysSupported.value);
154
153
  if (isPasskeysSupported.value === true) {
155
- console.log("Device supports passkeys, checking if user has passkeys...");
156
154
  await checkIfUserHasPasskeys();
157
- console.log("Does user have passkeys:", doesUserHavePasskeys.value);
158
155
  }
159
156
  document.addEventListener('focusin', handleGlobalFocusIn, true);
160
157
  focusFirstAvailableOtpInput();
@@ -193,8 +190,6 @@
193
190
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
194
191
  inProgress.value = true;
195
192
  const usePasskey = factorMode === 'passkey';
196
- console.log("Sending code with factorMode:", factorMode);
197
- console.log("Passkey options:", passkeyOptions);
198
193
  const resp = await callAdminForthApi({
199
194
  method: 'POST',
200
195
  path: '/plugin/twofa/confirmLogin',
@@ -205,15 +200,12 @@
205
200
  secret: null,
206
201
  }
207
202
  })
208
- console.log("Response from confirmLogin:", resp);
209
203
  if ( resp.allowedLogin ) {
210
204
  if ( route.meta.isPasskeysEnabled && !doesUserHavePasskeys.value ) {
211
205
  handlePasskeyAlert(route.meta.suggestionPeriod, router);
212
206
  }
213
- console.log("Login confirmed, finishing login...");
214
207
  await user.finishLogin();
215
208
  } else {
216
- console.log("Login not allowed, showing error:", resp.error);
217
209
  if (usePasskey) {
218
210
  showErrorTost(t(resp.error));
219
211
  codeError.value = resp.error || t('Passkey authentication failed');
@@ -277,18 +269,15 @@
277
269
 
278
270
  async function createSignInRequest() {
279
271
  let response;
280
- console.log("Creating sign-in request for passkey...");
281
272
  try {
282
273
  response = await callAdminForthApi({
283
274
  path: `/plugin/passkeys/signInRequest`,
284
275
  method: 'POST',
285
276
  });
286
277
  } catch (error) {
287
- console.log("Error creating sign-in request:", error);
288
278
  console.error('Error creating sign-in request:', error);
289
279
  return;
290
280
  }
291
- console.log("Sign-in request response:", response);
292
281
  if (response.ok === true) {
293
282
  return { _options: response.data, challengeId: response.challengeId };
294
283
  } else {
@@ -298,14 +287,12 @@
298
287
  }
299
288
 
300
289
  async function authenticate(options) {
301
- console.log("Authenticating with options:", options);
302
290
  try {
303
291
  const abortController = new AbortController();
304
292
  const credential = await navigator.credentials.get({
305
293
  publicKey: options,
306
294
  signal: abortController.signal,
307
295
  });
308
- console.log("Credential obtained:", credential);
309
296
  return credential;
310
297
  } catch (error) {
311
298
  console.error('Error during authentication:', error);
@@ -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
@@ -24,11 +24,11 @@
24
24
 
25
25
  <div class="mt-6 flex justify-center items-center gap-32 w-full">
26
26
  <p v-if="doesUserHavePasskeys===true" class="underline hover:no-underline text-lightPrimary whitespace-nowrap hover:cursor-pointer" @click="modalMode = 'passkey'" >use passkey</p>
27
- <button
28
- class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ <Button
28
+ class="px-4 py-2 rounded border"
29
29
  @click="onCancel"
30
30
  :disabled="inProgress"
31
- >{{ $t('Cancel') }}</button>
31
+ >{{ $t('Cancel') }}</Button>
32
32
  </div>
33
33
  </div>
34
34
 
@@ -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) {
@@ -150,11 +150,8 @@
150
150
  await nextTick();
151
151
  await isCMAAvailable();
152
152
  tagOtpInputs();
153
- console.log("Checking if device supports passkeys:", isPasskeysSupported.value);
154
153
  if (isPasskeysSupported.value === true) {
155
- console.log("Device supports passkeys, checking if user has passkeys...");
156
154
  await checkIfUserHasPasskeys();
157
- console.log("Does user have passkeys:", doesUserHavePasskeys.value);
158
155
  }
159
156
  document.addEventListener('focusin', handleGlobalFocusIn, true);
160
157
  focusFirstAvailableOtpInput();
@@ -193,8 +190,6 @@
193
190
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
194
191
  inProgress.value = true;
195
192
  const usePasskey = factorMode === 'passkey';
196
- console.log("Sending code with factorMode:", factorMode);
197
- console.log("Passkey options:", passkeyOptions);
198
193
  const resp = await callAdminForthApi({
199
194
  method: 'POST',
200
195
  path: '/plugin/twofa/confirmLogin',
@@ -205,15 +200,12 @@
205
200
  secret: null,
206
201
  }
207
202
  })
208
- console.log("Response from confirmLogin:", resp);
209
203
  if ( resp.allowedLogin ) {
210
204
  if ( route.meta.isPasskeysEnabled && !doesUserHavePasskeys.value ) {
211
205
  handlePasskeyAlert(route.meta.suggestionPeriod, router);
212
206
  }
213
- console.log("Login confirmed, finishing login...");
214
207
  await user.finishLogin();
215
208
  } else {
216
- console.log("Login not allowed, showing error:", resp.error);
217
209
  if (usePasskey) {
218
210
  showErrorTost(t(resp.error));
219
211
  codeError.value = resp.error || t('Passkey authentication failed');
@@ -277,18 +269,15 @@
277
269
 
278
270
  async function createSignInRequest() {
279
271
  let response;
280
- console.log("Creating sign-in request for passkey...");
281
272
  try {
282
273
  response = await callAdminForthApi({
283
274
  path: `/plugin/passkeys/signInRequest`,
284
275
  method: 'POST',
285
276
  });
286
277
  } catch (error) {
287
- console.log("Error creating sign-in request:", error);
288
278
  console.error('Error creating sign-in request:', error);
289
279
  return;
290
280
  }
291
- console.log("Sign-in request response:", response);
292
281
  if (response.ok === true) {
293
282
  return { _options: response.data, challengeId: response.challengeId };
294
283
  } else {
@@ -298,14 +287,12 @@
298
287
  }
299
288
 
300
289
  async function authenticate(options) {
301
- console.log("Authenticating with options:", options);
302
290
  try {
303
291
  const abortController = new AbortController();
304
292
  const credential = await navigator.credentials.get({
305
293
  publicKey: options,
306
294
  signal: abortController.signal,
307
295
  });
308
- console.log("Credential obtained:", credential);
309
296
  return credential;
310
297
  } catch (error) {
311
298
  console.error('Error during authentication:', error);
@@ -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.0.6",
3
+ "version": "2.1.1",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",