import { computed, onScopeDispose, ref } from 'vue'; import { useLocalization } from '@/composables/useLocalization'; import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor'; /** Keep secrets within the consuming component and discard late responses after hiding them. */ export function useTwoFactorAuth() { const { t } = useLocalization(); const errors = ref([]); const manualSetupKey = ref(null); const qrCodeSvg = ref(null); const recoveryCodesList = ref([]); const loading = ref(false); let pending: AbortController | null = null; async function fetchJson(url: string, signal: AbortSignal): Promise { const response = await fetch(url, { headers: { Accept: 'application/json' }, credentials: 'same-origin', signal, }); if (!response.ok || response.redirected) { throw new Error(String(response.status)); } return response.json(); } function clearErrors(): void { errors.value = []; } function clearTwoFactorAuthData(): void { pending?.abort(); pending = null; loading.value = false; manualSetupKey.value = null; qrCodeSvg.value = null; recoveryCodesList.value = []; clearErrors(); } async function load(kind: 'setup' | 'recovery'): Promise { clearTwoFactorAuthData(); const request = new AbortController(); pending = request; loading.value = true; try { if (kind === 'setup') { const [qr, key] = await Promise.all([ fetchJson<{ svg: string }>(qrCode.url(), request.signal), fetchJson<{ secretKey: string }>( secretKey.url(), request.signal, ), ]); if (pending !== request) { return; } qrCodeSvg.value = qr.svg; manualSetupKey.value = key.secretKey; } else { const codes = await fetchJson( recoveryCodes.url(), request.signal, ); if (pending === request) { recoveryCodesList.value = codes; } } } catch (error) { if (pending === request && !request.signal.aborted) { const status = error instanceof Error ? error.message : ''; errors.value = [ t( status === '423' || status === '401' || status === '419' ? 'security.confirmAgain' : kind === 'setup' ? 'security.setupError' : 'security.recoveryError', ), ]; } } finally { if (pending === request) { pending = null; loading.value = false; } } } onScopeDispose(clearTwoFactorAuthData); return { qrCodeSvg, manualSetupKey, recoveryCodesList, errors, loading, hasSetupData: computed(() => Boolean(qrCodeSvg.value && manualSetupKey.value), ), clearSetupData: clearTwoFactorAuthData, clearRecoveryCodes: clearTwoFactorAuthData, clearTwoFactorAuthData, clearErrors, fetchSetupData: () => load('setup'), fetchRecoveryCodes: () => load('recovery'), }; }