@adminforth/two-factors-auth 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.log CHANGED
@@ -4,11 +4,12 @@
4
4
 
5
5
  sending incremental file list
6
6
  custom/
7
+ custom/TwoFaModal.vue
7
8
  custom/TwoFactorsConfirmation.vue
8
9
  custom/TwoFactorsSetup.vue
9
10
  custom/package-lock.json
10
11
  custom/package.json
11
12
  custom/tsconfig.json
12
13
 
13
- sent 26,506 bytes received 115 bytes 53,242.00 bytes/sec
14
- total size is 26,077 speedup is 0.98
14
+ sent 31,056 bytes received 134 bytes 62,380.00 bytes/sec
15
+ total size is 30,563 speedup is 0.98
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 top-0 bottom-0 left-0 right-0"
3
+ v-if ="modelShow">
4
+ <div class="relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
+ <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100">
6
+ {{ $t('Please enter your authenticator code') }}
7
+ </div>
8
+
9
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
10
+ <v-otp-input
11
+ ref="code"
12
+ container-class="grid grid-cols-6 gap-3 w-full"
13
+ input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
14
+ :num-inputs="6"
15
+ inputType="number"
16
+ inputmode="numeric"
17
+ :should-auto-focus="true"
18
+ :should-focus-order="true"
19
+ v-model:value="bindValue"
20
+ @on-complete="handleOnComplete"
21
+ />
22
+ </div>
23
+
24
+ <div class="mt-6 flex justify-center gap-3">
25
+ <button
26
+ class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ @click="onCancel"
28
+ :disabled="inProgress"
29
+ >{{ $t('Cancel') }}</button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import VOtpInput from 'vue3-otp-input';
37
+ import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue';
38
+ import { useUserStore } from '@/stores/user';
39
+ import { useI18n } from 'vue-i18n';
40
+ declare global {
41
+ interface Window {
42
+ adminforthTwoFaModal: {
43
+ getCode: () => Promise<any>;
44
+ };
45
+ }
46
+ }
47
+ const props = defineProps<{
48
+ autoFinishLogin?: boolean
49
+ }>();
50
+ const emit = defineEmits<{
51
+ (e: 'resolved', payload: any): void
52
+ (e: 'rejected', err?: any): void
53
+ (e: 'closed'): void
54
+ }>();
55
+
56
+ const modelShow = ref(false);
57
+ let resolveFn: ((code: string) => void) | null = null;
58
+ let verifyingCallback: ((code: string) => boolean) | null = null;
59
+ let verifyFn: null | ((code: string) => Promise<boolean> | boolean) = null;
60
+
61
+ window.adminforthTwoFaModal = {
62
+ getCode: (verifyingCallback?: () => Promise<boolean>) => new Promise((resolve) => {
63
+ if (modelShow.value) {
64
+ throw new Error('Modal is already open');
65
+ }
66
+ modelShow.value = true;
67
+ resolveFn = resolve;
68
+ verifyFn = verifyingCallback ?? null;
69
+ }),
70
+ };
71
+
72
+ const { t } = useI18n();
73
+ const user = useUserStore();
74
+
75
+ const code = ref<any>(null);
76
+ const otpRoot = ref<HTMLElement | null>(null);
77
+ const bindValue = ref('');
78
+
79
+ function tagOtpInputs() {
80
+ const root = otpRoot.value;
81
+ if (!root) return;
82
+ root.querySelectorAll('input.otp-input').forEach((el, idx) => {
83
+ el.setAttribute('name', 'mfaCode');
84
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
85
+ el.setAttribute('autocomplete', 'one-time-code');
86
+ el.setAttribute('inputmode', 'numeric');
87
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
88
+ });
89
+ }
90
+
91
+ function handlePaste(event: ClipboardEvent) {
92
+ event.preventDefault();
93
+ const pastedText = event.clipboardData?.getData('text') || '';
94
+ if (pastedText.length === 6) {
95
+ code.value?.fillInput(pastedText);
96
+ }
97
+ }
98
+
99
+ async function submit() {
100
+ if (bindValue.value.length !== 6) return;
101
+ await sendCode(bindValue.value);
102
+ }
103
+
104
+ async function handleOnComplete(value: string) {
105
+ await sendCode(value);
106
+ }
107
+
108
+ async function sendCode(value: string) {
109
+ if (!resolveFn) {
110
+ throw new Error('Modal is not initialized properly');
111
+ }
112
+
113
+ if (verifyFn) {
114
+ try {
115
+ const ok = await verifyFn(value);
116
+ if (!ok) {
117
+ return;
118
+ }
119
+ } catch {
120
+ return;
121
+ }
122
+ }
123
+
124
+ modelShow.value = false;
125
+ resolveFn(value);
126
+ }
127
+
128
+
129
+ function onCancel() {
130
+ modelShow.value = false;
131
+ emit('rejected', new Error('cancelled'));
132
+ emit('closed');
133
+ }
134
+
135
+ onMounted(async () => {
136
+ await nextTick();
137
+ tagOtpInputs();
138
+ window.addEventListener('paste', handlePaste as any);
139
+ });
140
+ onBeforeUnmount(() => {
141
+ window.removeEventListener('paste', handlePaste as any);
142
+ });
143
+ </script>
144
+
@@ -4,7 +4,7 @@
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
@@ -4,7 +4,7 @@
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 top-0 bottom-0 left-0 right-0"
3
+ v-if ="modelShow">
4
+ <div class="relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
+ <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100">
6
+ {{ $t('Please enter your authenticator code') }}
7
+ </div>
8
+
9
+ <div class="my-4 w-full flex justify-center" ref="otpRoot">
10
+ <v-otp-input
11
+ ref="code"
12
+ container-class="grid grid-cols-6 gap-3 w-full"
13
+ input-classes="bg-gray-50 text-center flex justify-center otp-input border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-10 h-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
14
+ :num-inputs="6"
15
+ inputType="number"
16
+ inputmode="numeric"
17
+ :should-auto-focus="true"
18
+ :should-focus-order="true"
19
+ v-model:value="bindValue"
20
+ @on-complete="handleOnComplete"
21
+ />
22
+ </div>
23
+
24
+ <div class="mt-6 flex justify-center gap-3">
25
+ <button
26
+ class="px-4 py-2 rounded border bg-gray-100 dark:bg-gray-600"
27
+ @click="onCancel"
28
+ :disabled="inProgress"
29
+ >{{ $t('Cancel') }}</button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import VOtpInput from 'vue3-otp-input';
37
+ import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue';
38
+ import { useUserStore } from '@/stores/user';
39
+ import { useI18n } from 'vue-i18n';
40
+ declare global {
41
+ interface Window {
42
+ adminforthTwoFaModal: {
43
+ getCode: () => Promise<any>;
44
+ };
45
+ }
46
+ }
47
+ const props = defineProps<{
48
+ autoFinishLogin?: boolean
49
+ }>();
50
+ const emit = defineEmits<{
51
+ (e: 'resolved', payload: any): void
52
+ (e: 'rejected', err?: any): void
53
+ (e: 'closed'): void
54
+ }>();
55
+
56
+ const modelShow = ref(false);
57
+ let resolveFn: ((code: string) => void) | null = null;
58
+ let verifyingCallback: ((code: string) => boolean) | null = null;
59
+ let verifyFn: null | ((code: string) => Promise<boolean> | boolean) = null;
60
+
61
+ window.adminforthTwoFaModal = {
62
+ getCode: (verifyingCallback?: () => Promise<boolean>) => new Promise((resolve) => {
63
+ if (modelShow.value) {
64
+ throw new Error('Modal is already open');
65
+ }
66
+ modelShow.value = true;
67
+ resolveFn = resolve;
68
+ verifyFn = verifyingCallback ?? null;
69
+ }),
70
+ };
71
+
72
+ const { t } = useI18n();
73
+ const user = useUserStore();
74
+
75
+ const code = ref<any>(null);
76
+ const otpRoot = ref<HTMLElement | null>(null);
77
+ const bindValue = ref('');
78
+
79
+ function tagOtpInputs() {
80
+ const root = otpRoot.value;
81
+ if (!root) return;
82
+ root.querySelectorAll('input.otp-input').forEach((el, idx) => {
83
+ el.setAttribute('name', 'mfaCode');
84
+ el.setAttribute('id', `mfaCode-${idx + 1}`);
85
+ el.setAttribute('autocomplete', 'one-time-code');
86
+ el.setAttribute('inputmode', 'numeric');
87
+ el.setAttribute('aria-labelledby', 'mfaCode-label');
88
+ });
89
+ }
90
+
91
+ function handlePaste(event: ClipboardEvent) {
92
+ event.preventDefault();
93
+ const pastedText = event.clipboardData?.getData('text') || '';
94
+ if (pastedText.length === 6) {
95
+ code.value?.fillInput(pastedText);
96
+ }
97
+ }
98
+
99
+ async function submit() {
100
+ if (bindValue.value.length !== 6) return;
101
+ await sendCode(bindValue.value);
102
+ }
103
+
104
+ async function handleOnComplete(value: string) {
105
+ await sendCode(value);
106
+ }
107
+
108
+ async function sendCode(value: string) {
109
+ if (!resolveFn) {
110
+ throw new Error('Modal is not initialized properly');
111
+ }
112
+
113
+ if (verifyFn) {
114
+ try {
115
+ const ok = await verifyFn(value);
116
+ if (!ok) {
117
+ return;
118
+ }
119
+ } catch {
120
+ return;
121
+ }
122
+ }
123
+
124
+ modelShow.value = false;
125
+ resolveFn(value);
126
+ }
127
+
128
+
129
+ function onCancel() {
130
+ modelShow.value = false;
131
+ emit('rejected', new Error('cancelled'));
132
+ emit('closed');
133
+ }
134
+
135
+ onMounted(async () => {
136
+ await nextTick();
137
+ tagOtpInputs();
138
+ window.addEventListener('paste', handlePaste as any);
139
+ });
140
+ onBeforeUnmount(() => {
141
+ window.removeEventListener('paste', handlePaste as any);
142
+ });
143
+ </script>
144
+
@@ -4,7 +4,7 @@
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
@@ -4,7 +4,7 @@
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
30
30
  path: '/setup2fa',
31
31
  component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true } }
32
32
  });
33
+ const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || [];
34
+ everyPageBottomInjections.push({ file: this.componentPath('TwoFaModal.vue'), meta: {} });
33
35
  this.activate(resourceConfig, adminforth);
34
36
  }
35
37
  activate(resourceConfig, adminforth) {
@@ -175,5 +177,24 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
175
177
  }
176
178
  }),
177
179
  });
180
+ server.endpoint({
181
+ method: 'POST',
182
+ path: `/plugin/twofa/verify`,
183
+ noAuth: false,
184
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, body }) {
185
+ if (!(body === null || body === void 0 ? void 0 : body.code))
186
+ return { error: 'Code is required' };
187
+ const authRes = this.adminforth.config.resources
188
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
189
+ const connector = this.adminforth.connectors[authRes.dataSource];
190
+ const pkName = authRes.columns.find(c => c.primaryKey).name;
191
+ const user = yield connector.getRecordByPrimaryKey(authRes, adminUser.dbUser[pkName]);
192
+ const secret = user[this.options.twoFaSecretFieldName];
193
+ if (!secret)
194
+ return { error: '2FA is not set up for this user' };
195
+ const verified = twofactor.verifyToken(secret, body.code, this.options.timeStepWindow);
196
+ return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
197
+ })
198
+ });
178
199
  }
179
200
  }
package/index.ts CHANGED
@@ -33,6 +33,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
33
33
  path:'/setup2fa',
34
34
  component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true }}
35
35
  })
36
+ const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || []
37
+ everyPageBottomInjections.push({ file: this.componentPath('TwoFaModal.vue'), meta: {} })
36
38
  this.activate( resourceConfig, adminforth )
37
39
  }
38
40
 
@@ -188,5 +190,25 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
188
190
  }
189
191
  },
190
192
  });
193
+ server.endpoint({
194
+ method: 'POST',
195
+ path: `/plugin/twofa/verify`,
196
+ noAuth: false,
197
+ handler: async ({ adminUser, body }) => {
198
+ if (!body?.code) return { error: 'Code is required' };
199
+
200
+ const authRes = this.adminforth.config.resources
201
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
202
+ const connector = this.adminforth.connectors[authRes.dataSource];
203
+ const pkName = authRes.columns.find(c => c.primaryKey).name;
204
+ const user = await connector.getRecordByPrimaryKey(authRes, adminUser.dbUser[pkName]);
205
+
206
+ const secret = user[this.options.twoFaSecretFieldName];
207
+ if (!secret) return { error: '2FA is not set up for this user' };
208
+
209
+ const verified = twofactor.verifyToken(secret, body.code, this.options.timeStepWindow);
210
+ return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
211
+ }
212
+ });
191
213
  }
192
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",