@adminforth/two-factors-auth 1.3.0 → 1.5.0

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
@@ -4,11 +4,12 @@
4
4
 
5
5
  sending incremental file list
6
6
  custom/
7
+ custom/TwoFaModal.vue
7
8
  custom/TwoFactorsConfirmation.vue
8
9
  custom/TwoFactorsSetup.vue
9
10
  custom/package-lock.json
10
11
  custom/package.json
11
12
  custom/tsconfig.json
12
13
 
13
- sent 26,257 bytes received 115 bytes 52,744.00 bytes/sec
14
- total size is 25,834 speedup is 0.98
14
+ sent 30,935 bytes received 134 bytes 62,138.00 bytes/sec
15
+ total size is 30,449 speedup is 0.98
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 top-0 bottom-0 left-0 right-0"
3
+ v-if ="modelShow">
4
+ <div class="relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
+ <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100">
6
+ {{ $t('Please enter your authenticator code') }}
7
+ </div>
8
+
9
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
10
+ <v-otp-input
11
+ ref="code"
12
+ container-class="grid grid-cols-6 gap-3 w-full"
13
+ input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 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"
14
+ :num-inputs="6"
15
+ inputType="number"
16
+ inputmode="numeric"
17
+ :should-auto-focus="true"
18
+ :should-focus-order="true"
19
+ v-model:value="bindValue"
20
+ @on-complete="handleOnComplete"
21
+ />
22
+ </div>
23
+
24
+ <div class="mt-6 flex justify-center gap-3">
25
+ <button
26
+ class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ @click="onCancel"
28
+ :disabled="inProgress"
29
+ >{{ $t('Cancel') }}</button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import VOtpInput from 'vue3-otp-input';
37
+ import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue';
38
+ import { useUserStore } from '@/stores/user';
39
+ import { useI18n } from 'vue-i18n';
40
+ declare global {
41
+ interface Window {
42
+ adminforthTwoFaModal: {
43
+ getCode: () => Promise<any>;
44
+ };
45
+ }
46
+ }
47
+ const props = defineProps<{
48
+ autoFinishLogin?: boolean
49
+ }>();
50
+ const emit = defineEmits<{
51
+ (e: 'resolved', payload: any): void
52
+ (e: 'rejected', err?: any): void
53
+ (e: 'closed'): void
54
+ }>();
55
+
56
+ const modelShow = ref(false);
57
+ let resolveFn: ((code: string) => void) | null = null;
58
+ let verifyingCallback: ((code: string) => boolean) | null = null;
59
+ let verifyFn: null | ((code: string) => Promise<boolean> | boolean) = null;
60
+
61
+ window.adminforthTwoFaModal = {
62
+ getCode: (verifyingCallback?: () => Promise<boolean>) => new Promise((resolve) => {
63
+ if (modelShow.value) {
64
+ throw new Error('Modal is already open');
65
+ }
66
+ modelShow.value = true;
67
+ resolveFn = resolve;
68
+ verifyFn = verifyingCallback ?? null;
69
+ }),
70
+ };
71
+
72
+ const { t } = useI18n();
73
+ const user = useUserStore();
74
+
75
+ const code = ref<any>(null);
76
+ const otpRoot = ref<HTMLElement | null>(null);
77
+ const bindValue = ref('');
78
+
79
+ function tagOtpInputs() {
80
+ const root = otpRoot.value;
81
+ if (!root) return;
82
+ root.querySelectorAll('input.otp-input').forEach((el, idx) => {
83
+ el.setAttribute('name', 'mfaCode');
84
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
85
+ el.setAttribute('autocomplete', 'one-time-code');
86
+ el.setAttribute('inputmode', 'numeric');
87
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
88
+ });
89
+ }
90
+
91
+ function handlePaste(event: ClipboardEvent) {
92
+ event.preventDefault();
93
+ const pastedText = event.clipboardData?.getData('text') || '';
94
+ if (pastedText.length === 6) {
95
+ code.value?.fillInput(pastedText);
96
+ }
97
+ }
98
+
99
+ async function submit() {
100
+ if (bindValue.value.length !== 6) return;
101
+ await sendCode(bindValue.value);
102
+ }
103
+
104
+ async function handleOnComplete(value: string) {
105
+ await sendCode(value);
106
+ }
107
+
108
+ async function sendCode(value: string) {
109
+ if (!resolveFn) {
110
+ throw new Error('Modal is not initialized properly');
111
+ }
112
+
113
+ if (verifyFn) {
114
+ try {
115
+ const ok = await verifyFn(value);
116
+ if (!ok) {
117
+ return;
118
+ }
119
+ } catch {
120
+ return;
121
+ }
122
+ }
123
+
124
+ modelShow.value = false;
125
+ resolveFn(value);
126
+ }
127
+
128
+
129
+ function onCancel() {
130
+ modelShow.value = false;
131
+ emit('rejected', new Error('cancelled'));
132
+ emit('closed');
133
+ }
134
+
135
+ onMounted(async () => {
136
+ await nextTick();
137
+ tagOtpInputs();
138
+ window.addEventListener('paste', handlePaste as any);
139
+ });
140
+ onBeforeUnmount(() => {
141
+ window.removeEventListener('paste', handlePaste as any);
142
+ });
143
+ </script>
144
+
@@ -12,24 +12,30 @@
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
14
  <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
15
- <div class="p-8 w-full max-w-md max-h-full" >
16
- <div class="m-3">{{$t('Please enter your authenticator code')}} </div>
17
- <div class="my-4 flex justify-center items-center">
15
+ <div class="p-8 w-full max-w-md max-h-full custom-auth-wrapper" >
16
+ <div id="mfaCode-label" class="m-4">{{$t('Please enter your authenticator code')}} </div>
17
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
18
18
  <v-otp-input
19
19
  ref="code"
20
+ container-class="grid grid-cols-6 gap-3 w-full"
20
21
  input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 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"
21
- :conditionalClass="['one', 'two', 'three', 'four', 'five', 'six']"
22
+ :num-inputs="6"
22
23
  inputType="number"
23
24
  inputmode="numeric"
24
- :num-inputs="6"
25
- v-model:value="bindValue"
26
25
  :should-auto-focus="true"
27
26
  :should-focus-order="true"
27
+ v-model:value="bindValue"
28
28
  @on-complete="handleOnComplete"
29
29
  />
30
30
  </div>
31
- <!-- <Vue2FACodeInput v-model="code"/> -->
32
- <LinkButton to="/login" class="w-full">{{$t('Back to login')}}</LinkButton>
31
+ <div class="mt-6 flex justify-center">
32
+ <LinkButton
33
+ to="/login"
34
+ class="w-[290px] mx-4"
35
+ >
36
+ {{$t('Back to login')}}
37
+ </LinkButton>
38
+ </div>
33
39
  </div>
34
40
  </div>
35
41
  </div>
@@ -39,68 +45,50 @@
39
45
 
40
46
 
41
47
  </template>
42
-
43
-
48
+
49
+
44
50
  <script setup>
45
-
46
- import { onMounted, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
51
+
52
+ import { onMounted, nextTick, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
47
53
  import { useCoreStore } from '@/stores/core';
48
54
  import { useUserStore } from '@/stores/user';
49
- import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
50
55
  import { callAdminForthApi, loadFile } from '@/utils';
51
- import { useRouter } from 'vue-router';
52
56
  import { showErrorTost } from '@/composables/useFrontendApi';
53
57
  import { LinkButton } from '@/afcl';
54
- import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
55
58
  import VOtpInput from "vue3-otp-input";
56
59
  import { useI18n } from 'vue-i18n';
57
60
 
58
61
  const { t } = useI18n();
59
62
  const code = ref(null);
63
+ const otpRoot = ref(null);
64
+ const bindValue = ref('');
60
65
 
61
66
  const handleOnComplete = (value) => {
62
67
  sendCode(value);
63
68
  };
64
69
 
65
- const fillInput = (value) => {
66
- code.value?.fillInput(value);
67
- };
70
+ function tagOtpInputs() {
71
+ const root = otpRoot.value;
72
+ if (!root) return;
73
+ const inputs = root.querySelectorAll('input.otp-input');
74
+ inputs.forEach((el, idx) => {
75
+ el.setAttribute('name', 'mfaCode');
76
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
77
+ el.setAttribute('autocomplete', 'one-time-code');
78
+ el.setAttribute('inputmode', 'numeric');
79
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
80
+ });
81
+ }
68
82
 
69
- const router = useRouter();
70
83
  const inProgress = ref(false);
71
-
84
+
72
85
  const coreStore = useCoreStore();
73
86
  const user = useUserStore();
74
87
 
75
-
76
- // use this simple function to automatically focus on the next input
77
-
78
-
79
-
80
- const showPw = ref(false);
81
-
82
- const error = ref(null);
83
- const totp = ref({});
84
- const totpJWT = ref(null);
85
-
86
-
87
- function parseJwt(token) {
88
- // Split the token into its parts
89
- const base64Url = token.split('.')[1];
90
-
91
- // Base64-decode the payload
92
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
93
- const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
94
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
95
- }).join(''));
96
-
97
- // Parse the JSON payload
98
- return JSON.parse(jsonPayload);
99
- }
100
-
88
+
101
89
  onMounted(async () => {
102
- coreStore.getPublicConfig();
103
- window.addEventListener('paste', handlePaste);
90
+ await nextTick();
91
+ tagOtpInputs();
104
92
  });
105
93
 
106
94
  onBeforeUnmount(() => {
@@ -161,9 +149,9 @@
161
149
  }
162
150
 
163
151
  /**
164
- * This particular piece of code makes the last input have a gap in the middle.
165
- */
166
- .spaced-code-input {
152
+ * This particular piece of code makes the last input have a gap in the middle.
153
+ */
154
+ .spaced-code-input {
167
155
  & .vue3-2fa-code-input-box {
168
156
  &:nth-child(3) {
169
157
  @apply mr-4;
@@ -175,4 +163,3 @@
175
163
  }
176
164
  }
177
165
  </style>
178
-
@@ -12,14 +12,14 @@
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
14
  <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
15
- <div class="p-10 w-full max-w-md max-h-full" >
15
+ <div class="p-10 w-full max-w-md max-h-full custom-auth-wrapper" >
16
16
  <div class="m-3" >{{$t('Scan this QR code with your authenticator app or open by')}} <a class="text-blue-600" :href="totpUri">{{$t('click')}}</a></div>
17
17
  <div class="flex justify-center m-3" >
18
18
  <img :src="totpQrCode" class="min-w-[200px], min-h-[200px]" alt="QR code" />
19
19
  </div>
20
20
  <div class="m-3 ">
21
21
  <div class="m-1">{{$t('Or copy this code to app manually:')}}</div>
22
- <div class="w-full max-w-[46rem]">
22
+ <div class="w-full">
23
23
  <div class="relative">
24
24
  <label for="npm-install-copy-text" class="sr-only">{{$t('Label')}}</label>
25
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>
@@ -33,21 +33,22 @@
33
33
  </div>
34
34
  </div>
35
35
  </div>
36
- <div class="my-4 flex justify-center items-center">
36
+ <div class="my-4 w-full flex justify-center p-2" ref="otpRoot">
37
37
  <v-otp-input
38
38
  ref="code"
39
- input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-11 h-11 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
- :conditionalClass="['one', 'two', 'three', 'four', 'five', 'six']"
41
- inputType="number"
42
- inputmode="numeric"
39
+ container-class="grid grid-cols-6 gap-3 w-full"
40
+ 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-[43.33px] 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"
43
41
  :num-inputs="6"
42
+ inputType="number"
43
+ inputmode="numeric"
44
44
  :should-auto-focus="true"
45
45
  :should-focus-order="true"
46
+ v-model:value="bindValue"
46
47
  @on-complete="handleOnComplete"
47
48
  />
48
49
  </div>
49
50
  <!-- <Vue2FACodeInput v-model="code" autofocus /> -->
50
- <div class="flex flex-row gap-2.5 pl-3 pr-3 h-12">
51
+ <div class="flex flex-row gap-2.5 px-3 h-12">
51
52
  <LinkButton to="/login" class="w-full">
52
53
  {{$t('Back to login')}}
53
54
  </LinkButton>
@@ -67,7 +68,7 @@
67
68
 
68
69
  <script setup lang="ts">
69
70
 
70
- import { onMounted, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
71
+ import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch } from 'vue';
71
72
  import { useCoreStore } from '@/stores/core';
72
73
  import { useUserStore } from '@/stores/user';
73
74
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
@@ -89,6 +90,7 @@ const handleOnComplete = (value) => {
89
90
 
90
91
  const router = useRouter();
91
92
  const inProgress = ref(false);
93
+ const otpRoot = ref(null);
92
94
 
93
95
  const coreStore = useCoreStore();
94
96
  const user = useUserStore();
@@ -151,12 +153,27 @@ onMounted(async () => {
151
153
  }
152
154
 
153
155
  window.addEventListener('paste', handlePaste);
156
+ await nextTick();
157
+ tagOtpInputs();
154
158
  });
155
159
 
156
160
  onBeforeUnmount(() => {
157
161
  window.removeEventListener('paste', handlePaste);
158
162
  });
159
163
 
164
+ function tagOtpInputs() {
165
+ const root = otpRoot.value;
166
+ if (!root) return;
167
+ const inputs = root.querySelectorAll('input.otp-input');
168
+ inputs.forEach((el, idx) => {
169
+ el.setAttribute('name', 'mfaCode');
170
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
171
+ el.setAttribute('autocomplete', 'one-time-code');
172
+ el.setAttribute('inputmode', 'numeric');
173
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
174
+ });
175
+ }
176
+
160
177
  async function sendCode (value) {
161
178
  inProgress.value = true;
162
179
 
@@ -206,5 +223,5 @@ const handleSkip = async () => {
206
223
  <style>
207
224
  .otp-input {
208
225
  margin: 0 5px;
209
- }
226
+ }
210
227
  </style>
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 top-0 bottom-0 left-0 right-0"
3
+ v-if ="modelShow">
4
+ <div class="relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
+ <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100">
6
+ {{ $t('Please enter your authenticator code') }}
7
+ </div>
8
+
9
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
10
+ <v-otp-input
11
+ ref="code"
12
+ container-class="grid grid-cols-6 gap-3 w-full"
13
+ input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 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"
14
+ :num-inputs="6"
15
+ inputType="number"
16
+ inputmode="numeric"
17
+ :should-auto-focus="true"
18
+ :should-focus-order="true"
19
+ v-model:value="bindValue"
20
+ @on-complete="handleOnComplete"
21
+ />
22
+ </div>
23
+
24
+ <div class="mt-6 flex justify-center gap-3">
25
+ <button
26
+ class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ @click="onCancel"
28
+ :disabled="inProgress"
29
+ >{{ $t('Cancel') }}</button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import VOtpInput from 'vue3-otp-input';
37
+ import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue';
38
+ import { useUserStore } from '@/stores/user';
39
+ import { useI18n } from 'vue-i18n';
40
+ declare global {
41
+ interface Window {
42
+ adminforthTwoFaModal: {
43
+ getCode: () => Promise<any>;
44
+ };
45
+ }
46
+ }
47
+ const props = defineProps<{
48
+ autoFinishLogin?: boolean
49
+ }>();
50
+ const emit = defineEmits<{
51
+ (e: 'resolved', payload: any): void
52
+ (e: 'rejected', err?: any): void
53
+ (e: 'closed'): void
54
+ }>();
55
+
56
+ const modelShow = ref(false);
57
+ let resolveFn: ((code: string) => void) | null = null;
58
+ let verifyingCallback: ((code: string) => boolean) | null = null;
59
+ let verifyFn: null | ((code: string) => Promise<boolean> | boolean) = null;
60
+
61
+ window.adminforthTwoFaModal = {
62
+ getCode: (verifyingCallback?: () => Promise<boolean>) => new Promise((resolve) => {
63
+ if (modelShow.value) {
64
+ throw new Error('Modal is already open');
65
+ }
66
+ modelShow.value = true;
67
+ resolveFn = resolve;
68
+ verifyFn = verifyingCallback ?? null;
69
+ }),
70
+ };
71
+
72
+ const { t } = useI18n();
73
+ const user = useUserStore();
74
+
75
+ const code = ref<any>(null);
76
+ const otpRoot = ref<HTMLElement | null>(null);
77
+ const bindValue = ref('');
78
+
79
+ function tagOtpInputs() {
80
+ const root = otpRoot.value;
81
+ if (!root) return;
82
+ root.querySelectorAll('input.otp-input').forEach((el, idx) => {
83
+ el.setAttribute('name', 'mfaCode');
84
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
85
+ el.setAttribute('autocomplete', 'one-time-code');
86
+ el.setAttribute('inputmode', 'numeric');
87
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
88
+ });
89
+ }
90
+
91
+ function handlePaste(event: ClipboardEvent) {
92
+ event.preventDefault();
93
+ const pastedText = event.clipboardData?.getData('text') || '';
94
+ if (pastedText.length === 6) {
95
+ code.value?.fillInput(pastedText);
96
+ }
97
+ }
98
+
99
+ async function submit() {
100
+ if (bindValue.value.length !== 6) return;
101
+ await sendCode(bindValue.value);
102
+ }
103
+
104
+ async function handleOnComplete(value: string) {
105
+ await sendCode(value);
106
+ }
107
+
108
+ async function sendCode(value: string) {
109
+ if (!resolveFn) {
110
+ throw new Error('Modal is not initialized properly');
111
+ }
112
+
113
+ if (verifyFn) {
114
+ try {
115
+ const ok = await verifyFn(value);
116
+ if (!ok) {
117
+ return;
118
+ }
119
+ } catch {
120
+ return;
121
+ }
122
+ }
123
+
124
+ modelShow.value = false;
125
+ resolveFn(value);
126
+ }
127
+
128
+
129
+ function onCancel() {
130
+ modelShow.value = false;
131
+ emit('rejected', new Error('cancelled'));
132
+ emit('closed');
133
+ }
134
+
135
+ onMounted(async () => {
136
+ await nextTick();
137
+ tagOtpInputs();
138
+ window.addEventListener('paste', handlePaste as any);
139
+ });
140
+ onBeforeUnmount(() => {
141
+ window.removeEventListener('paste', handlePaste as any);
142
+ });
143
+ </script>
144
+
@@ -12,24 +12,30 @@
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
14
  <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
15
- <div class="p-8 w-full max-w-md max-h-full" >
16
- <div class="m-3">{{$t('Please enter your authenticator code')}} </div>
17
- <div class="my-4 flex justify-center items-center">
15
+ <div class="p-8 w-full max-w-md max-h-full custom-auth-wrapper" >
16
+ <div id="mfaCode-label" class="m-4">{{$t('Please enter your authenticator code')}} </div>
17
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
18
18
  <v-otp-input
19
19
  ref="code"
20
+ container-class="grid grid-cols-6 gap-3 w-full"
20
21
  input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 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"
21
- :conditionalClass="['one', 'two', 'three', 'four', 'five', 'six']"
22
+ :num-inputs="6"
22
23
  inputType="number"
23
24
  inputmode="numeric"
24
- :num-inputs="6"
25
- v-model:value="bindValue"
26
25
  :should-auto-focus="true"
27
26
  :should-focus-order="true"
27
+ v-model:value="bindValue"
28
28
  @on-complete="handleOnComplete"
29
29
  />
30
30
  </div>
31
- <!-- <Vue2FACodeInput v-model="code"/> -->
32
- <LinkButton to="/login" class="w-full">{{$t('Back to login')}}</LinkButton>
31
+ <div class="mt-6 flex justify-center">
32
+ <LinkButton
33
+ to="/login"
34
+ class="w-[290px] mx-4"
35
+ >
36
+ {{$t('Back to login')}}
37
+ </LinkButton>
38
+ </div>
33
39
  </div>
34
40
  </div>
35
41
  </div>
@@ -39,68 +45,50 @@
39
45
 
40
46
 
41
47
  </template>
42
-
43
-
48
+
49
+
44
50
  <script setup>
45
-
46
- import { onMounted, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
51
+
52
+ import { onMounted, nextTick, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
47
53
  import { useCoreStore } from '@/stores/core';
48
54
  import { useUserStore } from '@/stores/user';
49
- import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
50
55
  import { callAdminForthApi, loadFile } from '@/utils';
51
- import { useRouter } from 'vue-router';
52
56
  import { showErrorTost } from '@/composables/useFrontendApi';
53
57
  import { LinkButton } from '@/afcl';
54
- import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
55
58
  import VOtpInput from "vue3-otp-input";
56
59
  import { useI18n } from 'vue-i18n';
57
60
 
58
61
  const { t } = useI18n();
59
62
  const code = ref(null);
63
+ const otpRoot = ref(null);
64
+ const bindValue = ref('');
60
65
 
61
66
  const handleOnComplete = (value) => {
62
67
  sendCode(value);
63
68
  };
64
69
 
65
- const fillInput = (value) => {
66
- code.value?.fillInput(value);
67
- };
70
+ function tagOtpInputs() {
71
+ const root = otpRoot.value;
72
+ if (!root) return;
73
+ const inputs = root.querySelectorAll('input.otp-input');
74
+ inputs.forEach((el, idx) => {
75
+ el.setAttribute('name', 'mfaCode');
76
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
77
+ el.setAttribute('autocomplete', 'one-time-code');
78
+ el.setAttribute('inputmode', 'numeric');
79
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
80
+ });
81
+ }
68
82
 
69
- const router = useRouter();
70
83
  const inProgress = ref(false);
71
-
84
+
72
85
  const coreStore = useCoreStore();
73
86
  const user = useUserStore();
74
87
 
75
-
76
- // use this simple function to automatically focus on the next input
77
-
78
-
79
-
80
- const showPw = ref(false);
81
-
82
- const error = ref(null);
83
- const totp = ref({});
84
- const totpJWT = ref(null);
85
-
86
-
87
- function parseJwt(token) {
88
- // Split the token into its parts
89
- const base64Url = token.split('.')[1];
90
-
91
- // Base64-decode the payload
92
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
93
- const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
94
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
95
- }).join(''));
96
-
97
- // Parse the JSON payload
98
- return JSON.parse(jsonPayload);
99
- }
100
-
88
+
101
89
  onMounted(async () => {
102
- coreStore.getPublicConfig();
103
- window.addEventListener('paste', handlePaste);
90
+ await nextTick();
91
+ tagOtpInputs();
104
92
  });
105
93
 
106
94
  onBeforeUnmount(() => {
@@ -161,9 +149,9 @@
161
149
  }
162
150
 
163
151
  /**
164
- * This particular piece of code makes the last input have a gap in the middle.
165
- */
166
- .spaced-code-input {
152
+ * This particular piece of code makes the last input have a gap in the middle.
153
+ */
154
+ .spaced-code-input {
167
155
  & .vue3-2fa-code-input-box {
168
156
  &:nth-child(3) {
169
157
  @apply mr-4;
@@ -175,4 +163,3 @@
175
163
  }
176
164
  }
177
165
  </style>
178
-
@@ -12,14 +12,14 @@
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
14
  <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
15
- <div class="p-10 w-full max-w-md max-h-full" >
15
+ <div class="p-10 w-full max-w-md max-h-full custom-auth-wrapper" >
16
16
  <div class="m-3" >{{$t('Scan this QR code with your authenticator app or open by')}} <a class="text-blue-600" :href="totpUri">{{$t('click')}}</a></div>
17
17
  <div class="flex justify-center m-3" >
18
18
  <img :src="totpQrCode" class="min-w-[200px], min-h-[200px]" alt="QR code" />
19
19
  </div>
20
20
  <div class="m-3 ">
21
21
  <div class="m-1">{{$t('Or copy this code to app manually:')}}</div>
22
- <div class="w-full max-w-[46rem]">
22
+ <div class="w-full">
23
23
  <div class="relative">
24
24
  <label for="npm-install-copy-text" class="sr-only">{{$t('Label')}}</label>
25
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>
@@ -33,21 +33,22 @@
33
33
  </div>
34
34
  </div>
35
35
  </div>
36
- <div class="my-4 flex justify-center items-center">
36
+ <div class="my-4 w-full flex justify-center p-2" ref="otpRoot">
37
37
  <v-otp-input
38
38
  ref="code"
39
- input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-11 h-11 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
- :conditionalClass="['one', 'two', 'three', 'four', 'five', 'six']"
41
- inputType="number"
42
- inputmode="numeric"
39
+ container-class="grid grid-cols-6 gap-3 w-full"
40
+ 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-[43.33px] 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"
43
41
  :num-inputs="6"
42
+ inputType="number"
43
+ inputmode="numeric"
44
44
  :should-auto-focus="true"
45
45
  :should-focus-order="true"
46
+ v-model:value="bindValue"
46
47
  @on-complete="handleOnComplete"
47
48
  />
48
49
  </div>
49
50
  <!-- <Vue2FACodeInput v-model="code" autofocus /> -->
50
- <div class="flex flex-row gap-2.5 pl-3 pr-3 h-12">
51
+ <div class="flex flex-row gap-2.5 px-3 h-12">
51
52
  <LinkButton to="/login" class="w-full">
52
53
  {{$t('Back to login')}}
53
54
  </LinkButton>
@@ -67,7 +68,7 @@
67
68
 
68
69
  <script setup lang="ts">
69
70
 
70
- import { onMounted, onBeforeUnmount, ref, watchEffect,computed,watch } from 'vue';
71
+ import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch } from 'vue';
71
72
  import { useCoreStore } from '@/stores/core';
72
73
  import { useUserStore } from '@/stores/user';
73
74
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
@@ -89,6 +90,7 @@ const handleOnComplete = (value) => {
89
90
 
90
91
  const router = useRouter();
91
92
  const inProgress = ref(false);
93
+ const otpRoot = ref(null);
92
94
 
93
95
  const coreStore = useCoreStore();
94
96
  const user = useUserStore();
@@ -151,12 +153,27 @@ onMounted(async () => {
151
153
  }
152
154
 
153
155
  window.addEventListener('paste', handlePaste);
156
+ await nextTick();
157
+ tagOtpInputs();
154
158
  });
155
159
 
156
160
  onBeforeUnmount(() => {
157
161
  window.removeEventListener('paste', handlePaste);
158
162
  });
159
163
 
164
+ function tagOtpInputs() {
165
+ const root = otpRoot.value;
166
+ if (!root) return;
167
+ const inputs = root.querySelectorAll('input.otp-input');
168
+ inputs.forEach((el, idx) => {
169
+ el.setAttribute('name', 'mfaCode');
170
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
171
+ el.setAttribute('autocomplete', 'one-time-code');
172
+ el.setAttribute('inputmode', 'numeric');
173
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
174
+ });
175
+ }
176
+
160
177
  async function sendCode (value) {
161
178
  inProgress.value = true;
162
179
 
@@ -206,5 +223,5 @@ const handleSkip = async () => {
206
223
  <style>
207
224
  .otp-input {
208
225
  margin: 0 5px;
209
- }
226
+ }
210
227
  </style>
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
30
30
  path: '/setup2fa',
31
31
  component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true } }
32
32
  });
33
+ const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || [];
34
+ everyPageBottomInjections.push({ file: this.componentPath('TwoFaModal.vue'), meta: {} });
33
35
  this.activate(resourceConfig, adminforth);
34
36
  }
35
37
  activate(resourceConfig, adminforth) {
@@ -49,7 +51,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
49
51
  }
50
52
  const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
51
53
  const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
52
- beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response }) {
54
+ beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response, extra }) {
55
+ var _b;
53
56
  const secret = adminUser.dbUser[this.options.twoFaSecretFieldName];
54
57
  const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
55
58
  const brandName = adminforth.config.customization.brandName;
@@ -57,6 +60,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
57
60
  const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
58
61
  const authPk = authResource.columns.find((col) => col.primaryKey).name;
59
62
  const userPk = adminUser.dbUser[authPk];
63
+ const rememberMe = ((_b = extra === null || extra === void 0 ? void 0 : extra.body) === null || _b === void 0 ? void 0 : _b.rememberMe) || false;
64
+ const rememberMeDays = rememberMe ? adminforth.config.auth.rememberMeDays || 30 : 1;
60
65
  let newSecret = null;
61
66
  const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
62
67
  if (!userNeeds2FA) {
@@ -68,7 +73,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
68
73
  newSecret = tempSecret.secret;
69
74
  }
70
75
  else {
71
- const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
76
+ const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
72
77
  response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
73
78
  return {
74
79
  body: {
@@ -78,7 +83,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
78
83
  ok: true
79
84
  };
80
85
  }
81
- const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
86
+ const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
82
87
  response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
83
88
  return {
84
89
  body: {
@@ -125,7 +130,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
125
130
  yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
126
131
  }
127
132
  this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
128
- this.adminforth.auth.setAuthCookie({ response, username: decoded.userName, pk: decoded.pk });
133
+ this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
129
134
  return { status: 'ok', allowedLogin: true };
130
135
  }
131
136
  else {
@@ -140,7 +145,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
140
145
  const verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
141
146
  if (verified) {
142
147
  this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
143
- this.adminforth.auth.setAuthCookie({ response, username: decoded.userName, pk: decoded.pk });
148
+ this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
144
149
  return { status: 'ok', allowedLogin: true };
145
150
  }
146
151
  else {
@@ -172,5 +177,24 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
172
177
  }
173
178
  }),
174
179
  });
180
+ server.endpoint({
181
+ method: 'POST',
182
+ path: `/plugin/twofa/verify`,
183
+ noAuth: false,
184
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, body }) {
185
+ if (!(body === null || body === void 0 ? void 0 : body.code))
186
+ return { error: 'Code is required' };
187
+ const authRes = this.adminforth.config.resources
188
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
189
+ const connector = this.adminforth.connectors[authRes.dataSource];
190
+ const pkName = authRes.columns.find(c => c.primaryKey).name;
191
+ const user = yield connector.getRecordByPrimaryKey(authRes, adminUser.dbUser[pkName]);
192
+ const secret = user[this.options.twoFaSecretFieldName];
193
+ if (!secret)
194
+ return { error: '2FA is not set up for this user' };
195
+ const verified = twofactor.verifyToken(secret, body.code, this.options.timeStepWindow);
196
+ return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
197
+ })
198
+ });
175
199
  }
176
200
  }
package/index.ts CHANGED
@@ -33,6 +33,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
33
33
  path:'/setup2fa',
34
34
  component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true }}
35
35
  })
36
+ const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || []
37
+ everyPageBottomInjections.push({ file: this.componentPath('TwoFaModal.vue'), meta: {} })
36
38
  this.activate( resourceConfig, adminforth )
37
39
  }
38
40
 
@@ -58,7 +60,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
58
60
  const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
59
61
  const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
60
62
  beforeLoginConfirmationArray.push(
61
- async({ adminUser, response }: { adminUser: AdminUser, response: IAdminForthHttpResponse} )=> {
63
+ async({ adminUser, response, extra }: { adminUser: AdminUser, response: IAdminForthHttpResponse, extra?: any} )=> {
62
64
  const secret = adminUser.dbUser[this.options.twoFaSecretFieldName]
63
65
  const userName = adminUser.dbUser[adminforth.config.auth.usernameField]
64
66
  const brandName = adminforth.config.customization.brandName;
@@ -66,6 +68,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
66
68
  const authResource = adminforth.config.resources.find((res)=>res.resourceId === adminforth.config.auth.usersResourceId )
67
69
  const authPk = authResource.columns.find((col)=>col.primaryKey).name
68
70
  const userPk = adminUser.dbUser[authPk]
71
+ const rememberMe = extra?.body?.rememberMe || false;
72
+ const rememberMeDays = rememberMe ? adminforth.config.auth.rememberMeDays || 30 : 1;
69
73
  let newSecret = null;
70
74
 
71
75
  const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
@@ -79,7 +83,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
79
83
  const tempSecret = twofactor.generateSecret({name: brandName,account: userName})
80
84
  newSecret = tempSecret.secret
81
85
  } else {
82
- const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk, userCanSkipSetup }, 'tempTotp', '2h');
86
+ const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
83
87
  response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
84
88
 
85
89
  return {
@@ -90,7 +94,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
90
94
  ok: true
91
95
  }
92
96
  }
93
- const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup }, 'tempTotp', '2h');
97
+ const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
94
98
  response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString() } `);
95
99
 
96
100
  return {
@@ -127,7 +131,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
127
131
  const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
128
132
  const totpTemporaryJWT = cookies.find((cookie)=>cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)?.value;
129
133
  const decoded = await this.adminforth.auth.verify(totpTemporaryJWT, 'tempTotp');
130
-
131
134
  if (!decoded)
132
135
  return {status:'error',message:'Invalid token'}
133
136
 
@@ -140,7 +143,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
140
143
  await connector.updateRecord({resource:this.authResource, recordId:decoded.pk, newValues:{[this.options.twoFaSecretFieldName]: decoded.newSecret}})
141
144
  }
142
145
  this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
143
- this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
146
+ this.adminforth.auth.setAuthCookie({expireInDays: decoded.rememberMeDays, response, username:decoded.userName, pk:decoded.pk})
144
147
  return { status: 'ok', allowedLogin: true }
145
148
  } else {
146
149
  return {error: 'Wrong or expired OTP code'}
@@ -153,7 +156,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
153
156
  const verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
154
157
  if (verified) {
155
158
  this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
156
- this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
159
+ this.adminforth.auth.setAuthCookie({expireInDays: decoded.rememberMeDays, response, username:decoded.userName, pk:decoded.pk})
157
160
  return { status: 'ok', allowedLogin: true }
158
161
  } else {
159
162
  return {error: 'Wrong or expired OTP code'}
@@ -187,5 +190,25 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
187
190
  }
188
191
  },
189
192
  });
193
+ server.endpoint({
194
+ method: 'POST',
195
+ path: `/plugin/twofa/verify`,
196
+ noAuth: false,
197
+ handler: async ({ adminUser, body }) => {
198
+ if (!body?.code) return { error: 'Code is required' };
199
+
200
+ const authRes = this.adminforth.config.resources
201
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
202
+ const connector = this.adminforth.connectors[authRes.dataSource];
203
+ const pkName = authRes.columns.find(c => c.primaryKey).name;
204
+ const user = await connector.getRecordByPrimaryKey(authRes, adminUser.dbUser[pkName]);
205
+
206
+ const secret = user[this.options.twoFaSecretFieldName];
207
+ if (!secret) return { error: '2FA is not set up for this user' };
208
+
209
+ const verified = twofactor.verifyToken(secret, body.code, this.options.timeStepWindow);
210
+ return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
211
+ }
212
+ });
190
213
  }
191
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",