111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
|
|
import type { TwoFactorSecretKey, TwoFactorSetupData } from '@/types';
|
|
|
|
export type UseTwoFactorAuthReturn = {
|
|
qrCodeSvg: string | null;
|
|
manualSetupKey: string | null;
|
|
recoveryCodesList: string[];
|
|
hasSetupData: boolean;
|
|
errors: string[];
|
|
clearErrors: () => void;
|
|
clearSetupData: () => void;
|
|
fetchQrCode: () => Promise<void>;
|
|
fetchSetupKey: () => Promise<void>;
|
|
fetchSetupData: () => Promise<void>;
|
|
fetchRecoveryCodes: () => Promise<void>;
|
|
};
|
|
|
|
export const OTP_MAX_LENGTH = 6;
|
|
|
|
const fetchJson = async <T>(url: string): Promise<T> => {
|
|
const response = await fetch(url, {
|
|
headers: { Accept: 'application/json' },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
|
const [qrCodeSvg, setQrCodeSvg] = useState<string | null>(null);
|
|
const [manualSetupKey, setManualSetupKey] = useState<string | null>(null);
|
|
const [recoveryCodesList, setRecoveryCodesList] = useState<string[]>([]);
|
|
const [errors, setErrors] = useState<string[]>([]);
|
|
|
|
const hasSetupData = useMemo<boolean>(
|
|
() => qrCodeSvg !== null && manualSetupKey !== null,
|
|
[qrCodeSvg, manualSetupKey],
|
|
);
|
|
|
|
const fetchQrCode = useCallback(async (): Promise<void> => {
|
|
try {
|
|
const { svg } = await fetchJson<TwoFactorSetupData>(qrCode.url());
|
|
setQrCodeSvg(svg);
|
|
} catch {
|
|
setErrors((prev) => [...prev, 'Failed to fetch QR code']);
|
|
setQrCodeSvg(null);
|
|
}
|
|
}, []);
|
|
|
|
const fetchSetupKey = useCallback(async (): Promise<void> => {
|
|
try {
|
|
const { secretKey: key } = await fetchJson<TwoFactorSecretKey>(
|
|
secretKey.url(),
|
|
);
|
|
setManualSetupKey(key);
|
|
} catch {
|
|
setErrors((prev) => [...prev, 'Failed to fetch a setup key']);
|
|
setManualSetupKey(null);
|
|
}
|
|
}, []);
|
|
|
|
const clearErrors = useCallback((): void => {
|
|
setErrors([]);
|
|
}, []);
|
|
|
|
const clearSetupData = useCallback((): void => {
|
|
setManualSetupKey(null);
|
|
setQrCodeSvg(null);
|
|
clearErrors();
|
|
}, [clearErrors]);
|
|
|
|
const fetchRecoveryCodes = useCallback(async (): Promise<void> => {
|
|
try {
|
|
clearErrors();
|
|
const codes = await fetchJson<string[]>(recoveryCodes.url());
|
|
setRecoveryCodesList(codes);
|
|
} catch {
|
|
setErrors((prev) => [...prev, 'Failed to fetch recovery codes']);
|
|
setRecoveryCodesList([]);
|
|
}
|
|
}, [clearErrors]);
|
|
|
|
const fetchSetupData = useCallback(async (): Promise<void> => {
|
|
try {
|
|
clearErrors();
|
|
await Promise.all([fetchQrCode(), fetchSetupKey()]);
|
|
} catch {
|
|
setQrCodeSvg(null);
|
|
setManualSetupKey(null);
|
|
}
|
|
}, [clearErrors, fetchQrCode, fetchSetupKey]);
|
|
|
|
return {
|
|
qrCodeSvg,
|
|
manualSetupKey,
|
|
recoveryCodesList,
|
|
hasSetupData,
|
|
errors,
|
|
clearErrors,
|
|
clearSetupData,
|
|
fetchQrCode,
|
|
fetchSetupKey,
|
|
fetchSetupData,
|
|
fetchRecoveryCodes,
|
|
};
|
|
};
|