@adminforth/two-factors-auth 1.0.29 → 1.0.30

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/Changelog.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [1.0.20] - 2024-12-05
2
+
3
+ ### Added
4
+
5
+ - Added usersFilterToAllowSkipSetup to TwoFactorsAuthPlugin to allow skip 2FA setup for specific users
6
+
1
7
  ## [1.0.19] - 2024-12-02
2
8
 
3
9
  ### Fixed
@@ -28,7 +28,7 @@
28
28
  />
29
29
  </div>
30
30
  <!-- <Vue2FACodeInput v-model="code"/> -->
31
- <Button @click="() => {router.push('./login')}" class="w-full">Cancel</Button>
31
+ <LinkButton to="/login" class="w-full">Back to login</LinkButton>
32
32
  </div>
33
33
  </div>
34
34
  </div>
@@ -49,7 +49,7 @@
49
49
  import { callAdminForthApi, loadFile } from '@/utils';
50
50
  import { useRouter } from 'vue-router';
51
51
  import { showErrorTost } from '@/composables/useFrontendApi';
52
- import { Button } from '@/afcl';
52
+ import { LinkButton } from '@/afcl';
53
53
  import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
54
54
  import VOtpInput from "vue3-otp-input";
55
55
 
@@ -22,19 +22,12 @@
22
22
  <div class="w-full max-w-[46rem]">
23
23
  <div class="relative">
24
24
  <label for="npm-install-copy-text" class="sr-only">Label</label>
25
- <input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-20" :value="totp.newSecret" readonly>
25
+ <input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-12" :value="totp.newSecret" readonly>
26
26
  <button @click="onCopyClick" data-copy-to-clipboard-target="npm-install-copy-text" class="absolute end-2.5 top-1/2 -translate-y-1/2 text-gray-900 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 rounded-lg py-2 px-2.5 inline-flex items-center justify-center bg-white border-gray-200 border">
27
- <span v-if="isCopied === false" id="default-message" class="inline-flex items-center">
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">
27
+ <span id="default-message" class="inline-flex items-center">
28
+ <svg class="w-3 h-4" 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>
32
- </span>
33
- <span v-if="isCopied === true" id="success-message" class="inline-flex items-center">
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
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
36
- </svg>
37
- <span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
38
31
  </span>
39
32
  </button>
40
33
  </div>
@@ -53,9 +46,14 @@
53
46
  />
54
47
  </div>
55
48
  <!-- <Vue2FACodeInput v-model="code" autofocus /> -->
56
- <Button @click="()=>{router.push('./login')}"
57
- class="w-full">Cancel
58
- </Button>
49
+ <div class="flex flex-row gap-2.5">
50
+ <LinkButton to="/login" class="w-full">
51
+ Back to login
52
+ </LinkButton>
53
+ <Button v-if="skipAllowed" @click="handleSkip" class="w-full">
54
+ Skip for now
55
+ </Button>
56
+ </div>
59
57
  </div>
60
58
  </div>
61
59
  </div>
@@ -75,12 +73,11 @@ import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbi
75
73
  import { callAdminForthApi, loadFile } from '@/utils';
76
74
  import { useRouter } from 'vue-router';
77
75
  import { showErrorTost } from '@/composables/useFrontendApi';
78
- import { Button } from '@/afcl';
76
+ import { Button, LinkButton } from '@/afcl';
79
77
  import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
80
78
  import VOtpInput from "vue3-otp-input";
81
79
 
82
80
  const code = ref(null);
83
- const isCopied = ref<boolean>(false);
84
81
  const handleOnComplete = (value) => {
85
82
  sendCode(value);
86
83
  };
@@ -91,6 +88,8 @@ const inProgress = ref(false);
91
88
  const coreStore = useCoreStore();
92
89
  const user = useUserStore();
93
90
 
91
+ const skipAllowed = ref(false);
92
+
94
93
  // use this simple function to automatically focus on the next input
95
94
 
96
95
  const showPw = ref(false);
@@ -122,7 +121,6 @@ function parseJwt(token) {
122
121
  }
123
122
 
124
123
  function onCopyClick(){
125
- isCopied.value = true;
126
124
  navigator.clipboard.writeText(totp.value.newSecret);
127
125
  window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
128
126
  }
@@ -136,9 +134,16 @@ onMounted(async () => {
136
134
  totpJWT.value = resp.totpJWT;
137
135
  }
138
136
 
139
-
140
137
  totp.value=parseJwt(totpJWT.value);
138
+
139
+ const allowSkipResp = await callAdminForthApi({
140
+ path: '/plugin/twofa/skip-allow',
141
+ method: 'GET',
142
+ });
141
143
 
144
+ if (allowSkipResp?.skipAllowed){
145
+ skipAllowed.value = true;
146
+ }
142
147
 
143
148
  window.addEventListener('paste', handlePaste);
144
149
  });
@@ -177,6 +182,18 @@ function handlePaste(event) {
177
182
  code.value?.fillInput(pastedText);
178
183
  }
179
184
  }
185
+
186
+ const handleSkip = async () => {
187
+ const resp = await callAdminForthApi({
188
+ method: 'POST',
189
+ path: '/plugin/twofa/confirmSetup',
190
+ });
191
+ if (resp.allowedLogin){
192
+ await user.finishLogin()
193
+ } else {
194
+ showErrorTost('Something went wrong');
195
+ }
196
+ }
180
197
  </script>
181
198
  <style>
182
199
  .otp-input {
@@ -28,7 +28,7 @@
28
28
  />
29
29
  </div>
30
30
  <!-- <Vue2FACodeInput v-model="code"/> -->
31
- <Button @click="() => {router.push('./login')}" class="w-full">Cancel</Button>
31
+ <LinkButton to="/login" class="w-full">Back to login</LinkButton>
32
32
  </div>
33
33
  </div>
34
34
  </div>
@@ -49,7 +49,7 @@
49
49
  import { callAdminForthApi, loadFile } from '@/utils';
50
50
  import { useRouter } from 'vue-router';
51
51
  import { showErrorTost } from '@/composables/useFrontendApi';
52
- import { Button } from '@/afcl';
52
+ import { LinkButton } from '@/afcl';
53
53
  import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
54
54
  import VOtpInput from "vue3-otp-input";
55
55
 
@@ -22,19 +22,12 @@
22
22
  <div class="w-full max-w-[46rem]">
23
23
  <div class="relative">
24
24
  <label for="npm-install-copy-text" class="sr-only">Label</label>
25
- <input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-20" :value="totp.newSecret" readonly>
25
+ <input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-12" :value="totp.newSecret" readonly>
26
26
  <button @click="onCopyClick" data-copy-to-clipboard-target="npm-install-copy-text" class="absolute end-2.5 top-1/2 -translate-y-1/2 text-gray-900 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 rounded-lg py-2 px-2.5 inline-flex items-center justify-center bg-white border-gray-200 border">
27
- <span v-if="isCopied === false" id="default-message" class="inline-flex items-center">
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">
27
+ <span id="default-message" class="inline-flex items-center">
28
+ <svg class="w-3 h-4" 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>
32
- </span>
33
- <span v-if="isCopied === true" id="success-message" class="inline-flex items-center">
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
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
36
- </svg>
37
- <span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
38
31
  </span>
39
32
  </button>
40
33
  </div>
@@ -53,9 +46,14 @@
53
46
  />
54
47
  </div>
55
48
  <!-- <Vue2FACodeInput v-model="code" autofocus /> -->
56
- <Button @click="()=>{router.push('./login')}"
57
- class="w-full">Cancel
58
- </Button>
49
+ <div class="flex flex-row gap-2.5">
50
+ <LinkButton to="/login" class="w-full">
51
+ Back to login
52
+ </LinkButton>
53
+ <Button v-if="skipAllowed" @click="handleSkip" class="w-full">
54
+ Skip for now
55
+ </Button>
56
+ </div>
59
57
  </div>
60
58
  </div>
61
59
  </div>
@@ -75,12 +73,11 @@ import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbi
75
73
  import { callAdminForthApi, loadFile } from '@/utils';
76
74
  import { useRouter } from 'vue-router';
77
75
  import { showErrorTost } from '@/composables/useFrontendApi';
78
- import { Button } from '@/afcl';
76
+ import { Button, LinkButton } from '@/afcl';
79
77
  import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
80
78
  import VOtpInput from "vue3-otp-input";
81
79
 
82
80
  const code = ref(null);
83
- const isCopied = ref<boolean>(false);
84
81
  const handleOnComplete = (value) => {
85
82
  sendCode(value);
86
83
  };
@@ -91,6 +88,8 @@ const inProgress = ref(false);
91
88
  const coreStore = useCoreStore();
92
89
  const user = useUserStore();
93
90
 
91
+ const skipAllowed = ref(false);
92
+
94
93
  // use this simple function to automatically focus on the next input
95
94
 
96
95
  const showPw = ref(false);
@@ -122,7 +121,6 @@ function parseJwt(token) {
122
121
  }
123
122
 
124
123
  function onCopyClick(){
125
- isCopied.value = true;
126
124
  navigator.clipboard.writeText(totp.value.newSecret);
127
125
  window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
128
126
  }
@@ -136,9 +134,16 @@ onMounted(async () => {
136
134
  totpJWT.value = resp.totpJWT;
137
135
  }
138
136
 
139
-
140
137
  totp.value=parseJwt(totpJWT.value);
138
+
139
+ const allowSkipResp = await callAdminForthApi({
140
+ path: '/plugin/twofa/skip-allow',
141
+ method: 'GET',
142
+ });
141
143
 
144
+ if (allowSkipResp?.skipAllowed){
145
+ skipAllowed.value = true;
146
+ }
142
147
 
143
148
  window.addEventListener('paste', handlePaste);
144
149
  });
@@ -177,6 +182,18 @@ function handlePaste(event) {
177
182
  code.value?.fillInput(pastedText);
178
183
  }
179
184
  }
185
+
186
+ const handleSkip = async () => {
187
+ const resp = await callAdminForthApi({
188
+ method: 'POST',
189
+ path: '/plugin/twofa/confirmSetup',
190
+ });
191
+ if (resp.allowedLogin){
192
+ await user.finishLogin()
193
+ } else {
194
+ showErrorTost('Something went wrong');
195
+ }
196
+ }
180
197
  </script>
181
198
  <style>
182
199
  .otp-input {
package/dist/index.js CHANGED
@@ -55,12 +55,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
55
55
  if (!userNeeds2FA) {
56
56
  return { body: { loginAllowed: true }, ok: true };
57
57
  }
58
+ const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
58
59
  if (!secret) {
59
60
  const tempSecret = twofactor.generateSecret({ name: brandName, account: userName });
60
61
  newSecret = tempSecret.secret;
61
62
  }
62
63
  else {
63
- const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk }, 'tempTotp', '2h');
64
+ const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
64
65
  response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
65
66
  return {
66
67
  body: {
@@ -70,7 +71,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
70
71
  ok: true
71
72
  };
72
73
  }
73
- const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk }, 'tempTotp', '2h');
74
+ const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
74
75
  response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
75
76
  return {
76
77
  body: {
@@ -108,11 +109,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
108
109
  return { status: 'error', message: 'Invalid token' };
109
110
  }
110
111
  if (decoded.newSecret) {
111
- const verified = twofactor.verifyToken(decoded.newSecret, body.code);
112
+ const verified = decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code);
112
113
  if (verified) {
113
114
  this.connectors = this.adminforth.connectors;
114
- const connector = this.connectors[this.authResource.dataSource];
115
- yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
115
+ if (!decoded.userCanSkipSetup) {
116
+ const connector = this.connectors[this.authResource.dataSource];
117
+ yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
118
+ }
116
119
  this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
117
120
  this.adminforth.auth.setAuthCookie({ response, username: decoded.userName, pk: decoded.pk });
118
121
  return { status: 'ok', allowedLogin: true };
@@ -138,5 +141,27 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
138
141
  }
139
142
  })
140
143
  });
144
+ server.endpoint({
145
+ method: "GET",
146
+ path: "/plugin/twofa/skip-allow",
147
+ noAuth: true,
148
+ handler: (_d) => __awaiter(this, [_d], void 0, function* ({ cookies }) {
149
+ var _e;
150
+ const totpTemporaryJWT = (_e = cookies.find((cookie) => cookie.key === "adminforth_totpTemporaryJWT")) === null || _e === void 0 ? void 0 : _e.value;
151
+ const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "tempTotp");
152
+ if (!decoded) {
153
+ return { status: "error", message: "Invalid token" };
154
+ }
155
+ if (!decoded.newSecret) {
156
+ return { status: "ok", skipAllowed: false };
157
+ }
158
+ else {
159
+ return {
160
+ status: "ok",
161
+ skipAllowed: decoded.userCanSkipSetup,
162
+ };
163
+ }
164
+ }),
165
+ });
141
166
  }
142
167
  }
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdminForthPlugin } from "adminforth";
1
+ import { AdminForthPlugin, Filters } from "adminforth";
2
2
  import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, BeforeLoginConfirmationFunction, IAdminForthHttpResponse } from "adminforth";
3
3
  import twofactor from 'node-2fa';
4
4
  import { PluginOptions } from "./types.js"
@@ -61,11 +61,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
61
61
  return { body:{loginAllowed: true}, ok: true}
62
62
  }
63
63
 
64
+ const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
65
+
64
66
  if (!secret){
65
67
  const tempSecret = twofactor.generateSecret({name: brandName,account: userName})
66
68
  newSecret = tempSecret.secret
67
69
  } else {
68
- const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk }, 'tempTotp', '2h');
70
+ const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk, userCanSkipSetup }, 'tempTotp', '2h');
69
71
  response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
70
72
 
71
73
  return {
@@ -76,7 +78,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
76
78
  ok: true
77
79
  }
78
80
  }
79
- const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk },'tempTotp', '2h');
81
+ const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup },'tempTotp', '2h');
80
82
  response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString() } `);
81
83
 
82
84
  return {
@@ -115,11 +117,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
115
117
  return {status:'error',message:'Invalid token'}
116
118
  }
117
119
  if (decoded.newSecret) {
118
- const verified = twofactor.verifyToken(decoded.newSecret, body.code);
119
- if (verified) {
120
+ const verified = decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code);
121
+ if (verified) {
120
122
  this.connectors = this.adminforth.connectors
121
- const connector = this.connectors[this.authResource.dataSource];
122
- await connector.updateRecord({resource:this.authResource, recordId:decoded.pk, newValues:{[this.options.twoFaSecretFieldName]: decoded.newSecret}})
123
+ if (!decoded.userCanSkipSetup) {
124
+ const connector = this.connectors[this.authResource.dataSource];
125
+ await connector.updateRecord({resource:this.authResource, recordId:decoded.pk, newValues:{[this.options.twoFaSecretFieldName]: decoded.newSecret}})
126
+ }
123
127
  this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
124
128
  this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
125
129
  return { status: 'ok', allowedLogin: true }
@@ -142,5 +146,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
142
146
  }
143
147
  }
144
148
  })
149
+ server.endpoint({
150
+ method: "GET",
151
+ path: "/plugin/twofa/skip-allow",
152
+ noAuth: true,
153
+ handler: async ({ cookies }) => {
154
+ const totpTemporaryJWT = cookies.find(
155
+ (cookie) => cookie.key === "adminforth_totpTemporaryJWT"
156
+ )?.value;
157
+ const decoded = await this.adminforth.auth.verify(
158
+ totpTemporaryJWT,
159
+ "tempTotp"
160
+ );
161
+ if (!decoded) {
162
+ return { status: "error", message: "Invalid token" };
163
+ }
164
+ if (!decoded.newSecret) {
165
+ return { status: "ok", skipAllowed: false };
166
+ } else {
167
+ return {
168
+ status: "ok",
169
+ skipAllowed: decoded.userCanSkipSetup,
170
+ };
171
+ }
172
+ },
173
+ });
145
174
  }
146
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/types.ts CHANGED
@@ -16,5 +16,13 @@ export type PluginOptions = {
16
16
  * @param adminUser
17
17
  * @returns true if 2FA should be applied to the user and false if AdminForth should not challenge the user with 2FA.
18
18
  */
19
- usersFilterToApply: (adminUser: AdminUser) => boolean;
19
+ usersFilterToApply?: (adminUser: AdminUser) => boolean;
20
+
21
+ /**
22
+ * Optional function to allow users to skip 2FA setup.
23
+ * Should return true if the user should be allowed to skip the 2FA setup and false if AdminForth should require the user to set up 2FA.
24
+ * @param adminUser
25
+ * @returns true if the user should be allowed to skip the 2FA setup and false if AdminForth should require the user to set up 2FA.
26
+ */
27
+ usersFilterToAllowSkipSetup?: (adminUser: AdminUser) => boolean;
20
28
  }
File without changes