@adminforth/two-factors-auth 1.2.2 → 1.4.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 +2 -2
- package/custom/TwoFactorsConfirmation.vue +40 -53
- package/custom/TwoFactorsSetup.vue +28 -11
- package/dist/custom/TwoFactorsConfirmation.vue +40 -53
- package/dist/custom/TwoFactorsSetup.vue +28 -11
- package/dist/index.js +8 -5
- package/index.ts +7 -6
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -10,5 +10,5 @@ custom/package-lock.json
|
|
|
10
10
|
custom/package.json
|
|
11
11
|
custom/tsconfig.json
|
|
12
12
|
|
|
13
|
-
sent 26,
|
|
14
|
-
total size is
|
|
13
|
+
sent 26,506 bytes received 115 bytes 53,242.00 bytes/sec
|
|
14
|
+
total size is 26,077 speedup is 0.98
|
|
@@ -8,28 +8,34 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
11
|
-
<div id="authentication-modal" tabindex="-1" class=" 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">
|
|
11
|
+
<div id="authentication-modal" tabindex="-1" class="af-two-factors-confirmation 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
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-
|
|
17
|
-
<div class="my-4 flex justify-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
|
-
:
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
103
|
-
|
|
90
|
+
await nextTick();
|
|
91
|
+
tagOtpInputs();
|
|
104
92
|
});
|
|
105
93
|
|
|
106
94
|
onBeforeUnmount(() => {
|
|
@@ -161,9 +149,9 @@
|
|
|
161
149
|
}
|
|
162
150
|
|
|
163
151
|
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
11
|
-
<div id="authentication-modal" tabindex="-1" class=" 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">
|
|
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
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
|
|
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
|
|
36
|
+
<div class="my-4 w-full flex justify-center p-2" ref="otpRoot">
|
|
37
37
|
<v-otp-input
|
|
38
38
|
ref="code"
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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,
|
|
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>
|
|
@@ -8,28 +8,34 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
11
|
-
<div id="authentication-modal" tabindex="-1" class=" 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">
|
|
11
|
+
<div id="authentication-modal" tabindex="-1" class="af-two-factors-confirmation 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
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-
|
|
17
|
-
<div class="my-4 flex justify-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
|
-
:
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
103
|
-
|
|
90
|
+
await nextTick();
|
|
91
|
+
tagOtpInputs();
|
|
104
92
|
});
|
|
105
93
|
|
|
106
94
|
onBeforeUnmount(() => {
|
|
@@ -161,9 +149,9 @@
|
|
|
161
149
|
}
|
|
162
150
|
|
|
163
151
|
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
11
|
-
<div id="authentication-modal" tabindex="-1" class=" 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">
|
|
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
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
|
|
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
|
|
36
|
+
<div class="my-4 w-full flex justify-center p-2" ref="otpRoot">
|
|
37
37
|
<v-otp-input
|
|
38
38
|
ref="code"
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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,
|
|
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
|
@@ -49,7 +49,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
49
49
|
}
|
|
50
50
|
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
51
51
|
const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
|
|
52
|
-
beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response }) {
|
|
52
|
+
beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response, extra }) {
|
|
53
|
+
var _b;
|
|
53
54
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName];
|
|
54
55
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
|
|
55
56
|
const brandName = adminforth.config.customization.brandName;
|
|
@@ -57,6 +58,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
57
58
|
const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
|
|
58
59
|
const authPk = authResource.columns.find((col) => col.primaryKey).name;
|
|
59
60
|
const userPk = adminUser.dbUser[authPk];
|
|
61
|
+
const rememberMe = ((_b = extra === null || extra === void 0 ? void 0 : extra.body) === null || _b === void 0 ? void 0 : _b.rememberMe) || false;
|
|
62
|
+
const rememberMeDays = rememberMe ? adminforth.config.auth.rememberMeDays || 30 : 1;
|
|
60
63
|
let newSecret = null;
|
|
61
64
|
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
62
65
|
if (!userNeeds2FA) {
|
|
@@ -68,7 +71,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
68
71
|
newSecret = tempSecret.secret;
|
|
69
72
|
}
|
|
70
73
|
else {
|
|
71
|
-
const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
74
|
+
const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
|
|
72
75
|
response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
|
|
73
76
|
return {
|
|
74
77
|
body: {
|
|
@@ -78,7 +81,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
78
81
|
ok: true
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
84
|
+
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
|
|
82
85
|
response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
|
|
83
86
|
return {
|
|
84
87
|
body: {
|
|
@@ -125,7 +128,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
125
128
|
yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
|
|
126
129
|
}
|
|
127
130
|
this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
|
|
128
|
-
this.adminforth.auth.setAuthCookie({ response, username: decoded.userName, pk: decoded.pk });
|
|
131
|
+
this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
|
|
129
132
|
return { status: 'ok', allowedLogin: true };
|
|
130
133
|
}
|
|
131
134
|
else {
|
|
@@ -140,7 +143,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
140
143
|
const verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
|
|
141
144
|
if (verified) {
|
|
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
|
}
|
|
146
149
|
else {
|
package/index.ts
CHANGED
|
@@ -58,7 +58,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
58
58
|
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
59
59
|
const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
|
|
60
60
|
beforeLoginConfirmationArray.push(
|
|
61
|
-
async({ adminUser, response }: { adminUser: AdminUser, response: IAdminForthHttpResponse} )=> {
|
|
61
|
+
async({ adminUser, response, extra }: { adminUser: AdminUser, response: IAdminForthHttpResponse, extra?: any} )=> {
|
|
62
62
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName]
|
|
63
63
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField]
|
|
64
64
|
const brandName = adminforth.config.customization.brandName;
|
|
@@ -66,6 +66,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
66
66
|
const authResource = adminforth.config.resources.find((res)=>res.resourceId === adminforth.config.auth.usersResourceId )
|
|
67
67
|
const authPk = authResource.columns.find((col)=>col.primaryKey).name
|
|
68
68
|
const userPk = adminUser.dbUser[authPk]
|
|
69
|
+
const rememberMe = extra?.body?.rememberMe || false;
|
|
70
|
+
const rememberMeDays = rememberMe ? adminforth.config.auth.rememberMeDays || 30 : 1;
|
|
69
71
|
let newSecret = null;
|
|
70
72
|
|
|
71
73
|
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
@@ -79,7 +81,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
79
81
|
const tempSecret = twofactor.generateSecret({name: brandName,account: userName})
|
|
80
82
|
newSecret = tempSecret.secret
|
|
81
83
|
} else {
|
|
82
|
-
const value = this.adminforth.auth.issueJWT({userName,
|
|
84
|
+
const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
|
|
83
85
|
response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
|
|
84
86
|
|
|
85
87
|
return {
|
|
@@ -90,7 +92,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
90
92
|
ok: true
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
95
|
+
const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
|
|
94
96
|
response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString() } `);
|
|
95
97
|
|
|
96
98
|
return {
|
|
@@ -127,7 +129,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
127
129
|
const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
|
|
128
130
|
const totpTemporaryJWT = cookies.find((cookie)=>cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)?.value;
|
|
129
131
|
const decoded = await this.adminforth.auth.verify(totpTemporaryJWT, 'tempTotp');
|
|
130
|
-
|
|
131
132
|
if (!decoded)
|
|
132
133
|
return {status:'error',message:'Invalid token'}
|
|
133
134
|
|
|
@@ -140,7 +141,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
140
141
|
await connector.updateRecord({resource:this.authResource, recordId:decoded.pk, newValues:{[this.options.twoFaSecretFieldName]: decoded.newSecret}})
|
|
141
142
|
}
|
|
142
143
|
this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
|
|
143
|
-
this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
|
|
144
|
+
this.adminforth.auth.setAuthCookie({expireInDays: decoded.rememberMeDays, response, username:decoded.userName, pk:decoded.pk})
|
|
144
145
|
return { status: 'ok', allowedLogin: true }
|
|
145
146
|
} else {
|
|
146
147
|
return {error: 'Wrong or expired OTP code'}
|
|
@@ -153,7 +154,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
153
154
|
const verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
|
|
154
155
|
if (verified) {
|
|
155
156
|
this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
|
|
156
|
-
this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
|
|
157
|
+
this.adminforth.auth.setAuthCookie({expireInDays: decoded.rememberMeDays, response, username:decoded.userName, pk:decoded.pk})
|
|
157
158
|
return { status: 'ok', allowedLogin: true }
|
|
158
159
|
} else {
|
|
159
160
|
return {error: 'Wrong or expired OTP code'}
|