@adminforth/two-factors-auth 3.2.3 → 3.3.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 +2 -2
- package/custom/TwoFAModal.vue +1 -1
- package/custom/TwoFactorsConfirmation.vue +96 -65
- package/dist/custom/TwoFAModal.vue +1 -1
- package/dist/custom/TwoFactorsConfirmation.vue +96 -65
- package/dist/index.js +16 -4
- package/index.ts +21 -5
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -16,5 +16,5 @@ custom/pnpm-lock.yaml
|
|
|
16
16
|
custom/tsconfig.json
|
|
17
17
|
custom/utils.js
|
|
18
18
|
|
|
19
|
-
sent 285,
|
|
20
|
-
total size is 284,
|
|
19
|
+
sent 285,644 bytes received 229 bytes 571,746.00 bytes/sec
|
|
20
|
+
total size is 284,738 speedup is 1.00
|
package/custom/TwoFAModal.vue
CHANGED
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
if (!response.ok && response.error === 'No session ID or confirmation result'){
|
|
177
177
|
alert({message: t('Verification session finished or cancelled.'), variant: 'warning'});
|
|
178
178
|
} else if (!response.ok) {
|
|
179
|
-
alert({message: t('Verification failed'), variant: 'danger'});
|
|
179
|
+
alert({message: response.error || t('Verification failed'), variant: 'danger'});
|
|
180
180
|
} else if (response.ok) {
|
|
181
181
|
alert({message: t('Verification successful'), variant: 'success'});
|
|
182
182
|
}
|
|
@@ -7,73 +7,83 @@
|
|
|
7
7
|
'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class="w-full"
|
|
59
|
-
>
|
|
60
|
-
{{$t('back to login')}}
|
|
61
|
-
</Link>
|
|
62
|
-
</div>
|
|
63
|
-
</p>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
10
|
+
<div v-if="isLoading===false" id="authentication-modal" class="af-two-factors-confirmation flex items-center justify-center w-full p-4">
|
|
11
|
+
<div class="relative w-full max-w-md">
|
|
12
|
+
|
|
13
|
+
<div class="af-login-popup relative bg-white dark:bg-gray-700 rounded-lg shadow p-6" :class="codeError ? 'rounded-b-none' : ''">
|
|
14
|
+
|
|
15
|
+
<div class="af-2fa-header flex flex-col items-center justify-center gap-3 mb-6">
|
|
16
|
+
<div class="af-2fa-icon-wrap w-14 h-14 shrink-0 flex items-center justify-center rounded-full bg-lightPrimary dark:bg-darkPrimary">
|
|
17
|
+
<IconShieldOutline class="af-2fa-shield-icon w-7 h-7 text-white" />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="af-2fa-title-wrap text-center">
|
|
20
|
+
<p class="text-xl font-medium text-gray-900 dark:text-white">
|
|
21
|
+
{{ confirmationMode === 'code' ? $t('Two-factor Auth') : $t('Passkey') }}
|
|
22
|
+
</p>
|
|
23
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
24
|
+
{{ confirmationMode === 'code'
|
|
25
|
+
? $t('Please enter your authenticator code')
|
|
26
|
+
: $t('When you are ready, authenticate using the button below')
|
|
27
|
+
}}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div v-if="confirmationMode === 'code'" class="af-2fa-otp-root flex flex-col items-center gap-6" ref="otpRoot">
|
|
33
|
+
<v-otp-input
|
|
34
|
+
ref="code"
|
|
35
|
+
container-class="grid grid-cols-6 gap-3"
|
|
36
|
+
input-classes="bg-gray-50 text-center flex justify-center otp-input border 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-600 dark:border-gray-500 dark:text-white"
|
|
37
|
+
:num-inputs="6"
|
|
38
|
+
inputType="number"
|
|
39
|
+
inputmode="numeric"
|
|
40
|
+
:should-auto-focus="true"
|
|
41
|
+
v-model:value="bindValue"
|
|
42
|
+
@on-complete="handleOnComplete"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
|
|
46
|
+
<p>
|
|
47
|
+
{{$t('Having trouble?')}}
|
|
48
|
+
<button v-if="doesUserHavePasskeys" type="button"
|
|
49
|
+
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
|
|
50
|
+
@click="router.push({ hash: '#passkey' })">
|
|
51
|
+
{{$t('Use passkey instead')}}
|
|
52
|
+
</button>
|
|
53
|
+
<span v-if="doesUserHavePasskeys" class="mx-1">{{$t('or')}}</span>
|
|
54
|
+
<Link to="/login" class="hover-link">
|
|
55
|
+
{{$t('Back to login')}}
|
|
56
|
+
</Link>
|
|
57
|
+
</p>
|
|
66
58
|
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div v-else class="af-2fa-passkey-root flex flex-col gap-6">
|
|
62
|
+
<Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="af-2fa-passkey-btn w-full flex items-center justify-center gap-2">
|
|
63
|
+
<IconShieldOutline class="w-4 h-4" />
|
|
64
|
+
{{$t('Use passkey to verify')}}
|
|
65
|
+
</Button>
|
|
66
|
+
|
|
67
|
+
<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
|
|
68
|
+
<p>
|
|
69
|
+
{{$t('Having trouble?')}}
|
|
70
|
+
<button type="button"
|
|
71
|
+
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
|
|
72
|
+
@click="router.push({ hash: '#code' })">
|
|
73
|
+
{{$t('Use TOTP instead')}}
|
|
74
|
+
</button>
|
|
75
|
+
<span class="mx-1">{{$t('or')}}</span>
|
|
76
|
+
<Link to="/login" class="hover-link">
|
|
77
|
+
{{$t('Back to login')}}
|
|
78
|
+
</Link>
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
67
82
|
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
>
|
|
72
|
-
<p class="pl-6">{{ codeError }} </p>
|
|
83
|
+
|
|
84
|
+
<div v-if="codeError" class="af-two-factors-confirmation-error relative bg-red-100 text-red-700 text-xs px-4 py-2 rounded-b-lg shadow border-t border-red-200 text-center">
|
|
85
|
+
{{ codeError }}
|
|
73
86
|
</div>
|
|
74
|
-
<div v-else class="h-[36px] opacity-0">
|
|
75
|
-
|
|
76
|
-
</div>
|
|
77
87
|
</div>
|
|
78
88
|
</div>
|
|
79
89
|
<div v-else>
|
|
@@ -477,5 +487,26 @@
|
|
|
477
487
|
@apply ml-4;
|
|
478
488
|
}
|
|
479
489
|
}
|
|
480
|
-
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
:deep(.otp-input-container) {
|
|
493
|
+
display: flex;
|
|
494
|
+
gap: 0.75rem;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.hover-link {
|
|
498
|
+
text-decoration: none !important;
|
|
499
|
+
display: inline-block;
|
|
500
|
+
width: fit-content;
|
|
501
|
+
margin: 0 auto;
|
|
502
|
+
|
|
503
|
+
&:hover {
|
|
504
|
+
text-decoration: underline !important;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
button.hover-link {
|
|
509
|
+
font-family: inherit;
|
|
510
|
+
font-size: inherit;
|
|
511
|
+
}
|
|
481
512
|
</style>
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
if (!response.ok && response.error === 'No session ID or confirmation result'){
|
|
177
177
|
alert({message: t('Verification session finished or cancelled.'), variant: 'warning'});
|
|
178
178
|
} else if (!response.ok) {
|
|
179
|
-
alert({message: t('Verification failed'), variant: 'danger'});
|
|
179
|
+
alert({message: response.error || t('Verification failed'), variant: 'danger'});
|
|
180
180
|
} else if (response.ok) {
|
|
181
181
|
alert({message: t('Verification successful'), variant: 'success'});
|
|
182
182
|
}
|
|
@@ -7,73 +7,83 @@
|
|
|
7
7
|
'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class="w-full"
|
|
59
|
-
>
|
|
60
|
-
{{$t('back to login')}}
|
|
61
|
-
</Link>
|
|
62
|
-
</div>
|
|
63
|
-
</p>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
10
|
+
<div v-if="isLoading===false" id="authentication-modal" class="af-two-factors-confirmation flex items-center justify-center w-full p-4">
|
|
11
|
+
<div class="relative w-full max-w-md">
|
|
12
|
+
|
|
13
|
+
<div class="af-login-popup relative bg-white dark:bg-gray-700 rounded-lg shadow p-6" :class="codeError ? 'rounded-b-none' : ''">
|
|
14
|
+
|
|
15
|
+
<div class="af-2fa-header flex flex-col items-center justify-center gap-3 mb-6">
|
|
16
|
+
<div class="af-2fa-icon-wrap w-14 h-14 shrink-0 flex items-center justify-center rounded-full bg-lightPrimary dark:bg-darkPrimary">
|
|
17
|
+
<IconShieldOutline class="af-2fa-shield-icon w-7 h-7 text-white" />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="af-2fa-title-wrap text-center">
|
|
20
|
+
<p class="text-xl font-medium text-gray-900 dark:text-white">
|
|
21
|
+
{{ confirmationMode === 'code' ? $t('Two-factor Auth') : $t('Passkey') }}
|
|
22
|
+
</p>
|
|
23
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
24
|
+
{{ confirmationMode === 'code'
|
|
25
|
+
? $t('Please enter your authenticator code')
|
|
26
|
+
: $t('When you are ready, authenticate using the button below')
|
|
27
|
+
}}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div v-if="confirmationMode === 'code'" class="af-2fa-otp-root flex flex-col items-center gap-6" ref="otpRoot">
|
|
33
|
+
<v-otp-input
|
|
34
|
+
ref="code"
|
|
35
|
+
container-class="grid grid-cols-6 gap-3"
|
|
36
|
+
input-classes="bg-gray-50 text-center flex justify-center otp-input border 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-600 dark:border-gray-500 dark:text-white"
|
|
37
|
+
:num-inputs="6"
|
|
38
|
+
inputType="number"
|
|
39
|
+
inputmode="numeric"
|
|
40
|
+
:should-auto-focus="true"
|
|
41
|
+
v-model:value="bindValue"
|
|
42
|
+
@on-complete="handleOnComplete"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
|
|
46
|
+
<p>
|
|
47
|
+
{{$t('Having trouble?')}}
|
|
48
|
+
<button v-if="doesUserHavePasskeys" type="button"
|
|
49
|
+
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
|
|
50
|
+
@click="router.push({ hash: '#passkey' })">
|
|
51
|
+
{{$t('Use passkey instead')}}
|
|
52
|
+
</button>
|
|
53
|
+
<span v-if="doesUserHavePasskeys" class="mx-1">{{$t('or')}}</span>
|
|
54
|
+
<Link to="/login" class="hover-link">
|
|
55
|
+
{{$t('Back to login')}}
|
|
56
|
+
</Link>
|
|
57
|
+
</p>
|
|
66
58
|
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div v-else class="af-2fa-passkey-root flex flex-col gap-6">
|
|
62
|
+
<Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="af-2fa-passkey-btn w-full flex items-center justify-center gap-2">
|
|
63
|
+
<IconShieldOutline class="w-4 h-4" />
|
|
64
|
+
{{$t('Use passkey to verify')}}
|
|
65
|
+
</Button>
|
|
66
|
+
|
|
67
|
+
<div class="af-2fa-footer text-center text-xs text-gray-500 dark:text-gray-400">
|
|
68
|
+
<p>
|
|
69
|
+
{{$t('Having trouble?')}}
|
|
70
|
+
<button type="button"
|
|
71
|
+
class="hover-link bg-transparent text-[#16537E] border-none p-0 cursor-pointer ml-1"
|
|
72
|
+
@click="router.push({ hash: '#code' })">
|
|
73
|
+
{{$t('Use TOTP instead')}}
|
|
74
|
+
</button>
|
|
75
|
+
<span class="mx-1">{{$t('or')}}</span>
|
|
76
|
+
<Link to="/login" class="hover-link">
|
|
77
|
+
{{$t('Back to login')}}
|
|
78
|
+
</Link>
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
67
82
|
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
>
|
|
72
|
-
<p class="pl-6">{{ codeError }} </p>
|
|
83
|
+
|
|
84
|
+
<div v-if="codeError" class="af-two-factors-confirmation-error relative bg-red-100 text-red-700 text-xs px-4 py-2 rounded-b-lg shadow border-t border-red-200 text-center">
|
|
85
|
+
{{ codeError }}
|
|
73
86
|
</div>
|
|
74
|
-
<div v-else class="h-[36px] opacity-0">
|
|
75
|
-
|
|
76
|
-
</div>
|
|
77
87
|
</div>
|
|
78
88
|
</div>
|
|
79
89
|
<div v-else>
|
|
@@ -477,5 +487,26 @@
|
|
|
477
487
|
@apply ml-4;
|
|
478
488
|
}
|
|
479
489
|
}
|
|
480
|
-
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
:deep(.otp-input-container) {
|
|
493
|
+
display: flex;
|
|
494
|
+
gap: 0.75rem;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.hover-link {
|
|
498
|
+
text-decoration: none !important;
|
|
499
|
+
display: inline-block;
|
|
500
|
+
width: fit-content;
|
|
501
|
+
margin: 0 auto;
|
|
502
|
+
|
|
503
|
+
&:hover {
|
|
504
|
+
text-decoration: underline !important;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
button.hover-link {
|
|
509
|
+
font-family: inherit;
|
|
510
|
+
font-size: inherit;
|
|
511
|
+
}
|
|
481
512
|
</style>
|
package/dist/index.js
CHANGED
|
@@ -134,24 +134,36 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
134
134
|
return false;
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
waitForResponse(id) {
|
|
137
|
+
waitForResponse(id, timeoutMs) {
|
|
138
138
|
return new Promise((resolve) => {
|
|
139
|
-
|
|
139
|
+
let timeout;
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
this.pending.delete(id);
|
|
143
|
+
};
|
|
144
|
+
timeout = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
resolve({ ok: false, error: 'Verification timed out' });
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
this.pending.set(id, (value) => {
|
|
149
|
+
cleanup();
|
|
150
|
+
resolve(value);
|
|
151
|
+
});
|
|
140
152
|
});
|
|
141
153
|
}
|
|
142
154
|
resolveResponse(id, data) {
|
|
143
155
|
const resolve = this.pending.get(id);
|
|
144
156
|
if (resolve) {
|
|
145
157
|
resolve(data);
|
|
146
|
-
this.pending.delete(id);
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
verifyAuto(adminUser) {
|
|
150
161
|
return __awaiter(this, void 0, void 0, function* () {
|
|
151
162
|
const sessionId = crypto.randomUUID();
|
|
152
163
|
const jwt = this.adminforth.auth.issueJWT({ sessionId, adminUserPk: adminUser.pk }, 'auto2FA', '5m');
|
|
164
|
+
const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
|
|
153
165
|
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
|
|
154
|
-
const result = yield
|
|
166
|
+
const result = yield resultPromise;
|
|
155
167
|
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
|
|
156
168
|
return result;
|
|
157
169
|
});
|
package/index.ts
CHANGED
|
@@ -143,9 +143,21 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
143
143
|
|
|
144
144
|
private pending = new Map<string, (value: any) => void>();
|
|
145
145
|
|
|
146
|
-
private waitForResponse(id: string): Promise<any> {
|
|
146
|
+
private waitForResponse(id: string, timeoutMs: number): Promise<any> {
|
|
147
147
|
return new Promise((resolve) => {
|
|
148
|
-
|
|
148
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
this.pending.delete(id);
|
|
152
|
+
};
|
|
153
|
+
timeout = setTimeout(() => {
|
|
154
|
+
cleanup();
|
|
155
|
+
resolve({ ok: false, error: 'Verification timed out' });
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
this.pending.set(id, (value: any) => {
|
|
158
|
+
cleanup();
|
|
159
|
+
resolve(value);
|
|
160
|
+
});
|
|
149
161
|
});
|
|
150
162
|
}
|
|
151
163
|
|
|
@@ -153,16 +165,20 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
153
165
|
const resolve = this.pending.get(id);
|
|
154
166
|
if (resolve) {
|
|
155
167
|
resolve(data);
|
|
156
|
-
this.pending.delete(id);
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
public async verifyAuto(adminUser: AdminUser) {
|
|
161
172
|
const sessionId = crypto.randomUUID();
|
|
162
173
|
const jwt = this.adminforth.auth.issueJWT({sessionId, adminUserPk: adminUser.pk}, 'auto2FA', '5m');
|
|
174
|
+
const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
|
|
175
|
+
|
|
163
176
|
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
|
|
164
|
-
|
|
177
|
+
|
|
178
|
+
const result = await resultPromise;
|
|
179
|
+
|
|
165
180
|
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
|
|
181
|
+
|
|
166
182
|
return result;
|
|
167
183
|
}
|
|
168
184
|
|
|
@@ -1127,4 +1143,4 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
1127
1143
|
}
|
|
1128
1144
|
});
|
|
1129
1145
|
}
|
|
1130
|
-
}
|
|
1146
|
+
}
|