@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 CHANGED
@@ -16,5 +16,5 @@ custom/pnpm-lock.yaml
16
16
  custom/tsconfig.json
17
17
  custom/utils.js
18
18
 
19
- sent 285,463 bytes received 229 bytes 571,384.00 bytes/sec
20
- total size is 284,569 speedup is 1.00
19
+ sent 285,644 bytes received 229 bytes 571,746.00 bytes/sec
20
+ total size is 284,738 speedup is 1.00
@@ -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
- <div v-if="isLoading===false" id="authentication-modal" tabindex="-1" class="af-two-factors-confirmation overflow-y-auto overflow-x-hidden z-50 min-w-[00px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
12
- <div class="relative p-4 w-full max-w-md max-h-full">
13
- <!-- Modal content -->
14
- <div class="af-login-popup relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" :class="codeError ? 'rounded-b-none' : ''">
15
- <div class="p-8 w-full max-w-md max-h-full custom-auth-wrapper" >
16
- <div v-if="confirmationMode === 'code'" class="af-totp-confirmation">
17
- <div id="mfaCode-label" class="mx-4">{{$t('Please enter your authenticator code')}} </div>
18
- <div class="mt-4 w-full flex flex-col gap-4 justify-center" ref="otpRoot">
19
- <v-otp-input
20
- ref="code"
21
- container-class="grid grid-cols-6 gap-3 w-full"
22
- input-classes="bg-gray-50 text-center 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"
23
- :num-inputs="6"
24
- inputType="number"
25
- inputmode="numeric"
26
- :should-auto-focus="true"
27
- :should-focus-order="true"
28
- v-model:value="bindValue"
29
- @on-complete="handleOnComplete"
30
- />
31
- <div class="flex items-center justify-between w-full">
32
- <Link v-if="confirmationMode === 'code' && doesUserHavePasskeys" :to="{ hash: '#passkey' }" class="w-max underline hover:no-underline hover:cursor-pointer text-lightPrimary whitespace-nowrap">{{$t('Use passkey')}}</Link>
33
- <Link
34
- v-if="confirmationMode === 'code'"
35
- to="/login"
36
- class="w-max"
37
- >
38
- {{$t('Back to login')}}
39
- </Link>
40
- </div>
41
- </div>
42
- </div>
43
- <div v-else class="af-passkey-confirmation flex flex-col items-center justify-center py-4 gap-6">
44
- <IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
45
- <p class="text-4xl font-semibold mb-4">{{$t('Passkey')}}</p>
46
- <p class="mb-2 max-w-[300px]">{{$t('When you are ready, authenticate using the button below')}}</p>
47
- <Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
48
- {{$t('Use passkey')}}
49
- </Button>
50
- <div v-if="confirmationMode === 'passkey'" class="max-w-sm px-6 pt-3 w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
51
- <p class="mb-3 font-normal text-gray-700 dark:text-gray-400">
52
- {{$t('Have issues with passkey?')}}
53
- <div v-if="doesUserHavePasskeys" class="flex justify-start cursor-pointer gap-2" >
54
- <Link v-if="confirmationMode === 'passkey'" :to="{ hash: '#code' }" class="underline hover:no-underline text-lightPrimary whitespace-nowrap">{{$t('use TOTP')}}</Link>
55
- <p> {{$t('or')}}</p>
56
- <Link
57
- to="/login"
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
- <div
69
- v-if="codeError"
70
- class="af-two-factors-confirmation-error relative top-full left-0 bg-red-100 text-red-700 text-sm px-2 py-2 rounded-b-lg shadow"
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
- <div v-if="isLoading===false" id="authentication-modal" tabindex="-1" class="af-two-factors-confirmation overflow-y-auto overflow-x-hidden z-50 min-w-[00px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
12
- <div class="relative p-4 w-full max-w-md max-h-full">
13
- <!-- Modal content -->
14
- <div class="af-login-popup relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black text-gray-500" :class="codeError ? 'rounded-b-none' : ''">
15
- <div class="p-8 w-full max-w-md max-h-full custom-auth-wrapper" >
16
- <div v-if="confirmationMode === 'code'" class="af-totp-confirmation">
17
- <div id="mfaCode-label" class="mx-4">{{$t('Please enter your authenticator code')}} </div>
18
- <div class="mt-4 w-full flex flex-col gap-4 justify-center" ref="otpRoot">
19
- <v-otp-input
20
- ref="code"
21
- container-class="grid grid-cols-6 gap-3 w-full"
22
- input-classes="bg-gray-50 text-center 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"
23
- :num-inputs="6"
24
- inputType="number"
25
- inputmode="numeric"
26
- :should-auto-focus="true"
27
- :should-focus-order="true"
28
- v-model:value="bindValue"
29
- @on-complete="handleOnComplete"
30
- />
31
- <div class="flex items-center justify-between w-full">
32
- <Link v-if="confirmationMode === 'code' && doesUserHavePasskeys" :to="{ hash: '#passkey' }" class="w-max underline hover:no-underline hover:cursor-pointer text-lightPrimary whitespace-nowrap">{{$t('Use passkey')}}</Link>
33
- <Link
34
- v-if="confirmationMode === 'code'"
35
- to="/login"
36
- class="w-max"
37
- >
38
- {{$t('Back to login')}}
39
- </Link>
40
- </div>
41
- </div>
42
- </div>
43
- <div v-else class="af-passkey-confirmation flex flex-col items-center justify-center py-4 gap-6">
44
- <IconShieldOutline class="w-16 h-16 text-lightPrimary dark:text-darkPrimary"/>
45
- <p class="text-4xl font-semibold mb-4">{{$t('Passkey')}}</p>
46
- <p class="mb-2 max-w-[300px]">{{$t('When you are ready, authenticate using the button below')}}</p>
47
- <Button @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
48
- {{$t('Use passkey')}}
49
- </Button>
50
- <div v-if="confirmationMode === 'passkey'" class="max-w-sm px-6 pt-3 w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
51
- <p class="mb-3 font-normal text-gray-700 dark:text-gray-400">
52
- {{$t('Have issues with passkey?')}}
53
- <div v-if="doesUserHavePasskeys" class="flex justify-start cursor-pointer gap-2" >
54
- <Link v-if="confirmationMode === 'passkey'" :to="{ hash: '#code' }" class="underline hover:no-underline text-lightPrimary whitespace-nowrap">{{$t('use TOTP')}}</Link>
55
- <p> {{$t('or')}}</p>
56
- <Link
57
- to="/login"
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
- <div
69
- v-if="codeError"
70
- class="af-two-factors-confirmation-error relative top-full left-0 bg-red-100 text-red-700 text-sm px-2 py-2 rounded-b-lg shadow"
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
- this.pending.set(id, resolve);
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 this.waitForResponse(jwt);
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
- this.pending.set(id, resolve);
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
- const result = await this.waitForResponse(jwt);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "3.2.3",
3
+ "version": "3.3.1",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",