@adminforth/two-factors-auth 4.0.1 → 4.1.0

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
@@ -14,7 +14,8 @@ custom/package-lock.json
14
14
  custom/package.json
15
15
  custom/pnpm-lock.yaml
16
16
  custom/tsconfig.json
17
+ custom/use2faApi.ts
17
18
  custom/utils.js
18
19
 
19
- sent 286,243 bytes received 229 bytes 572,944.00 bytes/sec
20
- total size is 285,337 speedup is 1.00
20
+ sent 288,076 bytes received 248 bytes 576,648.00 bytes/sec
21
+ total size is 287,093 speedup is 1.00
@@ -101,7 +101,7 @@
101
101
  <script setup lang="ts">
102
102
 
103
103
  import VOtpInput from 'vue3-otp-input';
104
- import { ref, nextTick, watch, onMounted } from 'vue';
104
+ import { ref, nextTick, watch } from 'vue';
105
105
  import { useUserStore } from '@/stores/user';
106
106
  import { useI18n } from 'vue-i18n';
107
107
  import { callAdminForthApi } from '@/utils';
@@ -110,26 +110,16 @@
110
110
  import { getPasskey } from './utils.js'
111
111
  import { useAdminforth } from '@/adminforth';
112
112
  import websocket from '@/websocket';
113
+ import { getAdminForthClientId } from '@/utils/clientId';
113
114
  import type { AdminUser } from '@/types/Common';
114
-
115
- type TwoFaConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> };
116
-
117
- declare global {
118
- interface Window {
119
- adminforthTwoFaModal: {
120
- get2FaConfirmationResult: (
121
- title?: string,
122
- verifyingCallback?: (confirmationResult: string) => Promise<boolean>
123
- ) => Promise<TwoFaConfirmationResult>;
124
- };
125
- }
126
- }
115
+ import { useTwoFactorsAuthApi, type TwoFaConfirmationResult } from './use2faApi.js';
127
116
  const props = defineProps<{
128
117
  autoFinishLogin?: boolean
129
118
  adminUser?: AdminUser
130
119
  }>();
131
120
 
132
121
  const { alert } = useAdminforth();
122
+ const twoFactorsAuthApi = useTwoFactorsAuthApi();
133
123
 
134
124
  const isAwaiting2FAResult = ref(false);
135
125
  let allowAddNewSessions = true;
@@ -150,7 +140,8 @@
150
140
  watch( props, () => {
151
141
  if (props.adminUser) {
152
142
  websocket.unsubscribeByPrefix(`/user2fa/`);
153
- websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
143
+ const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
144
+ websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
154
145
  if (!allowAddNewSessions) {
155
146
  alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
156
147
  return;
@@ -162,7 +153,7 @@
162
153
  }
163
154
  try {
164
155
  isAwaiting2FAResult.value = true;
165
- confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
156
+ confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
166
157
  } catch (error) {
167
158
  if (!isTwoFaCancel(error)) {
168
159
  console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
@@ -188,7 +179,7 @@
188
179
  }
189
180
  allowAddNewSessions = true;
190
181
  });
191
- websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
182
+ websocket.subscribe(`${user2FaTopic}/resolve`, async (data: {sessionId: string}) => {
192
183
  if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
193
184
  onCancel();
194
185
  sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
@@ -224,7 +215,6 @@
224
215
 
225
216
  const modelShow = ref(false);
226
217
  let resolveFn: ((confirmationResult: any) => void) | null = null;
227
- let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
228
218
  let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
229
219
  let rejectFn: ((err?: any) => void) | null = null;
230
220
 
@@ -233,37 +223,42 @@
233
223
  }
234
224
 
235
225
 
236
- window.adminforthTwoFaModal = {
237
- get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean>) =>
238
- new Promise((resolve, reject) => {
239
- (async () => {
240
- if (modelShow.value) {
241
- throw new Error(t('Modal is already open'));
242
- }
243
- const skipAllowModal = await checkIfSkipAllowModal();
244
- if (skipAllowModal) {
245
- resolve({ code: "123456" }); // dummy code
246
- return;
247
- }
248
- await checkIfUserHasPasskeys();
249
- if (title) {
250
- customDialogTitle.value = title;
251
- }
252
- resolveFn = resolve;
253
- rejectFn = reject;
254
- verifyFn = verifyingCallback ?? null;
255
- modelShow.value = true;
256
- if (modalMode.value === 'totp') {
257
- await addEventListenerForOTPInput();
258
- }
259
- })().catch((error) => {
260
- if (!isTwoFaCancel(error)) {
261
- console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
262
- }
263
- reject(error);
264
- });
265
- }),
266
- };
226
+ function get2FaConfirmationResult(
227
+ title?: string,
228
+ verifyingCallback?: (confirmationResult: string) => Promise<boolean> | boolean
229
+ ): Promise<TwoFaConfirmationResult> {
230
+ return new Promise((resolve, reject) => {
231
+ (async () => {
232
+ if (modelShow.value) {
233
+ throw new Error(t('Modal is already open'));
234
+ }
235
+ const skipAllowModal = await checkIfSkipAllowModal();
236
+ if (skipAllowModal) {
237
+ resolve({ mode: 'totp', result: '123456' });
238
+ return;
239
+ }
240
+ await checkIfUserHasPasskeys();
241
+ if (title) {
242
+ customDialogTitle.value = title;
243
+ }
244
+ resolveFn = resolve;
245
+ rejectFn = reject;
246
+ verifyFn = verifyingCallback ?? null;
247
+ modelShow.value = true;
248
+ if (modalMode.value === 'totp') {
249
+ await addEventListenerForOTPInput();
250
+ }
251
+ })().catch((error) => {
252
+ if (!isTwoFaCancel(error)) {
253
+ console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
254
+ }
255
+ reject(error);
256
+ });
257
+ });
258
+ }
259
+
260
+ twoFactorsAuthApi.setGet2FaConfirmationResultHandler(get2FaConfirmationResult);
261
+ twoFactorsAuthApi.initGlobalApi();
267
262
 
268
263
  const { t } = useI18n();
269
264
  const user = useUserStore();
@@ -274,15 +269,19 @@
274
269
  const doesUserHavePasskeys = ref(false);
275
270
  const modalMode = ref<"totp" | "passkey">("totp");
276
271
  const isLoading = ref(false);
272
+ const isFetchingPasskey = ref(false);
277
273
  const customDialogTitle = ref("");
278
274
 
279
275
  async function usePasskeyButtonClick() {
276
+ isFetchingPasskey.value = true;
280
277
  let passkeyData;
281
278
  try {
282
279
  passkeyData = await getPasskey();
283
280
  } catch (error) {
284
281
  onCancel();
285
282
  return null;
283
+ } finally {
284
+ isFetchingPasskey.value = false;
286
285
  }
287
286
  modelShow.value = false;
288
287
  const dataToReturn = {
@@ -291,7 +290,7 @@
291
290
  }
292
291
  customDialogTitle.value = "";
293
292
  removeEventListenerForOTPInput();
294
- resolveFn(dataToReturn);
293
+ resolveFn?.(dataToReturn);
295
294
  }
296
295
 
297
296
  function tagOtpInputs() {
@@ -352,7 +351,7 @@
352
351
  bindValue.value = '';
353
352
  confirmationResult.value?.clearInput();
354
353
  removeEventListenerForOTPInput();
355
- rejectFn("Cancel");
354
+ rejectFn?.("Cancel");
356
355
  emit('rejected', new Error('cancelled'));
357
356
  emit('closed');
358
357
  }
@@ -438,10 +437,10 @@
438
437
  ((firstEmpty || inputs[0]) as HTMLInputElement).focus();
439
438
  }
440
439
 
441
- function handleGlobalFocusIn(event) {
440
+ function handleGlobalFocusIn(event: FocusEvent) {
442
441
  const inputs = getOtpInputs();
443
442
  if (!inputs.length) return;
444
- const target = event.target;
443
+ const target = event.target as Element | null;
445
444
  if (!target) return;
446
445
  if (!inputs.includes(target)) {
447
446
  requestAnimationFrame(() => {
@@ -133,11 +133,13 @@
133
133
  import utc from 'dayjs/plugin/utc';
134
134
  import timezone from 'dayjs/plugin/timezone';
135
135
  import { useCoreStore } from '@/stores/core';
136
+ import { useTwoFactorsAuth } from './use2faApi';
136
137
 
137
138
  const { t } = useI18n();
138
139
  dayjs.extend(utc);
139
140
  dayjs.extend(timezone);
140
141
  const coreStore = useCoreStore();
142
+ const twoFactorsAuth = useTwoFactorsAuth();
141
143
 
142
144
  interface Passkey {
143
145
  id: string;
@@ -176,8 +178,9 @@
176
178
  async function startAddPasskey() {
177
179
  isFetchingPasskey.value = true;
178
180
  try {
179
- const confirmationResult = await window.adminforthTwoFaModal
180
- .get2FaConfirmationResult(t('To add passkey first verify yourself'));
181
+ const confirmationResult = await twoFactorsAuth.get2FaConfirmationResult(
182
+ t('To add passkey first verify yourself')
183
+ );
181
184
 
182
185
  if (!confirmationResult) return;
183
186
 
@@ -227,7 +230,9 @@
227
230
  body: { mode: addPasskeyMode.value, confirmationResult },
228
231
  });
229
232
  if (!response.ok) return { error: response.error ?? t('Verification failed') };
233
+ console.log('Passkey challenge response:', response.data);
230
234
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(response.data);
235
+ console.log('Parsed PublicKeyCredentialCreationOptions:', options);
231
236
  return { options, challengeId: response.challengeId };
232
237
  } catch (error) {
233
238
  console.error('Error requesting passkey challenge:', error);
@@ -0,0 +1,52 @@
1
+ import { ref } from 'vue';
2
+ import { defineStore } from 'pinia';
3
+
4
+ export type TwoFaConfirmationResult =
5
+ | { mode: 'totp'; result: string }
6
+ | { mode: 'passkey'; result: Record<string, any> };
7
+
8
+ type VerifyFn = (confirmationResult: string) => Promise<boolean> | boolean;
9
+ type Get2FaConfirmationResultHandler = (
10
+ title?: string,
11
+ verifyingCallback?: VerifyFn
12
+ ) => Promise<TwoFaConfirmationResult>;
13
+
14
+ declare global {
15
+ interface Window {
16
+ adminforthTwoFaModal: {
17
+ get2FaConfirmationResult: Get2FaConfirmationResultHandler;
18
+ };
19
+ }
20
+ }
21
+
22
+ export const useTwoFactorsAuthApi = defineStore('twoFactorsAuthApi', () => {
23
+ const get2FaConfirmationResultHandler = ref<Get2FaConfirmationResultHandler | null>(null);
24
+
25
+ function setGet2FaConfirmationResultHandler(handler: Get2FaConfirmationResultHandler | null) {
26
+ get2FaConfirmationResultHandler.value = handler;
27
+ }
28
+
29
+ async function get2FaConfirmationResult(
30
+ title?: string,
31
+ verifyingCallback?: VerifyFn
32
+ ): Promise<TwoFaConfirmationResult> {
33
+ if (!get2FaConfirmationResultHandler.value) {
34
+ throw new Error('Two factors auth modal API is not initialized');
35
+ }
36
+ return get2FaConfirmationResultHandler.value(title, verifyingCallback);
37
+ }
38
+
39
+ function initGlobalApi() {
40
+ window.adminforthTwoFaModal = {
41
+ get2FaConfirmationResult,
42
+ };
43
+ }
44
+
45
+ return {
46
+ get2FaConfirmationResult,
47
+ setGet2FaConfirmationResultHandler,
48
+ initGlobalApi,
49
+ };
50
+ });
51
+
52
+ export const useTwoFactorsAuth = useTwoFactorsAuthApi;
@@ -101,7 +101,7 @@
101
101
  <script setup lang="ts">
102
102
 
103
103
  import VOtpInput from 'vue3-otp-input';
104
- import { ref, nextTick, watch, onMounted } from 'vue';
104
+ import { ref, nextTick, watch } from 'vue';
105
105
  import { useUserStore } from '@/stores/user';
106
106
  import { useI18n } from 'vue-i18n';
107
107
  import { callAdminForthApi } from '@/utils';
@@ -110,26 +110,16 @@
110
110
  import { getPasskey } from './utils.js'
111
111
  import { useAdminforth } from '@/adminforth';
112
112
  import websocket from '@/websocket';
113
+ import { getAdminForthClientId } from '@/utils/clientId';
113
114
  import type { AdminUser } from '@/types/Common';
114
-
115
- type TwoFaConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> };
116
-
117
- declare global {
118
- interface Window {
119
- adminforthTwoFaModal: {
120
- get2FaConfirmationResult: (
121
- title?: string,
122
- verifyingCallback?: (confirmationResult: string) => Promise<boolean>
123
- ) => Promise<TwoFaConfirmationResult>;
124
- };
125
- }
126
- }
115
+ import { useTwoFactorsAuthApi, type TwoFaConfirmationResult } from './use2faApi.js';
127
116
  const props = defineProps<{
128
117
  autoFinishLogin?: boolean
129
118
  adminUser?: AdminUser
130
119
  }>();
131
120
 
132
121
  const { alert } = useAdminforth();
122
+ const twoFactorsAuthApi = useTwoFactorsAuthApi();
133
123
 
134
124
  const isAwaiting2FAResult = ref(false);
135
125
  let allowAddNewSessions = true;
@@ -150,7 +140,8 @@
150
140
  watch( props, () => {
151
141
  if (props.adminUser) {
152
142
  websocket.unsubscribeByPrefix(`/user2fa/`);
153
- websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
143
+ const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
144
+ websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
154
145
  if (!allowAddNewSessions) {
155
146
  alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
156
147
  return;
@@ -162,7 +153,7 @@
162
153
  }
163
154
  try {
164
155
  isAwaiting2FAResult.value = true;
165
- confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
156
+ confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
166
157
  } catch (error) {
167
158
  if (!isTwoFaCancel(error)) {
168
159
  console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
@@ -188,7 +179,7 @@
188
179
  }
189
180
  allowAddNewSessions = true;
190
181
  });
191
- websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
182
+ websocket.subscribe(`${user2FaTopic}/resolve`, async (data: {sessionId: string}) => {
192
183
  if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
193
184
  onCancel();
194
185
  sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
@@ -224,7 +215,6 @@
224
215
 
225
216
  const modelShow = ref(false);
226
217
  let resolveFn: ((confirmationResult: any) => void) | null = null;
227
- let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
228
218
  let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
229
219
  let rejectFn: ((err?: any) => void) | null = null;
230
220
 
@@ -233,37 +223,42 @@
233
223
  }
234
224
 
235
225
 
236
- window.adminforthTwoFaModal = {
237
- get2FaConfirmationResult: (title?: string, verifyingCallback?: (confirmationResult: string) => Promise<boolean>) =>
238
- new Promise((resolve, reject) => {
239
- (async () => {
240
- if (modelShow.value) {
241
- throw new Error(t('Modal is already open'));
242
- }
243
- const skipAllowModal = await checkIfSkipAllowModal();
244
- if (skipAllowModal) {
245
- resolve({ code: "123456" }); // dummy code
246
- return;
247
- }
248
- await checkIfUserHasPasskeys();
249
- if (title) {
250
- customDialogTitle.value = title;
251
- }
252
- resolveFn = resolve;
253
- rejectFn = reject;
254
- verifyFn = verifyingCallback ?? null;
255
- modelShow.value = true;
256
- if (modalMode.value === 'totp') {
257
- await addEventListenerForOTPInput();
258
- }
259
- })().catch((error) => {
260
- if (!isTwoFaCancel(error)) {
261
- console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
262
- }
263
- reject(error);
264
- });
265
- }),
266
- };
226
+ function get2FaConfirmationResult(
227
+ title?: string,
228
+ verifyingCallback?: (confirmationResult: string) => Promise<boolean> | boolean
229
+ ): Promise<TwoFaConfirmationResult> {
230
+ return new Promise((resolve, reject) => {
231
+ (async () => {
232
+ if (modelShow.value) {
233
+ throw new Error(t('Modal is already open'));
234
+ }
235
+ const skipAllowModal = await checkIfSkipAllowModal();
236
+ if (skipAllowModal) {
237
+ resolve({ mode: 'totp', result: '123456' });
238
+ return;
239
+ }
240
+ await checkIfUserHasPasskeys();
241
+ if (title) {
242
+ customDialogTitle.value = title;
243
+ }
244
+ resolveFn = resolve;
245
+ rejectFn = reject;
246
+ verifyFn = verifyingCallback ?? null;
247
+ modelShow.value = true;
248
+ if (modalMode.value === 'totp') {
249
+ await addEventListenerForOTPInput();
250
+ }
251
+ })().catch((error) => {
252
+ if (!isTwoFaCancel(error)) {
253
+ console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
254
+ }
255
+ reject(error);
256
+ });
257
+ });
258
+ }
259
+
260
+ twoFactorsAuthApi.setGet2FaConfirmationResultHandler(get2FaConfirmationResult);
261
+ twoFactorsAuthApi.initGlobalApi();
267
262
 
268
263
  const { t } = useI18n();
269
264
  const user = useUserStore();
@@ -274,15 +269,19 @@
274
269
  const doesUserHavePasskeys = ref(false);
275
270
  const modalMode = ref<"totp" | "passkey">("totp");
276
271
  const isLoading = ref(false);
272
+ const isFetchingPasskey = ref(false);
277
273
  const customDialogTitle = ref("");
278
274
 
279
275
  async function usePasskeyButtonClick() {
276
+ isFetchingPasskey.value = true;
280
277
  let passkeyData;
281
278
  try {
282
279
  passkeyData = await getPasskey();
283
280
  } catch (error) {
284
281
  onCancel();
285
282
  return null;
283
+ } finally {
284
+ isFetchingPasskey.value = false;
286
285
  }
287
286
  modelShow.value = false;
288
287
  const dataToReturn = {
@@ -291,7 +290,7 @@
291
290
  }
292
291
  customDialogTitle.value = "";
293
292
  removeEventListenerForOTPInput();
294
- resolveFn(dataToReturn);
293
+ resolveFn?.(dataToReturn);
295
294
  }
296
295
 
297
296
  function tagOtpInputs() {
@@ -352,7 +351,7 @@
352
351
  bindValue.value = '';
353
352
  confirmationResult.value?.clearInput();
354
353
  removeEventListenerForOTPInput();
355
- rejectFn("Cancel");
354
+ rejectFn?.("Cancel");
356
355
  emit('rejected', new Error('cancelled'));
357
356
  emit('closed');
358
357
  }
@@ -438,10 +437,10 @@
438
437
  ((firstEmpty || inputs[0]) as HTMLInputElement).focus();
439
438
  }
440
439
 
441
- function handleGlobalFocusIn(event) {
440
+ function handleGlobalFocusIn(event: FocusEvent) {
442
441
  const inputs = getOtpInputs();
443
442
  if (!inputs.length) return;
444
- const target = event.target;
443
+ const target = event.target as Element | null;
445
444
  if (!target) return;
446
445
  if (!inputs.includes(target)) {
447
446
  requestAnimationFrame(() => {
@@ -133,11 +133,13 @@
133
133
  import utc from 'dayjs/plugin/utc';
134
134
  import timezone from 'dayjs/plugin/timezone';
135
135
  import { useCoreStore } from '@/stores/core';
136
+ import { useTwoFactorsAuth } from './use2faApi';
136
137
 
137
138
  const { t } = useI18n();
138
139
  dayjs.extend(utc);
139
140
  dayjs.extend(timezone);
140
141
  const coreStore = useCoreStore();
142
+ const twoFactorsAuth = useTwoFactorsAuth();
141
143
 
142
144
  interface Passkey {
143
145
  id: string;
@@ -176,8 +178,9 @@
176
178
  async function startAddPasskey() {
177
179
  isFetchingPasskey.value = true;
178
180
  try {
179
- const confirmationResult = await window.adminforthTwoFaModal
180
- .get2FaConfirmationResult(t('To add passkey first verify yourself'));
181
+ const confirmationResult = await twoFactorsAuth.get2FaConfirmationResult(
182
+ t('To add passkey first verify yourself')
183
+ );
181
184
 
182
185
  if (!confirmationResult) return;
183
186
 
@@ -227,7 +230,9 @@
227
230
  body: { mode: addPasskeyMode.value, confirmationResult },
228
231
  });
229
232
  if (!response.ok) return { error: response.error ?? t('Verification failed') };
233
+ console.log('Passkey challenge response:', response.data);
230
234
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(response.data);
235
+ console.log('Parsed PublicKeyCredentialCreationOptions:', options);
231
236
  return { options, challengeId: response.challengeId };
232
237
  } catch (error) {
233
238
  console.error('Error requesting passkey challenge:', error);
@@ -0,0 +1,52 @@
1
+ import { ref } from 'vue';
2
+ import { defineStore } from 'pinia';
3
+
4
+ export type TwoFaConfirmationResult =
5
+ | { mode: 'totp'; result: string }
6
+ | { mode: 'passkey'; result: Record<string, any> };
7
+
8
+ type VerifyFn = (confirmationResult: string) => Promise<boolean> | boolean;
9
+ type Get2FaConfirmationResultHandler = (
10
+ title?: string,
11
+ verifyingCallback?: VerifyFn
12
+ ) => Promise<TwoFaConfirmationResult>;
13
+
14
+ declare global {
15
+ interface Window {
16
+ adminforthTwoFaModal: {
17
+ get2FaConfirmationResult: Get2FaConfirmationResultHandler;
18
+ };
19
+ }
20
+ }
21
+
22
+ export const useTwoFactorsAuthApi = defineStore('twoFactorsAuthApi', () => {
23
+ const get2FaConfirmationResultHandler = ref<Get2FaConfirmationResultHandler | null>(null);
24
+
25
+ function setGet2FaConfirmationResultHandler(handler: Get2FaConfirmationResultHandler | null) {
26
+ get2FaConfirmationResultHandler.value = handler;
27
+ }
28
+
29
+ async function get2FaConfirmationResult(
30
+ title?: string,
31
+ verifyingCallback?: VerifyFn
32
+ ): Promise<TwoFaConfirmationResult> {
33
+ if (!get2FaConfirmationResultHandler.value) {
34
+ throw new Error('Two factors auth modal API is not initialized');
35
+ }
36
+ return get2FaConfirmationResultHandler.value(title, verifyingCallback);
37
+ }
38
+
39
+ function initGlobalApi() {
40
+ window.adminforthTwoFaModal = {
41
+ get2FaConfirmationResult,
42
+ };
43
+ }
44
+
45
+ return {
46
+ get2FaConfirmationResult,
47
+ setGet2FaConfirmationResultHandler,
48
+ initGlobalApi,
49
+ };
50
+ });
51
+
52
+ export const useTwoFactorsAuth = useTwoFactorsAuthApi;
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { AdminForthPlugin, suggestIfTypo } from "adminforth";
11
+ import * as AdminForthRuntime from "adminforth";
11
12
  import { PasskeyService } from "./services/passkeyService.js";
12
13
  import { TotpService } from "./services/totpService.js";
13
14
  import { verifyMfaConfirmation } from "./services/verifyMfaConfirmation.js";
@@ -19,6 +20,7 @@ import { createPasskeyHandlers } from "./handlers/createPasskeyHandlers.js";
19
20
  import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
20
21
  import { parsePeriod } from "./utils/parsePeriod.js";
21
22
  import crypto from 'crypto';
23
+ const getRequestWebsocketClientId = AdminForthRuntime.getRequestWebsocketClientId;
22
24
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
23
25
  constructor(options) {
24
26
  super(options, import.meta.url);
@@ -77,12 +79,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
77
79
  }
78
80
  verifyAuto(adminUser) {
79
81
  return __awaiter(this, void 0, void 0, function* () {
82
+ const websocketClientId = getRequestWebsocketClientId === null || getRequestWebsocketClientId === void 0 ? void 0 : getRequestWebsocketClientId();
83
+ if (!websocketClientId) {
84
+ return { ok: false, error: '2FA auto verification requires an active browser request context' };
85
+ }
80
86
  const sessionId = crypto.randomUUID();
81
87
  const jwt = this.adminforth.auth.issueJWT({ sessionId, adminUserPk: adminUser.pk }, 'auto2FA', '5m');
82
88
  const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
83
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
89
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt });
84
90
  const result = yield resultPromise;
85
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
91
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}/resolve`, { sessionId: jwt });
86
92
  return result;
87
93
  });
88
94
  }
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AdminForthPlugin, suggestIfTypo, HttpExtra } from "adminforth";
2
+ import * as AdminForthRuntime from "adminforth";
2
3
  import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, IAdminForthHttpResponse } from "adminforth";
3
4
  import { PluginOptions } from "./types.js"
4
5
  import { PasskeyService } from "./services/passkeyService.js";
@@ -13,6 +14,10 @@ import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
13
14
  import { parsePeriod } from "./utils/parsePeriod.js";
14
15
  import crypto from 'crypto';
15
16
 
17
+ const getRequestWebsocketClientId = (AdminForthRuntime as typeof AdminForthRuntime & {
18
+ getRequestWebsocketClientId?: () => string | undefined;
19
+ }).getRequestWebsocketClientId;
20
+
16
21
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
17
22
  options: PluginOptions;
18
23
  adminforth: IAdminForth;
@@ -87,15 +92,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
87
92
  }
88
93
 
89
94
  public async verifyAuto(adminUser: AdminUser) {
95
+ const websocketClientId = getRequestWebsocketClientId?.();
96
+ if (!websocketClientId) {
97
+ return { ok: false, error: '2FA auto verification requires an active browser request context' };
98
+ }
90
99
  const sessionId = crypto.randomUUID();
91
100
  const jwt = this.adminforth.auth.issueJWT({sessionId, adminUserPk: adminUser.pk}, 'auto2FA', '5m');
92
101
  const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
93
102
 
94
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
103
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt });
95
104
 
96
105
  const result = await resultPromise;
97
106
 
98
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
107
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}/resolve`, { sessionId: jwt });
99
108
 
100
109
  return result;
101
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,4 @@
1
+ allowBuilds:
2
+ adminforth: true
3
+ minimumReleaseAgeExclude:
4
+ - adminforth@3.0.1