@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.
@@ -8,85 +8,77 @@
8
8
  }: {}"
9
9
  >
10
10
 
11
- <div id="authentication-modal" tabindex="-1" class="af-two-factors-setup 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
- <div class="relative p-4 w-full max-w-md max-h-full">
13
- <!-- Modal content -->
14
- <div class="af-login-popup relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" :class="codeError ? 'rounded-b-none' : ''">
15
- <div class="p-10 w-full max-w-md max-h-full custom-auth-wrapper" >
16
- <div>{{$t('Scan this QR code with your authenticator app or open by')}} <a class="text-blue-600" :href="totpUri">{{$t('click')}}</a></div>
17
- <div class="flex justify-center m-3" >
18
- <img v-if="totpQrCode" :src="totpQrCode" class="af-qr-code min-w-[200px] min-h-[200px]" alt="QR code" />
19
- <div v-else class="af-qr-code min-w-[200px] min-h-[200px]"></div>
20
- </div>
21
- <div class="my-1">{{$t('Or copy this code to app manually:')}}</div>
22
- <div class="w-full">
23
- <div class="relative">
24
- <label for="npm-install-copy-text" class="sr-only">{{$t('Label')}}</label>
25
- <input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-12" :value="totp.newSecret" readonly>
26
- <button @click="onCopyClick" data-copy-to-clipboard-target="npm-install-copy-text" class="absolute end-2.5 top-1/2 -translate-y-1/2 text-gray-900 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 rounded-lg py-2 px-2.5 inline-flex items-center justify-center bg-white border-gray-200 border">
27
- <span id="default-message" class="inline-flex items-center">
28
- <svg class="w-3 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
29
- <path d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z"/>
30
- </svg>
31
- </span>
32
- </button>
33
- </div>
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
- <div
61
- v-if="codeError"
62
- class="af-two-factors-confirmation-error relative top-full left-0 bg-red-100 text-red-700 text-sm px-2 py-2 rounded-b-lg shadow"
63
- >
64
- <p class="pl-6">{{ codeError }} </p>
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
- <div v-else class="h-[36px] opacity-0">
67
-
68
- </div>
69
- </div>
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, onBeforeUnmount, nextTick, ref, computed, onBeforeMount } from 'vue';
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 { Button, LinkButton } from '@/afcl';
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 code = ref(null);
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "4.8.0",
3
+ "version": "4.8.2",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",