@adminforth/two-factors-auth 4.8.0 → 4.8.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 +6 -2
- package/custom/LoginWithPasskeyButton.vue +8 -9
- package/custom/TwoFAModal.vue +48 -225
- package/custom/TwoFaCard.vue +43 -0
- package/custom/TwoFaCodeInput.vue +131 -0
- package/custom/TwoFaHint.vue +10 -0
- package/custom/TwoFaLinkButton.vue +5 -0
- package/custom/TwoFactorsConfirmation.vue +39 -230
- package/custom/TwoFactorsPasskeysSettings.vue +45 -36
- package/custom/TwoFactorsSetup.vue +59 -150
- package/dist/custom/LoginWithPasskeyButton.vue +8 -9
- package/dist/custom/TwoFAModal.vue +48 -225
- package/dist/custom/TwoFaCard.vue +43 -0
- package/dist/custom/TwoFaCodeInput.vue +131 -0
- package/dist/custom/TwoFaHint.vue +10 -0
- package/dist/custom/TwoFaLinkButton.vue +5 -0
- package/dist/custom/TwoFactorsConfirmation.vue +39 -230
- package/dist/custom/TwoFactorsPasskeysSettings.vue +45 -36
- package/dist/custom/TwoFactorsSetup.vue +59 -150
- package/package.json +1 -1
|
@@ -8,85 +8,77 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
11
|
-
<div id="authentication-modal"
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</div>
|
|
35
|
-
<div class="my-4 w-full flex justify-center" ref="otpRoot">
|
|
36
|
-
<v-otp-input
|
|
37
|
-
ref="code"
|
|
38
|
-
container-class="grid grid-cols-6 gap-3 w-full"
|
|
39
|
-
input-classes="otp-input bg-gray-50 text-center border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block h-11 w-full 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"
|
|
40
|
-
:num-inputs="6"
|
|
41
|
-
inputType="number"
|
|
42
|
-
inputmode="numeric"
|
|
43
|
-
:should-auto-focus="true"
|
|
44
|
-
:should-focus-order="true"
|
|
45
|
-
v-model:value="bindValue"
|
|
46
|
-
@on-complete="handleOnComplete"
|
|
47
|
-
/>
|
|
48
|
-
</div>
|
|
49
|
-
<!-- <Vue2FACodeInput v-model="code" autofocus /> -->
|
|
50
|
-
<div class="flex flex-row gap-2.5 h-12">
|
|
51
|
-
<LinkButton to="/login" class="w-full">
|
|
52
|
-
{{$t('Back to login')}}
|
|
53
|
-
</LinkButton>
|
|
54
|
-
<Button v-if="skipAllowed" @click="handleSkip" class="w-full">
|
|
55
|
-
{{$t('Skip for now')}}
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
11
|
+
<div id="authentication-modal" class="af-two-factors-setup flex items-center justify-center w-full p-4">
|
|
12
|
+
<TwoFaCard
|
|
13
|
+
class="af-login-popup"
|
|
14
|
+
:title="$t('Set up two-factor authentication')"
|
|
15
|
+
:subtitle="$t('Add this account to your authenticator app')"
|
|
16
|
+
:error="codeError"
|
|
17
|
+
>
|
|
18
|
+
<div>
|
|
19
|
+
<p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-gray-900 dark:text-white">
|
|
20
|
+
<span class="flex w-[22px] h-[22px] shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-xs font-semibold text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">1</span>
|
|
21
|
+
{{ $t('Scan the QR code with your authenticator app') }}
|
|
22
|
+
</p>
|
|
23
|
+
<div class="flex items-center gap-4 rounded-[10px] border border-lightDialogBreakLine bg-gray-50 p-3.5 dark:border-darkDialogBreakLine dark:bg-gray-800">
|
|
24
|
+
<div class="w-[132px] h-[132px] shrink-0 rounded-lg bg-white p-2">
|
|
25
|
+
<img v-if="totpQrCode" :src="totpQrCode" class="af-qr-code w-full h-full" alt="QR code" />
|
|
26
|
+
</div>
|
|
27
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
28
|
+
<p class="text-[13px] leading-[19px] text-gray-500 dark:text-gray-400">{{ $t('Setting up on this device?') }}</p>
|
|
29
|
+
<a :href="totpUri" class="inline-flex items-center gap-1.5 text-[13px] font-medium text-lightPrimary hover:underline dark:text-darkPrimary">
|
|
30
|
+
{{ $t('Open in authenticator app') }}
|
|
31
|
+
<IconArrowUpRightFromSquareOutline class="w-4 h-4" />
|
|
32
|
+
</a>
|
|
33
|
+
</div>
|
|
59
34
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
>
|
|
64
|
-
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div>
|
|
38
|
+
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">{{ $t('Or enter the key manually') }}</p>
|
|
39
|
+
<div class="flex items-center gap-2 rounded-[10px] border border-lightDialogBreakLine bg-gray-50 py-1.5 pl-3.5 pr-1.5 dark:border-darkDialogBreakLine dark:bg-gray-800">
|
|
40
|
+
<span class="af-2fa-secret flex-1 min-w-0 select-all break-all font-mono text-sm tracking-wider text-gray-800 dark:text-gray-100">{{ totp.newSecret }}</span>
|
|
41
|
+
<button type="button" :aria-label="$t('Copy key')" class="af-2fa-copy-btn inline-flex w-9 h-9 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover dark:text-gray-400 dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover" @click="onCopyClick">
|
|
42
|
+
<IconFileCopyOutline class="w-5 h-5" />
|
|
43
|
+
</button>
|
|
65
44
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<TwoFaCodeInput ref="codeInput" :invalid="Boolean(codeError)" @complete="handleOnComplete">
|
|
48
|
+
<template #label>
|
|
49
|
+
<p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-gray-900 dark:text-white">
|
|
50
|
+
<span class="flex w-[22px] h-[22px] shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-xs font-semibold text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">2</span>
|
|
51
|
+
{{ $t('Enter the 6-digit code from the app') }}
|
|
52
|
+
</p>
|
|
53
|
+
</template>
|
|
54
|
+
</TwoFaCodeInput>
|
|
55
|
+
|
|
56
|
+
<template #footer>
|
|
57
|
+
<button v-if="skipAllowed" type="button" class="af-2fa-skip-btn inline-flex h-11 items-center whitespace-nowrap text-[13px] font-medium text-gray-600 hover:underline dark:text-gray-300" @click="handleSkip">
|
|
58
|
+
{{ $t('Skip for now') }}
|
|
59
|
+
</button>
|
|
60
|
+
<LinkButton to="/login" variant="secondary" class="af-2fa-back-to-login ms-auto whitespace-nowrap">{{ $t('Back to login') }}</LinkButton>
|
|
61
|
+
</template>
|
|
62
|
+
</TwoFaCard>
|
|
63
|
+
</div>
|
|
70
64
|
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
65
|
</template>
|
|
75
66
|
|
|
76
67
|
|
|
77
68
|
<script setup lang="ts">
|
|
78
69
|
|
|
79
|
-
import { onMounted,
|
|
70
|
+
import { onMounted, ref, computed, onBeforeMount } from 'vue';
|
|
80
71
|
import { useCoreStore } from '@/stores/core';
|
|
81
72
|
import { useUserStore } from '@/stores/user';
|
|
82
|
-
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
73
|
+
import { IconArrowUpRightFromSquareOutline, IconEyeSolid, IconEyeSlashSolid, IconFileCopyOutline } from '@iconify-prerendered/vue-flowbite';
|
|
83
74
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
84
75
|
import { useRouter } from 'vue-router';
|
|
85
76
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
86
|
-
import {
|
|
77
|
+
import { LinkButton } from '@/afcl';
|
|
87
78
|
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
88
|
-
import VOtpInput from "vue3-otp-input";
|
|
89
79
|
import adminforth from '@/adminforth';
|
|
80
|
+
import TwoFaCard from './TwoFaCard.vue';
|
|
81
|
+
import TwoFaCodeInput from './TwoFaCodeInput.vue';
|
|
90
82
|
import { useI18n } from 'vue-i18n';
|
|
91
83
|
import { useRoute } from 'vue-router';
|
|
92
84
|
import { handlePasskeyAlert } from './utils.js';
|
|
@@ -107,16 +99,14 @@ onBeforeMount(() => {
|
|
|
107
99
|
}
|
|
108
100
|
})
|
|
109
101
|
|
|
110
|
-
const
|
|
102
|
+
const codeInput = ref<InstanceType<typeof TwoFaCodeInput> | null>(null);
|
|
111
103
|
const codeError = ref(null);
|
|
112
|
-
const bindValue = ref('');
|
|
113
104
|
const handleOnComplete = (value) => {
|
|
114
105
|
sendCode(value);
|
|
115
106
|
};
|
|
116
107
|
|
|
117
108
|
const router = useRouter();
|
|
118
109
|
const inProgress = ref(false);
|
|
119
|
-
const otpRoot = ref<HTMLElement | null>(null);
|
|
120
110
|
|
|
121
111
|
const coreStore = useCoreStore();
|
|
122
112
|
const user = useUserStore();
|
|
@@ -179,72 +169,8 @@ onMounted(async () => {
|
|
|
179
169
|
if (allowSkipResp?.skipAllowed){
|
|
180
170
|
skipAllowed.value = true;
|
|
181
171
|
}
|
|
182
|
-
|
|
183
|
-
window.addEventListener('paste', handlePaste);
|
|
184
|
-
document.addEventListener('focusin', handleGlobalFocusIn, true);
|
|
185
|
-
await nextTick();
|
|
186
|
-
tagOtpInputs();
|
|
187
|
-
focusFirstAvailableOtpInput();
|
|
188
|
-
const rootEl = otpRoot.value;
|
|
189
|
-
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
onBeforeUnmount(() => {
|
|
193
|
-
window.removeEventListener('paste', handlePaste);
|
|
194
|
-
document.removeEventListener('focusin', handleGlobalFocusIn, true);
|
|
195
|
-
const rootEl = otpRoot.value;
|
|
196
|
-
rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
|
|
197
172
|
});
|
|
198
173
|
|
|
199
|
-
function tagOtpInputs() {
|
|
200
|
-
const root = otpRoot.value;
|
|
201
|
-
if (!root) return;
|
|
202
|
-
const inputs = root.querySelectorAll('input.otp-input');
|
|
203
|
-
inputs.forEach((el, idx) => {
|
|
204
|
-
el.setAttribute('name', 'mfaCode');
|
|
205
|
-
el.setAttribute('id', `mfaCode-${idx + 1}`);
|
|
206
|
-
el.setAttribute('autocomplete', 'one-time-code');
|
|
207
|
-
el.setAttribute('inputmode', 'numeric');
|
|
208
|
-
el.setAttribute('aria-labelledby', 'mfaCode-label');
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function getOtpInputs(): HTMLInputElement[] {
|
|
213
|
-
const root = otpRoot.value;
|
|
214
|
-
if (!root) return [];
|
|
215
|
-
return Array.from(root.querySelectorAll('input.otp-input')) as HTMLInputElement[];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function focusFirstAvailableOtpInput(): void {
|
|
219
|
-
const inputs = getOtpInputs();
|
|
220
|
-
if (!inputs.length) return;
|
|
221
|
-
const firstEmpty = inputs.find((i) => !i.value);
|
|
222
|
-
(firstEmpty || inputs[0]).focus();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function handleGlobalFocusIn(event: FocusEvent): void {
|
|
226
|
-
const inputs = getOtpInputs();
|
|
227
|
-
if (!inputs.length) return;
|
|
228
|
-
const target = event.target as HTMLElement | null;
|
|
229
|
-
if (!target) return;
|
|
230
|
-
if (!inputs.includes(target as HTMLInputElement)) {
|
|
231
|
-
requestAnimationFrame(() => {
|
|
232
|
-
focusFirstAvailableOtpInput();
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function handleFocusOut(): void {
|
|
238
|
-
requestAnimationFrame(() => {
|
|
239
|
-
const inputs = getOtpInputs();
|
|
240
|
-
if (!inputs.length) return;
|
|
241
|
-
const active = document.activeElement as HTMLElement | null;
|
|
242
|
-
if (!active || !inputs.includes(active as HTMLInputElement)) {
|
|
243
|
-
focusFirstAvailableOtpInput();
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
174
|
async function sendCode (value) {
|
|
249
175
|
inProgress.value = true;
|
|
250
176
|
|
|
@@ -264,18 +190,7 @@ async function sendCode (value) {
|
|
|
264
190
|
await user.finishLogin();
|
|
265
191
|
} else {
|
|
266
192
|
codeError.value = resp.error || t('Invalid code');
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
function handlePaste(event) {
|
|
272
|
-
event.preventDefault();
|
|
273
|
-
if (event.target.classList.contains('otp-input')) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const pastedText = event.clipboardData?.getData('text') || '';
|
|
277
|
-
if (pastedText.length === 6) {
|
|
278
|
-
code.value?.fillInput(pastedText);
|
|
193
|
+
codeInput.value!.clear();
|
|
279
194
|
}
|
|
280
195
|
}
|
|
281
196
|
|
|
@@ -299,10 +214,4 @@ const handleSkip = async () => {
|
|
|
299
214
|
codeError.value = 'Something went wrong';
|
|
300
215
|
}
|
|
301
216
|
}
|
|
302
|
-
</script>
|
|
303
|
-
<style scoped>
|
|
304
|
-
:deep(.otp-input-container) {
|
|
305
|
-
display: flex;
|
|
306
|
-
gap: 0.75rem;
|
|
307
|
-
}
|
|
308
|
-
</style>
|
|
217
|
+
</script>
|