Skip to content

Commit 7125098

Browse files
committed
fix: add focus on the otp input 2fa modal. Remoove temporary logs
1 parent 890f235 commit 7125098

File tree

4 files changed

+86
-22
lines changed

4 files changed

+86
-22
lines changed

custom/TwoFAModal.vue

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@
9797
(e: 'closed'): void
9898
}>();
9999
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+
100119
const modelShow = ref(false);
101120
let resolveFn: ((confirmationResult: string) => void) | null = null;
102121
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
@@ -113,6 +132,7 @@
113132
customDialogTitle.value = title;
114133
}
115134
modelShow.value = true;
135+
await addListeners();
116136
resolveFn = resolve;
117137
rejectFn = reject;
118138
verifyFn = verifyingCallback ?? null;
@@ -144,6 +164,7 @@
144164
result: passkeyData
145165
}
146166
customDialogTitle.value = "";
167+
removeListeners();
147168
resolveFn(dataToReturn);
148169
}
149170
@@ -195,6 +216,7 @@
195216
result: value
196217
}
197218
customDialogTitle.value = "";
219+
removeListeners();
198220
resolveFn(dataToReturn);
199221
}
200222
@@ -203,20 +225,40 @@
203225
modelShow.value = false;
204226
bindValue.value = '';
205227
confirmationResult.value?.clearInput();
228+
removeListeners();
206229
rejectFn("Cancel");
207230
emit('rejected', new Error('cancelled'));
208231
emit('closed');
209232
}
210233
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+
211244
watch(modelShow, async (open) => {
212245
if (open) {
213246
await nextTick();
214247
const htmlRef = document.querySelector('html');
215248
if (htmlRef) {
216249
htmlRef.style.overflow = 'hidden';
217250
}
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+
}
220262
} else {
221263
window.removeEventListener('paste', handlePaste);
222264
const htmlRef = document.querySelector('html');
@@ -248,7 +290,47 @@
248290
});
249291
}
250292
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>
252334

253335
<style scoped>
254336
:deep(.otp-input-container) {

custom/TwoFactorsConfirmation.vue

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,8 @@
150150
await nextTick();
151151
await isCMAAvailable();
152152
tagOtpInputs();
153-
console.log("Checking if device supports passkeys:", isPasskeysSupported.value);
154153
if (isPasskeysSupported.value === true) {
155-
console.log("Device supports passkeys, checking if user has passkeys...");
156154
await checkIfUserHasPasskeys();
157-
console.log("Does user have passkeys:", doesUserHavePasskeys.value);
158155
}
159156
document.addEventListener('focusin', handleGlobalFocusIn, true);
160157
focusFirstAvailableOtpInput();
@@ -193,8 +190,6 @@
193190
async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
194191
inProgress.value = true;
195192
const usePasskey = factorMode === 'passkey';
196-
console.log("Sending code with factorMode:", factorMode);
197-
console.log("Passkey options:", passkeyOptions);
198193
const resp = await callAdminForthApi({
199194
method: 'POST',
200195
path: '/plugin/twofa/confirmLogin',
@@ -205,15 +200,12 @@
205200
secret: null,
206201
}
207202
})
208-
console.log("Response from confirmLogin:", resp);
209203
if ( resp.allowedLogin ) {
210204
if ( route.meta.isPasskeysEnabled && !doesUserHavePasskeys.value ) {
211205
handlePasskeyAlert(route.meta.suggestionPeriod, router);
212206
}
213-
console.log("Login confirmed, finishing login...");
214207
await user.finishLogin();
215208
} else {
216-
console.log("Login not allowed, showing error:", resp.error);
217209
if (usePasskey) {
218210
showErrorTost(t(resp.error));
219211
codeError.value = resp.error || t('Passkey authentication failed');
@@ -277,18 +269,15 @@
277269
278270
async function createSignInRequest() {
279271
let response;
280-
console.log("Creating sign-in request for passkey...");
281272
try {
282273
response = await callAdminForthApi({
283274
path: `/plugin/passkeys/signInRequest`,
284275
method: 'POST',
285276
});
286277
} catch (error) {
287-
console.log("Error creating sign-in request:", error);
288278
console.error('Error creating sign-in request:', error);
289279
return;
290280
}
291-
console.log("Sign-in request response:", response);
292281
if (response.ok === true) {
293282
return { _options: response.data, challengeId: response.challengeId };
294283
} else {
@@ -298,14 +287,12 @@
298287
}
299288
300289
async function authenticate(options) {
301-
console.log("Authenticating with options:", options);
302290
try {
303291
const abortController = new AbortController();
304292
const credential = await navigator.credentials.get({
305293
publicKey: options,
306294
signal: abortController.signal,
307295
});
308-
console.log("Credential obtained:", credential);
309296
return credential;
310297
} catch (error) {
311298
console.error('Error during authentication:', error);

custom/TwoFactorsPasskeysSettings.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
class="w-96"
145145
:click-to-close-outside="false"
146146
:buttons="[]"
147+
:closable="false"
147148
>
148149
<div class="flex flex-col">
149150
<button

index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,21 +383,15 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
383383
let verified = null;
384384
if (body.usePasskey && this.options.passkeys) {
385385
// passkeys are enabled and user wants to use them
386-
console.log("Using passkey for 2FA verification");
387386
const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`});
388-
console.log("Passkey cookies:", passkeysCookies);
389387
if (!passkeysCookies) {
390388
return { error: 'Passkey token is required' };
391389
}
392-
console.log("Verifying passkey cookies");
393390
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
394391
if (!decodedPasskeysCookies) {
395392
return { error: 'Invalid passkey' };
396393
}
397-
console.log("Passkey cookies decoded:", decodedPasskeysCookies);
398-
console.log("Verifying passkey response");
399394
const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
400-
console.log("Passkey response verification result:", res);
401395
if (res.ok && res.passkeyConfirmed) {
402396
verified = true;
403397
}

0 commit comments

Comments
 (0)