@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.
@@ -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
- <form class="max-w-sm mx-auto mt-6 flex flex-col items-center ">
19
- <div class="flex mb-2 space-x-2 rtl:space-x-reverse ">
20
- <div>
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-blue-700 dark:text-blue-500 me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
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-blue-700 dark:text-blue-500">Copied</span>
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
- <form class="max-w-sm mx-auto mt-6 flex flex-col items-center ">
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
- function focusNextInput(el, prevId, nextId) {
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
- <form class="max-w-sm mx-auto mt-6 flex flex-col items-center ">
19
- <div class="flex mb-2 space-x-2 rtl:space-x-reverse ">
20
- <div>
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-blue-700 dark:text-blue-500 me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
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-blue-700 dark:text-blue-500">Copied</span>
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
- <form class="max-w-sm mx-auto mt-6 flex flex-col items-center ">
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
- function focusNextInput(el, prevId, nextId) {
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.resourceId);
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.resourceId )
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.15",
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 && cp -rf custom dist/ && npm version patch && npm publish --access public"
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
  }