@adminforth/two-factors-auth 1.0.15 → 1.0.17
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/custom/TwoFactorsConfirmation.vue +48 -63
- package/custom/TwoFactorsSetup.vue +20 -59
- package/custom/package-lock.json +21 -0
- package/custom/package.json +15 -0
- package/dist/custom/TwoFactorsConfirmation.vue +48 -63
- package/dist/custom/TwoFactorsSetup.vue +20 -59
- package/dist/custom/package-lock.json +21 -0
- package/dist/custom/package.json +15 -0
- package/dist/index.js +8 -4
- package/index.ts +12 -7
- package/package.json +3 -2
- package/types.ts +10 -0
|
@@ -14,38 +14,10 @@
|
|
|
14
14
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
|
|
15
15
|
<div class="p-8 w-full max-w-md max-h-full" >
|
|
16
16
|
<div class="m-3" >Please enter your authenticator code </div>
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<label for="code-1" class="sr-only">First code</label>
|
|
22
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-next="code-2" id="code-1" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
23
|
-
</div>
|
|
24
|
-
<div>
|
|
25
|
-
<label for="code-2" class="sr-only">Second code</label>
|
|
26
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-1" data-focus-input-next="code-3" id="code-2" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
27
|
-
</div>
|
|
28
|
-
<div>
|
|
29
|
-
<label for="code-3" class="sr-only">Third code</label>
|
|
30
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-2" data-focus-input-next="code-4" id="code-3" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
31
|
-
</div>
|
|
32
|
-
<div>
|
|
33
|
-
<label for="code-4" class="sr-only">Fourth code</label>
|
|
34
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-3" data-focus-input-next="code-5" id="code-4" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
35
|
-
</div>
|
|
36
|
-
<div>
|
|
37
|
-
<label for="code-5" class="sr-only">Fifth code</label>
|
|
38
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-4" data-focus-input-next="code-6" id="code-5" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
39
|
-
</div>
|
|
40
|
-
<div>
|
|
41
|
-
<label for="code-6" class="sr-only">Sixth code</label>
|
|
42
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-5" id="code-6" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
<p id="helper-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400"></p>
|
|
46
|
-
</form>
|
|
47
|
-
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
|
48
|
-
>Cancel</button>
|
|
17
|
+
<Vue2FACodeInput v-model="code"/>
|
|
18
|
+
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
|
19
|
+
Cancel
|
|
20
|
+
</button>
|
|
49
21
|
</div>
|
|
50
22
|
</div>
|
|
51
23
|
</div>
|
|
@@ -59,7 +31,7 @@
|
|
|
59
31
|
|
|
60
32
|
<script setup>
|
|
61
33
|
|
|
62
|
-
import { onMounted, ref, watchEffect,computed } from 'vue';
|
|
34
|
+
import { onMounted, ref, watchEffect,computed,watch } from 'vue';
|
|
63
35
|
import { useCoreStore } from '@/stores/core';
|
|
64
36
|
import { useUserStore } from '@/stores/user';
|
|
65
37
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
@@ -67,14 +39,15 @@
|
|
|
67
39
|
import { useRouter } from 'vue-router';
|
|
68
40
|
import { initFlowbite } from 'flowbite'
|
|
69
41
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
42
|
+
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
43
|
+
|
|
44
|
+
const code = ref(null);
|
|
73
45
|
const router = useRouter();
|
|
74
46
|
const inProgress = ref(false);
|
|
75
47
|
|
|
76
48
|
const coreStore = useCoreStore();
|
|
77
49
|
const user = useUserStore();
|
|
50
|
+
|
|
78
51
|
|
|
79
52
|
// use this simple function to automatically focus on the next input
|
|
80
53
|
|
|
@@ -103,41 +76,15 @@
|
|
|
103
76
|
|
|
104
77
|
onMounted(async () => {
|
|
105
78
|
coreStore.getPublicConfig()
|
|
106
|
-
|
|
107
|
-
function focusNextInput(el, prevId, nextId) {
|
|
108
|
-
if (el.value.length === 0) {
|
|
109
|
-
if (prevId) {
|
|
110
|
-
document.getElementById(prevId).focus();
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
if (nextId) {
|
|
114
|
-
document.getElementById(nextId).focus();
|
|
115
|
-
} else {
|
|
116
|
-
sendCode()
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
document.querySelectorAll('[data-focus-input-init]').forEach(function(element) {
|
|
122
|
-
element.addEventListener('keyup', function() {
|
|
123
|
-
const prevId = this.getAttribute('data-focus-input-prev');
|
|
124
|
-
const nextId = this.getAttribute('data-focus-input-next');
|
|
125
|
-
focusNextInput(this, prevId, nextId);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
79
|
});
|
|
132
80
|
|
|
133
81
|
async function sendCode () {
|
|
134
82
|
inProgress.value = true;
|
|
135
|
-
const code = document.getElementById('code-1').value + document.getElementById('code-2').value + document.getElementById('code-3').value + document.getElementById('code-4').value + document.getElementById('code-5').value + document.getElementById('code-6').value;
|
|
136
83
|
const resp = await callAdminForthApi({
|
|
137
84
|
method: 'POST',
|
|
138
85
|
path: '/plugin/twofa/confirmSetup',
|
|
139
86
|
body: {
|
|
140
|
-
code: code,
|
|
87
|
+
code: code.value,
|
|
141
88
|
secret: null,
|
|
142
89
|
}
|
|
143
90
|
})
|
|
@@ -147,5 +94,43 @@
|
|
|
147
94
|
showErrorTost('Invalid code');
|
|
148
95
|
}
|
|
149
96
|
}
|
|
97
|
+
|
|
98
|
+
watch(code, async (nv)=>{
|
|
99
|
+
if (nv){
|
|
100
|
+
sendCode();
|
|
101
|
+
}
|
|
102
|
+
})
|
|
150
103
|
</script>
|
|
104
|
+
|
|
105
|
+
<style lang='scss'>
|
|
106
|
+
.vue3-2fa-code-input {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
gap: 1rem;
|
|
110
|
+
margin-bottom: 1rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.vue3-2fa-code-input-box {
|
|
114
|
+
&[type='text'] {
|
|
115
|
+
@apply w-10 h-10 text-center text-xl border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500;
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* This particular piece of code makes the last input have a gap in the middle.
|
|
123
|
+
*/
|
|
124
|
+
.spaced-code-input {
|
|
125
|
+
& .vue3-2fa-code-input-box {
|
|
126
|
+
&:nth-child(3) {
|
|
127
|
+
@apply mr-4;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&:nth-child(4) {
|
|
131
|
+
@apply ml-4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
151
136
|
|
|
@@ -28,47 +28,19 @@
|
|
|
28
28
|
<svg class="w-3 h-3 me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
29
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
30
|
</svg>
|
|
31
|
-
<span class="text-xs font-semibold">Copy</span>
|
|
31
|
+
<span @click="onCopyClick" class="text-xs font-semibold">Copy</span>
|
|
32
32
|
</span>
|
|
33
33
|
<span id="success-message" class="hidden inline-flex items-center">
|
|
34
|
-
<svg class="w-3 h-3 text-
|
|
34
|
+
<svg class="w-3 h-3 text-lightPrimary dark:text-darkPrimary me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
|
|
35
35
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
|
|
36
36
|
</svg>
|
|
37
|
-
<span class="text-xs font-semibold text-
|
|
37
|
+
<span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
|
|
38
38
|
</span>
|
|
39
39
|
</button>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
43
|
-
<
|
|
44
|
-
<div class="flex mb-2 space-x-2 rtl:space-x-reverse ">
|
|
45
|
-
<div>
|
|
46
|
-
<label for="code-1" class="sr-only">First code</label>
|
|
47
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-next="code-2" id="code-1" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<label for="code-2" class="sr-only">Second code</label>
|
|
51
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-1" data-focus-input-next="code-3" id="code-2" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
52
|
-
</div>
|
|
53
|
-
<div>
|
|
54
|
-
<label for="code-3" class="sr-only">Third code</label>
|
|
55
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-2" data-focus-input-next="code-4" id="code-3" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
56
|
-
</div>
|
|
57
|
-
<div>
|
|
58
|
-
<label for="code-4" class="sr-only">Fourth code</label>
|
|
59
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-3" data-focus-input-next="code-5" id="code-4" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
60
|
-
</div>
|
|
61
|
-
<div>
|
|
62
|
-
<label for="code-5" class="sr-only">Fifth code</label>
|
|
63
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-4" data-focus-input-next="code-6" id="code-5" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
64
|
-
</div>
|
|
65
|
-
<div>
|
|
66
|
-
<label for="code-6" class="sr-only">Sixth code</label>
|
|
67
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-5" id="code-6" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
<p id="helper-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400"></p>
|
|
71
|
-
</form>
|
|
43
|
+
<Vue2FACodeInput v-model="code"/>
|
|
72
44
|
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Cancel</button>
|
|
73
45
|
</div>
|
|
74
46
|
</div>
|
|
@@ -83,16 +55,16 @@
|
|
|
83
55
|
|
|
84
56
|
<script setup>
|
|
85
57
|
|
|
86
|
-
import { onMounted, ref, watchEffect,computed } from 'vue';
|
|
58
|
+
import { onMounted, ref, watchEffect,computed,watch } from 'vue';
|
|
87
59
|
import { useCoreStore } from '@/stores/core';
|
|
88
60
|
import { useUserStore } from '@/stores/user';
|
|
89
61
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
90
62
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
91
63
|
import { useRouter } from 'vue-router';
|
|
92
64
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
65
|
+
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
93
66
|
|
|
94
|
-
|
|
95
|
-
|
|
67
|
+
const code = ref(null);
|
|
96
68
|
|
|
97
69
|
const router = useRouter();
|
|
98
70
|
const inProgress = ref(false);
|
|
@@ -130,7 +102,17 @@ function parseJwt(token) {
|
|
|
130
102
|
|
|
131
103
|
// Parse the JSON payload
|
|
132
104
|
return JSON.parse(jsonPayload);
|
|
133
|
-
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onCopyClick(){
|
|
108
|
+
window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
watch(code, async (nv)=>{
|
|
112
|
+
if (nv){
|
|
113
|
+
sendCode();
|
|
114
|
+
}
|
|
115
|
+
})
|
|
134
116
|
|
|
135
117
|
onMounted(async () => {
|
|
136
118
|
coreStore.getPublicConfig()
|
|
@@ -140,27 +122,7 @@ onMounted(async () => {
|
|
|
140
122
|
if (resp && resp.totpJWT){
|
|
141
123
|
totpJWT.value = resp.totpJWT;
|
|
142
124
|
}
|
|
143
|
-
|
|
144
|
-
if (el.value.length === 0) {
|
|
145
|
-
if (prevId) {
|
|
146
|
-
document.getElementById(prevId).focus();
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
if (nextId) {
|
|
150
|
-
document.getElementById(nextId).focus();
|
|
151
|
-
} else {
|
|
152
|
-
sendCode()
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
document.querySelectorAll('[data-focus-input-init]').forEach(function(element) {
|
|
158
|
-
element.addEventListener('keyup', function() {
|
|
159
|
-
const prevId = this.getAttribute('data-focus-input-prev');
|
|
160
|
-
const nextId = this.getAttribute('data-focus-input-next');
|
|
161
|
-
focusNextInput(this, prevId, nextId);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
125
|
+
|
|
164
126
|
|
|
165
127
|
totp.value=parseJwt(totpJWT.value);
|
|
166
128
|
|
|
@@ -169,12 +131,11 @@ onMounted(async () => {
|
|
|
169
131
|
|
|
170
132
|
async function sendCode () {
|
|
171
133
|
inProgress.value = true;
|
|
172
|
-
const code = document.getElementById('code-1').value + document.getElementById('code-2').value + document.getElementById('code-3').value + document.getElementById('code-4').value + document.getElementById('code-5').value + document.getElementById('code-6').value;
|
|
173
134
|
const resp = await callAdminForthApi({
|
|
174
135
|
method: 'POST',
|
|
175
136
|
path: '/plugin/twofa/confirmSetup',
|
|
176
137
|
body: {
|
|
177
|
-
code: code,
|
|
138
|
+
code: code.value,
|
|
178
139
|
secret: totp.value.newSecret,
|
|
179
140
|
}
|
|
180
141
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@loltech/vue3-2fa-code-input": "^1.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/@loltech/vue3-2fa-code-input": {
|
|
16
|
+
"version": "1.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/@loltech/vue3-2fa-code-input/-/vue3-2fa-code-input-1.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-styyoQjsARSeVf6E5qMvYIVkLRrwYtfL8xDl5W+g9wKF8qlr2XGU2gi9maEg0X4Us41kJryXBzfRMUcv5tKIBA=="
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"description": "",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@loltech/vue3-2fa-code-input": "^1.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -14,38 +14,10 @@
|
|
|
14
14
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" >
|
|
15
15
|
<div class="p-8 w-full max-w-md max-h-full" >
|
|
16
16
|
<div class="m-3" >Please enter your authenticator code </div>
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<label for="code-1" class="sr-only">First code</label>
|
|
22
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-next="code-2" id="code-1" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
23
|
-
</div>
|
|
24
|
-
<div>
|
|
25
|
-
<label for="code-2" class="sr-only">Second code</label>
|
|
26
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-1" data-focus-input-next="code-3" id="code-2" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
27
|
-
</div>
|
|
28
|
-
<div>
|
|
29
|
-
<label for="code-3" class="sr-only">Third code</label>
|
|
30
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-2" data-focus-input-next="code-4" id="code-3" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
31
|
-
</div>
|
|
32
|
-
<div>
|
|
33
|
-
<label for="code-4" class="sr-only">Fourth code</label>
|
|
34
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-3" data-focus-input-next="code-5" id="code-4" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
35
|
-
</div>
|
|
36
|
-
<div>
|
|
37
|
-
<label for="code-5" class="sr-only">Fifth code</label>
|
|
38
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-4" data-focus-input-next="code-6" id="code-5" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
39
|
-
</div>
|
|
40
|
-
<div>
|
|
41
|
-
<label for="code-6" class="sr-only">Sixth code</label>
|
|
42
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-5" id="code-6" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
<p id="helper-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400"></p>
|
|
46
|
-
</form>
|
|
47
|
-
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
|
48
|
-
>Cancel</button>
|
|
17
|
+
<Vue2FACodeInput v-model="code"/>
|
|
18
|
+
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
|
19
|
+
Cancel
|
|
20
|
+
</button>
|
|
49
21
|
</div>
|
|
50
22
|
</div>
|
|
51
23
|
</div>
|
|
@@ -59,7 +31,7 @@
|
|
|
59
31
|
|
|
60
32
|
<script setup>
|
|
61
33
|
|
|
62
|
-
import { onMounted, ref, watchEffect,computed } from 'vue';
|
|
34
|
+
import { onMounted, ref, watchEffect,computed,watch } from 'vue';
|
|
63
35
|
import { useCoreStore } from '@/stores/core';
|
|
64
36
|
import { useUserStore } from '@/stores/user';
|
|
65
37
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
@@ -67,14 +39,15 @@
|
|
|
67
39
|
import { useRouter } from 'vue-router';
|
|
68
40
|
import { initFlowbite } from 'flowbite'
|
|
69
41
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
42
|
+
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
43
|
+
|
|
44
|
+
const code = ref(null);
|
|
73
45
|
const router = useRouter();
|
|
74
46
|
const inProgress = ref(false);
|
|
75
47
|
|
|
76
48
|
const coreStore = useCoreStore();
|
|
77
49
|
const user = useUserStore();
|
|
50
|
+
|
|
78
51
|
|
|
79
52
|
// use this simple function to automatically focus on the next input
|
|
80
53
|
|
|
@@ -103,41 +76,15 @@
|
|
|
103
76
|
|
|
104
77
|
onMounted(async () => {
|
|
105
78
|
coreStore.getPublicConfig()
|
|
106
|
-
|
|
107
|
-
function focusNextInput(el, prevId, nextId) {
|
|
108
|
-
if (el.value.length === 0) {
|
|
109
|
-
if (prevId) {
|
|
110
|
-
document.getElementById(prevId).focus();
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
if (nextId) {
|
|
114
|
-
document.getElementById(nextId).focus();
|
|
115
|
-
} else {
|
|
116
|
-
sendCode()
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
document.querySelectorAll('[data-focus-input-init]').forEach(function(element) {
|
|
122
|
-
element.addEventListener('keyup', function() {
|
|
123
|
-
const prevId = this.getAttribute('data-focus-input-prev');
|
|
124
|
-
const nextId = this.getAttribute('data-focus-input-next');
|
|
125
|
-
focusNextInput(this, prevId, nextId);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
79
|
});
|
|
132
80
|
|
|
133
81
|
async function sendCode () {
|
|
134
82
|
inProgress.value = true;
|
|
135
|
-
const code = document.getElementById('code-1').value + document.getElementById('code-2').value + document.getElementById('code-3').value + document.getElementById('code-4').value + document.getElementById('code-5').value + document.getElementById('code-6').value;
|
|
136
83
|
const resp = await callAdminForthApi({
|
|
137
84
|
method: 'POST',
|
|
138
85
|
path: '/plugin/twofa/confirmSetup',
|
|
139
86
|
body: {
|
|
140
|
-
code: code,
|
|
87
|
+
code: code.value,
|
|
141
88
|
secret: null,
|
|
142
89
|
}
|
|
143
90
|
})
|
|
@@ -147,5 +94,43 @@
|
|
|
147
94
|
showErrorTost('Invalid code');
|
|
148
95
|
}
|
|
149
96
|
}
|
|
97
|
+
|
|
98
|
+
watch(code, async (nv)=>{
|
|
99
|
+
if (nv){
|
|
100
|
+
sendCode();
|
|
101
|
+
}
|
|
102
|
+
})
|
|
150
103
|
</script>
|
|
104
|
+
|
|
105
|
+
<style lang='scss'>
|
|
106
|
+
.vue3-2fa-code-input {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
gap: 1rem;
|
|
110
|
+
margin-bottom: 1rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.vue3-2fa-code-input-box {
|
|
114
|
+
&[type='text'] {
|
|
115
|
+
@apply w-10 h-10 text-center text-xl border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500;
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* This particular piece of code makes the last input have a gap in the middle.
|
|
123
|
+
*/
|
|
124
|
+
.spaced-code-input {
|
|
125
|
+
& .vue3-2fa-code-input-box {
|
|
126
|
+
&:nth-child(3) {
|
|
127
|
+
@apply mr-4;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&:nth-child(4) {
|
|
131
|
+
@apply ml-4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
151
136
|
|
|
@@ -28,47 +28,19 @@
|
|
|
28
28
|
<svg class="w-3 h-3 me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
29
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
30
|
</svg>
|
|
31
|
-
<span class="text-xs font-semibold">Copy</span>
|
|
31
|
+
<span @click="onCopyClick" class="text-xs font-semibold">Copy</span>
|
|
32
32
|
</span>
|
|
33
33
|
<span id="success-message" class="hidden inline-flex items-center">
|
|
34
|
-
<svg class="w-3 h-3 text-
|
|
34
|
+
<svg class="w-3 h-3 text-lightPrimary dark:text-darkPrimary me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
|
|
35
35
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
|
|
36
36
|
</svg>
|
|
37
|
-
<span class="text-xs font-semibold text-
|
|
37
|
+
<span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
|
|
38
38
|
</span>
|
|
39
39
|
</button>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
43
|
-
<
|
|
44
|
-
<div class="flex mb-2 space-x-2 rtl:space-x-reverse ">
|
|
45
|
-
<div>
|
|
46
|
-
<label for="code-1" class="sr-only">First code</label>
|
|
47
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-next="code-2" id="code-1" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<label for="code-2" class="sr-only">Second code</label>
|
|
51
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-1" data-focus-input-next="code-3" id="code-2" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
52
|
-
</div>
|
|
53
|
-
<div>
|
|
54
|
-
<label for="code-3" class="sr-only">Third code</label>
|
|
55
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-2" data-focus-input-next="code-4" id="code-3" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
56
|
-
</div>
|
|
57
|
-
<div>
|
|
58
|
-
<label for="code-4" class="sr-only">Fourth code</label>
|
|
59
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-3" data-focus-input-next="code-5" id="code-4" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
60
|
-
</div>
|
|
61
|
-
<div>
|
|
62
|
-
<label for="code-5" class="sr-only">Fifth code</label>
|
|
63
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-4" data-focus-input-next="code-6" id="code-5" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
64
|
-
</div>
|
|
65
|
-
<div>
|
|
66
|
-
<label for="code-6" class="sr-only">Sixth code</label>
|
|
67
|
-
<input type="text" maxlength="1" data-focus-input-init data-focus-input-prev="code-5" id="code-6" class="block w-9 h-9 py-3 text-sm font-extrabold text-center text-gray-900 bg-white border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" required />
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
<p id="helper-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400"></p>
|
|
71
|
-
</form>
|
|
43
|
+
<Vue2FACodeInput v-model="code"/>
|
|
72
44
|
<button @click="()=>{router.push('./login')}" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Cancel</button>
|
|
73
45
|
</div>
|
|
74
46
|
</div>
|
|
@@ -83,16 +55,16 @@
|
|
|
83
55
|
|
|
84
56
|
<script setup>
|
|
85
57
|
|
|
86
|
-
import { onMounted, ref, watchEffect,computed } from 'vue';
|
|
58
|
+
import { onMounted, ref, watchEffect,computed,watch } from 'vue';
|
|
87
59
|
import { useCoreStore } from '@/stores/core';
|
|
88
60
|
import { useUserStore } from '@/stores/user';
|
|
89
61
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
90
62
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
91
63
|
import { useRouter } from 'vue-router';
|
|
92
64
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
65
|
+
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
93
66
|
|
|
94
|
-
|
|
95
|
-
|
|
67
|
+
const code = ref(null);
|
|
96
68
|
|
|
97
69
|
const router = useRouter();
|
|
98
70
|
const inProgress = ref(false);
|
|
@@ -130,7 +102,17 @@ function parseJwt(token) {
|
|
|
130
102
|
|
|
131
103
|
// Parse the JSON payload
|
|
132
104
|
return JSON.parse(jsonPayload);
|
|
133
|
-
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onCopyClick(){
|
|
108
|
+
window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
watch(code, async (nv)=>{
|
|
112
|
+
if (nv){
|
|
113
|
+
sendCode();
|
|
114
|
+
}
|
|
115
|
+
})
|
|
134
116
|
|
|
135
117
|
onMounted(async () => {
|
|
136
118
|
coreStore.getPublicConfig()
|
|
@@ -140,27 +122,7 @@ onMounted(async () => {
|
|
|
140
122
|
if (resp && resp.totpJWT){
|
|
141
123
|
totpJWT.value = resp.totpJWT;
|
|
142
124
|
}
|
|
143
|
-
|
|
144
|
-
if (el.value.length === 0) {
|
|
145
|
-
if (prevId) {
|
|
146
|
-
document.getElementById(prevId).focus();
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
if (nextId) {
|
|
150
|
-
document.getElementById(nextId).focus();
|
|
151
|
-
} else {
|
|
152
|
-
sendCode()
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
document.querySelectorAll('[data-focus-input-init]').forEach(function(element) {
|
|
158
|
-
element.addEventListener('keyup', function() {
|
|
159
|
-
const prevId = this.getAttribute('data-focus-input-prev');
|
|
160
|
-
const nextId = this.getAttribute('data-focus-input-next');
|
|
161
|
-
focusNextInput(this, prevId, nextId);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
125
|
+
|
|
164
126
|
|
|
165
127
|
totp.value=parseJwt(totpJWT.value);
|
|
166
128
|
|
|
@@ -169,12 +131,11 @@ onMounted(async () => {
|
|
|
169
131
|
|
|
170
132
|
async function sendCode () {
|
|
171
133
|
inProgress.value = true;
|
|
172
|
-
const code = document.getElementById('code-1').value + document.getElementById('code-2').value + document.getElementById('code-3').value + document.getElementById('code-4').value + document.getElementById('code-5').value + document.getElementById('code-6').value;
|
|
173
134
|
const resp = await callAdminForthApi({
|
|
174
135
|
method: 'POST',
|
|
175
136
|
path: '/plugin/twofa/confirmSetup',
|
|
176
137
|
body: {
|
|
177
|
-
code: code,
|
|
138
|
+
code: code.value,
|
|
178
139
|
secret: totp.value.newSecret,
|
|
179
140
|
}
|
|
180
141
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@loltech/vue3-2fa-code-input": "^1.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/@loltech/vue3-2fa-code-input": {
|
|
16
|
+
"version": "1.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/@loltech/vue3-2fa-code-input/-/vue3-2fa-code-input-1.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-styyoQjsARSeVf6E5qMvYIVkLRrwYtfL8xDl5W+g9wKF8qlr2XGU2gi9maEg0X4Us41kJryXBzfRMUcv5tKIBA=="
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"description": "",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@loltech/vue3-2fa-code-input": "^1.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
14
14
|
super(options, import.meta.url);
|
|
15
15
|
this.options = options;
|
|
16
16
|
}
|
|
17
|
+
instanceUniqueRepresentation(pluginOptions) {
|
|
18
|
+
return `single`;
|
|
19
|
+
}
|
|
17
20
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
18
21
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
19
22
|
this.adminforth = adminforth;
|
|
@@ -41,16 +44,17 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
41
44
|
throw new Error(`Column ${this.options.twoFaSecretFieldName} not found in ${this.authResource.label}`);
|
|
42
45
|
}
|
|
43
46
|
this.adminforth.config.auth.beforeLoginConfirmation.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response }) {
|
|
44
|
-
if (adminUser.isRoot) {
|
|
45
|
-
return { body: { loginAllowed: true }, ok: true };
|
|
46
|
-
}
|
|
47
47
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName];
|
|
48
48
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
|
|
49
49
|
const brandName = adminforth.config.customization.brandName;
|
|
50
|
-
const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.
|
|
50
|
+
const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
|
|
51
51
|
const authPk = authResource.columns.find((col) => col.primaryKey).name;
|
|
52
52
|
const userPk = adminUser.dbUser[authPk];
|
|
53
53
|
let newSecret = null;
|
|
54
|
+
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
55
|
+
if (!userNeeds2FA) {
|
|
56
|
+
return { body: { loginAllowed: true }, ok: true };
|
|
57
|
+
}
|
|
54
58
|
if (!secret) {
|
|
55
59
|
const tempSecret = twofactor.generateSecret({ name: brandName, account: userName });
|
|
56
60
|
newSecret = tempSecret.secret;
|
package/index.ts
CHANGED
|
@@ -15,6 +15,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
15
15
|
this.options = options;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
instanceUniqueRepresentation(pluginOptions: any) : string {
|
|
19
|
+
return `single`;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
19
23
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
20
24
|
this.adminforth = adminforth;
|
|
@@ -44,16 +48,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
44
48
|
}
|
|
45
49
|
(this.adminforth.config.auth.beforeLoginConfirmation as BeforeLoginConfirmationFunction[]).push(
|
|
46
50
|
async({ adminUser, response }: { adminUser: AdminUser, response: IAdminForthHttpResponse} )=> {
|
|
47
|
-
if(adminUser.isRoot){
|
|
48
|
-
return { body: {loginAllowed: true}, ok: true}
|
|
49
|
-
}
|
|
50
51
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName]
|
|
51
52
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField]
|
|
52
53
|
const brandName = adminforth.config.customization.brandName
|
|
53
|
-
const authResource = adminforth.config.resources.find((res)=>res.resourceId === adminforth.config.auth.
|
|
54
|
+
const authResource = adminforth.config.resources.find((res)=>res.resourceId === adminforth.config.auth.usersResourceId )
|
|
54
55
|
const authPk = authResource.columns.find((col)=>col.primaryKey).name
|
|
55
56
|
const userPk = adminUser.dbUser[authPk]
|
|
56
|
-
let newSecret = null
|
|
57
|
+
let newSecret = null;
|
|
58
|
+
|
|
59
|
+
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
60
|
+
if (!userNeeds2FA){
|
|
61
|
+
return { body:{loginAllowed: true}, ok: true}
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
if (!secret){
|
|
58
65
|
const tempSecret = twofactor.generateSecret({name: brandName,account: userName})
|
|
59
66
|
newSecret = tempSecret.secret
|
|
@@ -65,10 +72,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
65
72
|
body:{
|
|
66
73
|
loginAllowed: false,
|
|
67
74
|
redirectTo: '/confirm2fa',
|
|
68
|
-
|
|
69
75
|
},
|
|
70
76
|
ok: true
|
|
71
|
-
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk },'tempTotp', )
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/two-factors-auth",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "AdminForth plugin for two factors authentication",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"rollout": "tsc &&
|
|
9
|
+
"rollout": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch && npm publish --access public",
|
|
10
|
+
"prepare": "npm link adminforth"
|
|
10
11
|
},
|
|
11
12
|
"author": "devforth",
|
|
12
13
|
"license": "ISC",
|
package/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AdminUser } from "adminforth";
|
|
2
|
+
|
|
1
3
|
export type PluginOptions = {
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,4 +9,12 @@ export type PluginOptions = {
|
|
|
7
9
|
* name should be equal to the value .
|
|
8
10
|
*/
|
|
9
11
|
twoFaSecretFieldName: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Optional function to filter users to apply 2FA.
|
|
15
|
+
* Should return true if 2FA should be applied to the user and false if AdminForth should not challenge the user with 2FA.
|
|
16
|
+
* @param adminUser
|
|
17
|
+
* @returns true if 2FA should be applied to the user and false if AdminForth should not challenge the user with 2FA.
|
|
18
|
+
*/
|
|
19
|
+
usersFilterToApply: (adminUser: AdminUser) => boolean;
|
|
10
20
|
}
|