mirror of https://github.com/sylv/micro.git
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
import { useMutation, useQuery } from '@apollo/client';
|
|
import clsx from 'clsx';
|
|
import { QRCodeSVG } from 'qrcode.react';
|
|
import { FC, Fragment, useCallback, useMemo } from 'react';
|
|
import { FiChevronLeft, FiChevronRight, FiCopy, FiDownload } from 'react-icons/fi';
|
|
import { graphql } from '../../../@generated';
|
|
import { Button, ButtonStyle } from '../../../components/button';
|
|
import { Container } from '../../../components/container';
|
|
import { Error } from '../../../components/error';
|
|
import { OtpInput } from '../../../components/input/otp';
|
|
import { PageLoader } from '../../../components/page-loader';
|
|
import { Steps } from '../../../components/steps';
|
|
import { useToasts } from '../../../components/toast';
|
|
import { navigate } from '../../../helpers/routing';
|
|
import { useAsync } from '../../../hooks/useAsync';
|
|
import { useQueryState } from '../../../hooks/useQueryState';
|
|
|
|
const GenerateOtp = graphql(`
|
|
query GenerateOTP {
|
|
generateOTP {
|
|
recoveryCodes
|
|
qrauthUrl
|
|
secret
|
|
}
|
|
}
|
|
`);
|
|
|
|
const ConfirmOTP = graphql(`
|
|
mutation ConfirmOTP($otpCode: String!) {
|
|
confirmOTP(otpCode: $otpCode)
|
|
}
|
|
`);
|
|
|
|
export const Page: FC = () => {
|
|
const result = useQuery(GenerateOtp);
|
|
const createToast = useToasts();
|
|
const [currentStep, setCurrentStep] = useQueryState('step', 0, Number);
|
|
const [confirmOtp] = useMutation(ConfirmOTP);
|
|
|
|
const copyable = useMemo(() => {
|
|
if (!result.data) return;
|
|
const prefix = `Use these in place of OTP codes in emergency situations. \nEach code will only work once. If you are close to running out, you should generate new codes.\n\n`;
|
|
const body = result.data.generateOTP.recoveryCodes.join('\n');
|
|
return prefix + body;
|
|
}, [result.data]);
|
|
|
|
const download = useCallback(() => {
|
|
if (!copyable) return;
|
|
const element = document.createElement('a');
|
|
const file = new Blob([copyable], { type: 'text/plain' });
|
|
element.href = URL.createObjectURL(file);
|
|
element.download = `${window.location.host}-recovery-codes.txt`;
|
|
document.body.append(element);
|
|
element.click();
|
|
}, [copyable]);
|
|
|
|
const copy = useCallback(() => {
|
|
if (!copyable) return;
|
|
navigator.clipboard.writeText(copyable);
|
|
createToast({
|
|
text: 'Copied recovery codes!',
|
|
});
|
|
}, [createToast, copyable]);
|
|
|
|
const [confirm, confirming] = useAsync(async (otpCode: string) => {
|
|
try {
|
|
await confirmOtp({ variables: { otpCode } });
|
|
createToast({ text: 'Successfully enabled 2FA!' });
|
|
navigate('/dashboard', { overwriteLastHistoryEntry: true });
|
|
} catch (error: any) {
|
|
if (error.message) {
|
|
createToast({
|
|
text: error.message,
|
|
error: true,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (result.loading) return <PageLoader />;
|
|
if (!result.data) return <Error error={result.error} />;
|
|
|
|
return (
|
|
<Container>
|
|
<Steps steps={['Backup', 'Scan', 'Verify']} stepIndex={currentStep} />
|
|
<div className="max-w-xl mx-auto mt-16">
|
|
<div className="flex gap-8 md:h-[20em] flex flex-col-reverse md:grid md:grid-cols-6">
|
|
{currentStep === 0 && (
|
|
<Fragment>
|
|
<div className="bg-gray-900 p-4 h-min rounded-xl font-mono whitespace-pre truncate text-gray-600 col-span-2 select-none text-center">
|
|
{result.data.generateOTP.recoveryCodes.join('\n')}
|
|
</div>
|
|
<div className="col-span-4">
|
|
<h2>Store your backup codes</h2>
|
|
<p className="max-w-xl text-gray-500 text-sm mt-2">
|
|
You should store these codes somewhere safe. If you lose access to your authenticator, you can use
|
|
these codes to access your account. Each code will only work once.
|
|
</p>
|
|
<div className="flex mt-8 gap-2">
|
|
<Button style={ButtonStyle.Secondary} className="w-auto" onClick={download}>
|
|
<FiDownload className="h-3.5 w-3.5" />
|
|
Download Codes
|
|
</Button>
|
|
<Button style={ButtonStyle.Secondary} className="w-auto" onClick={copy}>
|
|
<FiCopy className="h-3.5 w-3.5" />
|
|
Copy Codes
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
{currentStep === 1 && (
|
|
<Fragment>
|
|
<div className="p-4 rounded-xl h-min bg-white col-span-2">
|
|
<QRCodeSVG className="w-full h-full" size={128} value={result.data.generateOTP.qrauthUrl} />
|
|
</div>
|
|
<div className="col-span-4">
|
|
<h2>Scan the QR code</h2>
|
|
<div className="max-w-xl text-gray-500 text-sm mt-2 space-y-2">
|
|
<p>
|
|
Scan this QR code with your authenticator app. You can use any authenticator app that supports TOTP,
|
|
such as Google Authenticator and Authy.
|
|
</p>
|
|
<p className="text-xs text-gray-600">
|
|
If you can't scan the QR code, you can enter the code{' '}
|
|
<code className="text-purple-400">{result.data.generateOTP.secret}</code> manually.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
{currentStep === 2 && (
|
|
<Fragment>
|
|
<div className="col-span-full">
|
|
<h2>Verify your code</h2>
|
|
<p className="max-w-xl text-gray-500 text-sm mt-2">
|
|
Enter the code from your authenticator app to verify that it is working.
|
|
</p>
|
|
<div className="flex mt-4 gap-2">
|
|
<OtpInput loading={confirming} onCode={confirm} />
|
|
</div>
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
<div className="w-full flex justify-between items-center mt-8">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCurrentStep((prev) => prev - 1)}
|
|
disabled={currentStep === 0}
|
|
className={clsx(
|
|
`text-gray-400 flex items-center gap-1 hover:underline`,
|
|
currentStep === 0 && 'opacity-0 pointer-events-none',
|
|
)}
|
|
>
|
|
<FiChevronLeft className="h-4 w-4" /> Back
|
|
</button>
|
|
<Button
|
|
onClick={() => setCurrentStep((prev) => prev + 1)}
|
|
disabled={currentStep === 2}
|
|
className="w-auto ml-auto"
|
|
>
|
|
Next <FiChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
);
|
|
};
|