From 5d6cdbef891b558900a0017aabe4c5090ee9b264 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu Date: Fri, 16 Feb 2024 20:46:27 +0000 Subject: [PATCH 1/5] feat: ability to download all the 2FA recovery codes --- .../forms/2fa/view-recovery-codes-dialog.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 18714332a..323bc7198 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; @@ -41,6 +41,7 @@ export type ViewRecoveryCodesDialogProps = { export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => { const { toast } = useToast(); + const [recoveryCodesUrl, setRecoveryCodesUrl] = useState(''); const { mutateAsync: viewRecoveryCodes, data: viewRecoveryCodesData } = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); @@ -62,6 +63,16 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode return 'view'; }, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]); + useEffect(() => { + if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) { + const textBlob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], { + type: 'text/plain', + }); + if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl); + setRecoveryCodesUrl(URL.createObjectURL(textBlob)); + } + }, [viewRecoveryCodesData]); + const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => { try { await viewRecoveryCodes({ password }); @@ -139,8 +150,11 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode )} -
+
+ + +
)) From 0186f2dfeda8240be2bbf9e46e158f81d70e3321 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Sat, 17 Feb 2024 13:19:03 +0530 Subject: [PATCH 2/5] feat: ability to download 2FA recovery codes --- .../web/src/components/forms/2fa/view-recovery-codes-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 323bc7198..cfdae7015 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -153,7 +153,7 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
- +
From 39c6cbf66a9cb29f711e1af3b96243c31fe143ae Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu Date: Mon, 19 Feb 2024 11:25:15 +0000 Subject: [PATCH 3/5] feat: ability to download 2FA recovery codes --- .../2fa/enable-authenticator-app-dialog.tsx | 24 +++++++++++++++---- .../forms/2fa/view-recovery-codes-dialog.tsx | 10 ++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx index 7a181c4cc..671292bde 100644 --- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; @@ -56,6 +56,7 @@ export const EnableAuthenticatorAppDialog = ({ }: EnableAuthenticatorAppDialogProps) => { const router = useRouter(); const { toast } = useToast(); + const [recoveryCodesUrl, setRecoveryCodesUrl] = useState(''); const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } = trpc.twoFactorAuthentication.setup.useMutation(); @@ -115,6 +116,16 @@ export const EnableAuthenticatorAppDialog = ({ } }; + const downloadRecoveryCodes = () => { + if (enableTwoFactorAuthenticationData && enableTwoFactorAuthenticationData.recoveryCodes) { + const textBlob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], { + type: 'text/plain', + }); + if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl); + setRecoveryCodesUrl(URL.createObjectURL(textBlob)); + } + }; + const onEnableTwoFactorAuthenticationFormSubmit = async ({ token, }: TEnableTwoFactorAuthenticationForm) => { @@ -270,10 +281,13 @@ export const EnableAuthenticatorAppDialog = ({ )} -
- +
+ + + +
)) diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index cfdae7015..797b61b84 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; @@ -63,7 +63,7 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode return 'view'; }, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]); - useEffect(() => { + const downloadRecoveryCodes = () => { if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) { const textBlob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], { type: 'text/plain', @@ -71,7 +71,7 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl); setRecoveryCodesUrl(URL.createObjectURL(textBlob)); } - }, [viewRecoveryCodesData]); + }; const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => { try { @@ -153,7 +153,9 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
- +
From aba6b58c14581d302649adb9967da1d3dec73f76 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Wed, 21 Feb 2024 02:19:35 +0000 Subject: [PATCH 4/5] fix: simplify download api --- .../2fa/enable-authenticator-app-dialog.tsx | 46 +++++++++---------- packages/lib/client-only/download-file.ts | 19 ++++++++ packages/lib/client-only/download-pdf.ts | 13 ++---- 3 files changed, 46 insertions(+), 32 deletions(-) create mode 100644 packages/lib/client-only/download-file.ts diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx index 671292bde..27560c073 100644 --- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx @@ -1,14 +1,12 @@ -import { useMemo, useState } from 'react'; - -import { useRouter } from 'next/navigation'; +import { useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { flushSync } from 'react-dom'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { renderSVG } from 'uqr'; import { z } from 'zod'; +import { downloadFile } from '@documenso/lib/client-only/download-file'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -54,15 +52,16 @@ export const EnableAuthenticatorAppDialog = ({ open, onOpenChange, }: EnableAuthenticatorAppDialogProps) => { - const router = useRouter(); const { toast } = useToast(); - const [recoveryCodesUrl, setRecoveryCodesUrl] = useState(''); const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } = trpc.twoFactorAuthentication.setup.useMutation(); - const { mutateAsync: enableTwoFactorAuthentication, data: enableTwoFactorAuthenticationData } = - trpc.twoFactorAuthentication.enable.useMutation(); + const { + mutateAsync: enableTwoFactorAuthentication, + data: enableTwoFactorAuthenticationData, + isLoading: isEnableTwoFactorAuthenticationDataLoading, + } = trpc.twoFactorAuthentication.enable.useMutation(); const setupTwoFactorAuthenticationForm = useForm({ defaultValues: { @@ -118,11 +117,14 @@ export const EnableAuthenticatorAppDialog = ({ const downloadRecoveryCodes = () => { if (enableTwoFactorAuthenticationData && enableTwoFactorAuthenticationData.recoveryCodes) { - const textBlob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], { + const blob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], { type: 'text/plain', }); - if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl); - setRecoveryCodesUrl(URL.createObjectURL(textBlob)); + + downloadFile({ + filename: 'documenso-2FA-recovery-codes.txt', + data: blob, + }); } }; @@ -147,14 +149,6 @@ export const EnableAuthenticatorAppDialog = ({ } }; - const onCompleteClick = () => { - flushSync(() => { - onOpenChange(false); - }); - - router.refresh(); - }; - return ( @@ -283,11 +277,15 @@ export const EnableAuthenticatorAppDialog = ({
- - - + +
)) diff --git a/packages/lib/client-only/download-file.ts b/packages/lib/client-only/download-file.ts new file mode 100644 index 000000000..36351bedc --- /dev/null +++ b/packages/lib/client-only/download-file.ts @@ -0,0 +1,19 @@ +export type DownloadFileOptions = { + filename: string; + data: Blob; +}; + +export const downloadFile = ({ filename, data }: DownloadFileOptions) => { + if (typeof window === 'undefined') { + throw new Error('downloadFile can only be called in browser environments'); + } + + const link = window.document.createElement('a'); + + link.href = window.URL.createObjectURL(data); + link.download = filename; + + link.click(); + + window.URL.revokeObjectURL(link.href); +}; diff --git a/packages/lib/client-only/download-pdf.ts b/packages/lib/client-only/download-pdf.ts index ec7d0c252..0f757c98d 100644 --- a/packages/lib/client-only/download-pdf.ts +++ b/packages/lib/client-only/download-pdf.ts @@ -1,6 +1,7 @@ import type { DocumentData } from '@documenso/prisma/client'; import { getFile } from '../universal/upload/get-file'; +import { downloadFile } from './download-file'; type DownloadPDFProps = { documentData: DocumentData; @@ -14,16 +15,12 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) type: 'application/pdf', }); - const link = window.document.createElement('a'); - const [baseTitle] = fileName?.includes('.pdf') ? fileName.split('.pdf') : [fileName ?? 'document']; - link.href = window.URL.createObjectURL(blob); - link.download = `${baseTitle}_signed.pdf`; - - link.click(); - - window.URL.revokeObjectURL(link.href); + downloadFile({ + filename: baseTitle, + data: blob, + }); }; From 8287722f59f9b9f128045ce358db80139db9ea81 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Wed, 21 Feb 2024 02:29:19 +0000 Subject: [PATCH 5/5] fix: update view dialog to use new download api --- .../forms/2fa/view-recovery-codes-dialog.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 797b61b84..376a8939c 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -1,10 +1,11 @@ -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { z } from 'zod'; +import { downloadFile } from '@documenso/lib/client-only/download-file'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -41,10 +42,12 @@ export type ViewRecoveryCodesDialogProps = { export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => { const { toast } = useToast(); - const [recoveryCodesUrl, setRecoveryCodesUrl] = useState(''); - const { mutateAsync: viewRecoveryCodes, data: viewRecoveryCodesData } = - trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); + const { + mutateAsync: viewRecoveryCodes, + data: viewRecoveryCodesData, + isLoading: isViewRecoveryCodesDataLoading, + } = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); const viewRecoveryCodesForm = useForm({ defaultValues: { @@ -65,11 +68,14 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode const downloadRecoveryCodes = () => { if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) { - const textBlob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], { + const blob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], { type: 'text/plain', }); - if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl); - setRecoveryCodesUrl(URL.createObjectURL(textBlob)); + + downloadFile({ + filename: 'documenso-2FA-recovery-codes.txt', + data: blob, + }); } }; @@ -152,11 +158,15 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
- - - + +
))