From 37e9db6626a7336427b4a2de4e3825a64186f948 Mon Sep 17 00:00:00 2001 From: Prajwal Kulkarni Date: Tue, 6 Feb 2024 00:40:53 +0530 Subject: [PATCH 001/100] Remove document on go back click on step 1 Invoke onBackStep on "go back" click and conditionally render go back label --- packages/ui/primitives/document-flow/add-fields.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 74764df80..083fbdcbf 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -566,7 +566,9 @@ export const AddFieldsFormPartial = ({ onGoBackClick={() => { previousStep(); remove(); + documentFlow.onBackStep?.(); }} + goBackLabel={currentStep === 1 && typeof documentFlow.onBackStep === "function" ? "Remove" : undefined} onGoNextClick={() => void onFormSubmit()} /> From c08768a33038f3499e95f3125d453a087cbe42fa Mon Sep 17 00:00:00 2001 From: Prajwal Kulkarni Date: Tue, 6 Feb 2024 21:01:48 +0530 Subject: [PATCH 002/100] Format code with prettier --- packages/ui/primitives/document-flow/add-fields.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 083fbdcbf..0316f2c13 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -568,7 +568,11 @@ export const AddFieldsFormPartial = ({ remove(); documentFlow.onBackStep?.(); }} - goBackLabel={currentStep === 1 && typeof documentFlow.onBackStep === "function" ? "Remove" : undefined} + goBackLabel={ + currentStep === 1 && typeof documentFlow.onBackStep === 'function' + ? 'Remove' + : undefined + } onGoNextClick={() => void onFormSubmit()} /> From 4855882ae6ae4e4381cc79f97611e275ffce5cc5 Mon Sep 17 00:00:00 2001 From: Prajwal Kulkarni Date: Wed, 7 Feb 2024 21:31:51 +0530 Subject: [PATCH 003/100] Update label render condition --- .../src/app/(marketing)/singleplayer/client.tsx | 1 + packages/ui/primitives/document-flow/add-fields.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index a1b56257a..f9ca3bd9a 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -244,6 +244,7 @@ export const SinglePlayerClient = () => { recipients={uploadedFile ? [placeholderRecipient] : []} fields={fields} onSubmit={onFieldsSubmit} + isSinglePlayerMode={true} /> diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 0316f2c13..3e69c2e8d 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -53,6 +53,7 @@ export type AddFieldsFormProps = { recipients: Recipient[]; fields: Field[]; onSubmit: (_data: TAddFieldsFormSchema) => void; + isSinglePlayerMode?: boolean; }; export const AddFieldsFormPartial = ({ @@ -61,10 +62,12 @@ export const AddFieldsFormPartial = ({ recipients, fields, onSubmit, + isSinglePlayerMode = false, }: AddFieldsFormProps) => { const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement(); const { currentStep, totalSteps, previousStep } = useStep(); - + const canRenderBackButtonAsRemove = + currentStep === 1 && typeof documentFlow.onBackStep === 'function' && isSinglePlayerMode; const { control, handleSubmit, @@ -568,11 +571,7 @@ export const AddFieldsFormPartial = ({ remove(); documentFlow.onBackStep?.(); }} - goBackLabel={ - currentStep === 1 && typeof documentFlow.onBackStep === 'function' - ? 'Remove' - : undefined - } + goBackLabel={canRenderBackButtonAsRemove ? 'Remove' : undefined} onGoNextClick={() => void onFormSubmit()} /> From 897f0dabdefeebedb93f0a32be500d22d8c1fde0 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 15 Feb 2024 14:21:40 +0000 Subject: [PATCH 004/100] feat: 2fa pin input component --- apps/web/package.json | 2 + .../2fa/enable-authenticator-app-dialog.tsx | 44 +++++++++++++++---- package-lock.json | 32 ++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index fd4faa0c1..8656d5092 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -35,6 +35,7 @@ "perfect-freehand": "^1.2.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", + "rci": "^0.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", @@ -47,6 +48,7 @@ "typescript": "5.2.2", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", + "use-is-focused": "^0.0.1", "zod": "^3.22.4" }, "devDependencies": { 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..7a493d5b0 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'; @@ -27,8 +27,8 @@ import { FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; -import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; +import { PinInput, type PinInputState } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { RecoveryCodeList } from './recovery-code-list'; @@ -54,6 +54,7 @@ export const EnableAuthenticatorAppDialog = ({ open, onOpenChange, }: EnableAuthenticatorAppDialogProps) => { + const [state, setState] = useState('input'); const router = useRouter(); const { toast } = useToast(); @@ -119,13 +120,15 @@ export const EnableAuthenticatorAppDialog = ({ token, }: TEnableTwoFactorAuthenticationForm) => { try { - await enableTwoFactorAuthentication({ code: token }); + const enabled2fa = await enableTwoFactorAuthentication({ code: token }); toast({ title: 'Two-factor authentication enabled', description: 'Two-factor authentication has been enabled for your account. You will now be required to enter a code from your authenticator app when signing in.', }); + + return enabled2fa; } catch (_err) { toast({ title: 'Unable to setup two-factor authentication', @@ -136,6 +139,31 @@ export const EnableAuthenticatorAppDialog = ({ } }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onPinInputChange = ({ currentTarget: input }: any) => { + input.value = input.value.replace(/\D+/g, ''); + + if (input.value.length === 6) { + setState('loading'); + + void onEnableTwoFactorAuthenticationFormSubmit({ token: input.value }).then((success) => { + if (success) { + setState('success'); + return; + } + + setState('error'); + + setTimeout(() => { + setState('input'); + input.value = ''; + input.dispatchEvent(new Event('input')); + input.focus(); + }, 500); + }); + } + }; + const onCompleteClick = () => { flushSync(() => { onOpenChange(false); @@ -146,7 +174,7 @@ export const EnableAuthenticatorAppDialog = ({ return ( - + Enable Authenticator App @@ -241,18 +269,18 @@ export const EnableAuthenticatorAppDialog = ({ ( + render={({ field: _field }) => ( Token - + )} /> - + {/* @@ -260,7 +288,7 @@ export const EnableAuthenticatorAppDialog = ({ - + */} )) diff --git a/package-lock.json b/package-lock.json index 3c136e801..803a68344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,7 @@ "perfect-freehand": "^1.2.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", + "rci": "^0.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", @@ -163,6 +164,7 @@ "typescript": "5.2.2", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", + "use-is-focused": "^0.0.1", "zod": "^3.22.4" }, "devDependencies": { @@ -15726,6 +15728,18 @@ "node": ">= 0.8" } }, + "node_modules/rci": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rci/-/rci-0.1.0.tgz", + "integrity": "sha512-o/elFrXXRLdYDAq/qQUFE175TqzJ5nU3MYwIwa6WOZfljNJ4akQSy1n7zA79swB696MNIFDWJs+Do0q2FBTy+Q==", + "dependencies": { + "use-code-input": "0.0.2" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/re-resizable": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz", @@ -18860,6 +18874,24 @@ } } }, + "node_modules/use-code-input": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/use-code-input/-/use-code-input-0.0.2.tgz", + "integrity": "sha512-lDIUiRca0K8sF+c/KZ9cz5g6oPqlFiTmaDgwGzg0wlNSnFAvROtweKy0XpihEWJwo2tjETtgAxIh82RVGaBFHQ==", + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/use-is-focused": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/use-is-focused/-/use-is-focused-0.0.1.tgz", + "integrity": "sha512-EXVmfDqdzUJOYukC9rBCs4TYd93lDVAL6TxegnV0+3U4cBxWxhbyt1bOm5u1ox+0MZZjamBFU/NSTLTtex2uwQ==", + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", From 345c4b8b147d418fb087d093d621b3935951d881 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 15 Feb 2024 15:55:58 +0000 Subject: [PATCH 005/100] feat: use pin-input on sign in --- .../2fa/enable-authenticator-app-dialog.tsx | 56 ++++++------ apps/web/src/components/forms/signin.tsx | 45 ++++++++-- packages/ui/primitives/pin-input.tsx | 85 +++++++++++++++++++ packages/ui/styles/theme.css | 22 +++++ 4 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 packages/ui/primitives/pin-input.tsx 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 7a493d5b0..fa5d223d8 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 @@ -139,31 +139,6 @@ export const EnableAuthenticatorAppDialog = ({ } }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const onPinInputChange = ({ currentTarget: input }: any) => { - input.value = input.value.replace(/\D+/g, ''); - - if (input.value.length === 6) { - setState('loading'); - - void onEnableTwoFactorAuthenticationFormSubmit({ token: input.value }).then((success) => { - if (success) { - setState('success'); - return; - } - - setState('error'); - - setTimeout(() => { - setState('input'); - input.value = ''; - input.dispatchEvent(new Event('input')); - input.focus(); - }, 500); - }); - } - }; - const onCompleteClick = () => { flushSync(() => { onOpenChange(false); @@ -273,7 +248,36 @@ export const EnableAuthenticatorAppDialog = ({ Token - + { + console.log(code); + + if (code.length === 6) { + setState('loading'); + + void onEnableTwoFactorAuthenticationFormSubmit({ token: code }).then( + (success) => { + if (success) { + setState('success'); + return; + } + + setState('error'); + + setTimeout(() => { + setState('input'); + input.value = ''; + input.dispatchEvent(new Event('input')); + input.focus(); + }, 500); + }, + ); + } + }} + autoFocus + /> diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index ec690a568..b182d5d76 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -31,6 +31,7 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; +import { PinInput, type PinInputState } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ERROR_MESSAGES: Partial> = { @@ -72,6 +73,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< 'totp' | 'backup' >('totp'); + const [state, setState] = useState('input'); const form = useForm({ values: { @@ -151,18 +153,24 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign title: 'Unable to sign in', description: errorMessage ?? 'An unknown error occurred', }); - - return; } + setState('success'); + console.log(result); + if (!result?.url) { throw new Error('An unknown error occurred'); } window.location.href = result.url; } catch (err) { + form.setError('totpCode', { + message: 'invalid totp', + }); + toast({ title: 'An unknown error occurred', + variant: 'destructive', description: 'We encountered an unknown error while attempting to sign you In. Please try again later.', }); @@ -254,7 +262,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign open={isTwoFactorAuthenticationDialogOpen} onOpenChange={onCloseTwoFactorAuthenticationDialog} > - + Two-Factor Authentication @@ -265,13 +273,38 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign ( + render={({ field: _field }) => ( Authentication Token - + { + if (code.length === 6) { + setState('loading'); + form.setValue('totpCode', code); + + await form.handleSubmit(onFormSubmit)(); + + if (form.formState.isSubmitted && !form.formState.errors.totpCode) { + setState('success'); + return; + } + + setState('error'); + + setTimeout(() => { + setState('input'); + input.value = ''; + input.dispatchEvent(new Event('input')); + input.focus(); + }, 500); + } + }} + autoFocus + /> - )} /> diff --git a/packages/ui/primitives/pin-input.tsx b/packages/ui/primitives/pin-input.tsx new file mode 100644 index 000000000..b29d64376 --- /dev/null +++ b/packages/ui/primitives/pin-input.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useRef } from 'react'; + +import { CodeInput, getSegmentCssWidth } from 'rci'; +import { useIsFocused } from 'use-is-focused'; + +import { cn } from '@documenso/ui/lib/utils'; + +export type PinInputState = 'input' | 'loading' | 'error' | 'success'; +export type PinInputProps = { + id: string; + state: PinInputState; + autoFocus?: boolean; + onSubmit({ code, input }: { code: string; input: EventTarget & HTMLInputElement }): void; +}; + +const PinInput = ({ id, autoFocus, state, onSubmit }: PinInputProps) => { + const inputRef = useRef(null); + const focused = useIsFocused(inputRef); + + const width = getSegmentCssWidth('14px'); + + return ( + { + input.value = input.value.replace(/\D+/g, ''); + onSubmit({ code: input.value, input }); + }} + renderSegment={(segment) => { + const isCaret = focused && segment.state === 'cursor'; + const isSelection = focused && segment.state === 'selected'; + const isLoading = state === 'loading'; + const isSuccess = state === 'success'; + const isError = state === 'error'; + const isActive = isSuccess || isError || isSelection || isCaret; + + return ( +
+
+
+ ); + }} + /> + ); +}; + +export { PinInput }; diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index fe7bfa087..15c6a2f8d 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -114,3 +114,25 @@ .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgb(100 116 139 / 0.5); } + +@keyframes blink-caret { + 50% { + background: transparent; + } +} + +@keyframes shake { + 25% { + transform: translateX(10px); + } + 75% { + transform: translateX(-10px); + } +} + +@keyframes pulse-border { + 50% { + border-color: var(--segment-color); + box-shadow: var(--segment-color) 0 0 0 1px; + } +} From 94eee8b913b027cec90429123876c825283252ef Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 16 Feb 2024 20:49:52 +0000 Subject: [PATCH 006/100] chore: change font family --- packages/ui/primitives/pin-input.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ui/primitives/pin-input.tsx b/packages/ui/primitives/pin-input.tsx index b29d64376..9f579288c 100644 --- a/packages/ui/primitives/pin-input.tsx +++ b/packages/ui/primitives/pin-input.tsx @@ -30,13 +30,12 @@ const PinInput = ({ id, autoFocus, state, onSubmit }: PinInputProps) => { inputClassName="caret-transparent selection:bg-transparent ring:ring-2" autoFocus={autoFocus} length={6} - fontFamily="Inter" - fontSize="36px" + fontSize="30px" readOnly={state !== 'input'} disabled={state === 'loading'} inputRef={inputRef} padding={'14px'} - spacing={'18px'} + spacing={'24px'} spellCheck={false} inputMode="numeric" pattern="[0-9]*" @@ -69,8 +68,7 @@ const PinInput = ({ id, autoFocus, state, onSubmit }: PinInputProps) => { >
Date: Fri, 16 Feb 2024 21:20:16 +0000 Subject: [PATCH 007/100] chore: use token input on enable 2fa --- .../2fa/enable-authenticator-app-dialog.tsx | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 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 fa5d223d8..41a5ca573 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 @@ -130,6 +130,10 @@ export const EnableAuthenticatorAppDialog = ({ return enabled2fa; } catch (_err) { + enableTwoFactorAuthenticationForm.setError('token', { + message: 'Unable to setup two-factor authentication', + }); + toast({ title: 'Unable to setup two-factor authentication', description: @@ -251,40 +255,41 @@ export const EnableAuthenticatorAppDialog = ({ { - console.log(code); - + onSubmit={async ({ code, input }) => { if (code.length === 6) { setState('loading'); + enableTwoFactorAuthenticationForm.setValue('token', code); - void onEnableTwoFactorAuthenticationFormSubmit({ token: code }).then( - (success) => { - if (success) { - setState('success'); - return; - } + await enableTwoFactorAuthenticationForm.handleSubmit( + onEnableTwoFactorAuthenticationFormSubmit, + )(); - setState('error'); + if ( + enableTwoFactorAuthenticationForm.formState.isSubmitted && + !enableTwoFactorAuthenticationForm.formState.errors.totpCode + ) { + setState('success'); + return; + } - setTimeout(() => { - setState('input'); - input.value = ''; - input.dispatchEvent(new Event('input')); - input.focus(); - }, 500); - }, - ); + setState('error'); + + setTimeout(() => { + setState('input'); + input.value = ''; + input.dispatchEvent(new Event('input')); + input.focus(); + }, 500); } }} autoFocus /> - )} /> - {/* + @@ -292,7 +297,7 @@ export const EnableAuthenticatorAppDialog = ({ - */} + )) From 5e8d93f24b7d6c90d058fc7f0d0c62dee9767d49 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:42:17 +0200 Subject: [PATCH 008/100] feat: add kysely for raw type-safe SQL queries --- package-lock.json | 1508 ++++++++++++++++++++++++++++++++- packages/prisma/index.ts | 19 +- packages/prisma/package.json | 3 + packages/prisma/schema.prisma | 4 +- 4 files changed, 1529 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d47958aa..49945a007 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,6 +188,21 @@ "zod": "^3.20.0" } }, + "node_modules/@antfu/ni": { + "version": "0.21.8", + "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.8.tgz", + "integrity": "sha512-90X8pU2szlvw0AJo9EZMbYc2eQKkmO7mAdC4tD4r5co2Mm56MT37MIG8EyB7p4WRheuzGxuLDxJ63mF6+Zajiw==", + "dev": true, + "bin": { + "na": "bin/na.mjs", + "nci": "bin/nci.mjs", + "ni": "bin/ni.mjs", + "nlx": "bin/nlx.mjs", + "nr": "bin/nr.mjs", + "nu": "bin/nu.mjs", + "nun": "bin/nun.mjs" + } + }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", @@ -1408,6 +1423,39 @@ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.0.0.tgz", "integrity": "sha512-GMu2OJiTd1HSe74bbJYQnVvELANpYiGFZELyyTM1CR0sdv5ReQAcJ/c/8pIrPab3lO11+D+EpuGLUxqz+y832g==" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "dev": true, + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "dev": true, + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "dev": true + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "dev": true + }, "node_modules/@commitlint/cli": { "version": "17.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.8.1.tgz", @@ -3485,6 +3533,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.7.0.tgz", + "integrity": "sha512-GTPkYf1meO2UXXIrz/SIDFWz+P4kXo2PTt36LYh/oNxV1PieYi7ZgenQk4IV0ut71Je3Z8ZoNZ8Tr7v2c1X1pg==", + "dev": true, + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.1.1.tgz", @@ -4655,6 +4716,26 @@ } } }, + "node_modules/@prisma/debug": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.3.1.tgz", + "integrity": "sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==", + "dev": true, + "dependencies": { + "@types/debug": "4.1.8", + "debug": "4.3.4", + "strip-ansi": "6.0.1" + } + }, + "node_modules/@prisma/debug/node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@prisma/engines": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.4.2.tgz", @@ -4666,6 +4747,358 @@ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574.tgz", "integrity": "sha512-wvupDL4AA1vf4TQNANg7kR7y98ITqPsk6aacfBxZKtrJKRIsWjURHkZCGcQliHdqCiW/hGreO6d6ZuSv9MhdAA==" }, + "node_modules/@prisma/fetch-engine": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.3.1.tgz", + "integrity": "sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "@prisma/get-platform": "5.3.1", + "execa": "5.1.1", + "find-cache-dir": "3.3.2", + "fs-extra": "11.1.1", + "hasha": "5.2.2", + "http-proxy-agent": "7.0.0", + "https-proxy-agent": "7.0.2", + "kleur": "4.1.5", + "node-fetch": "2.7.0", + "p-filter": "2.1.0", + "p-map": "4.0.0", + "p-retry": "4.6.2", + "progress": "2.0.3", + "rimraf": "3.0.2", + "temp-dir": "2.0.0", + "tempy": "1.0.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@prisma/fetch-engine/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@prisma/generator-helper": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.3.1.tgz", + "integrity": "sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "@types/cross-spawn": "6.0.2", + "cross-spawn": "7.0.3", + "kleur": "4.1.5" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.3.1.tgz", + "integrity": "sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.3.1", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "fs-jetpack": "5.1.0", + "kleur": "4.1.5", + "replace-string": "3.1.0", + "strip-ansi": "6.0.1", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "ts-pattern": "4.3.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/ts-pattern": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", + "integrity": "sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==", + "dev": true + }, + "node_modules/@prisma/internals": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz", + "integrity": "sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==", + "dev": true, + "dependencies": { + "@antfu/ni": "0.21.8", + "@opentelemetry/api": "1.4.1", + "@prisma/debug": "5.3.1", + "@prisma/engines": "5.3.1", + "@prisma/fetch-engine": "5.3.1", + "@prisma/generator-helper": "5.3.1", + "@prisma/get-platform": "5.3.1", + "@prisma/prisma-schema-wasm": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "archiver": "5.3.2", + "arg": "5.0.2", + "checkpoint-client": "1.1.27", + "cli-truncate": "2.1.0", + "dotenv": "16.0.3", + "escape-string-regexp": "4.0.0", + "execa": "5.1.1", + "find-up": "5.0.0", + "fp-ts": "2.16.1", + "fs-extra": "11.1.1", + "fs-jetpack": "5.1.0", + "global-dirs": "3.0.1", + "globby": "11.1.0", + "indent-string": "4.0.0", + "is-windows": "1.0.2", + "is-wsl": "2.2.0", + "kleur": "4.1.5", + "new-github-issue-url": "0.2.1", + "node-fetch": "2.7.0", + "npm-packlist": "5.1.3", + "open": "7.4.2", + "p-map": "4.0.0", + "prompts": "2.4.2", + "read-pkg-up": "7.0.1", + "replace-string": "3.1.0", + "resolve": "1.22.4", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-indent": "3.0.0", + "temp-dir": "2.0.0", + "tempy": "1.0.1", + "terminal-link": "2.1.1", + "tmp": "0.2.1", + "ts-pattern": "4.3.0" + } + }, + "node_modules/@prisma/internals/node_modules/@prisma/engines": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", + "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", + "dev": true, + "hasInstallScript": true + }, + "node_modules/@prisma/internals/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/@prisma/internals/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@prisma/internals/node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/internals/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/@prisma/internals/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@prisma/internals/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@prisma/internals/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@prisma/internals/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@prisma/internals/node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@prisma/internals/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@prisma/internals/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@prisma/internals/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@prisma/internals/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/@prisma/internals/node_modules/ts-pattern": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", + "integrity": "sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==", + "dev": true + }, + "node_modules/@prisma/prisma-schema-wasm": { + "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", + "integrity": "sha512-+zUI7NQDXfcNnU8HgrAj4jRMv8yRfITLzcfv0Urf0adKimM+hkkVG4rX38i9zWMlxekkEBw7NLFx3Gxxy8d3iQ==", + "dev": true + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -7814,6 +8247,15 @@ "@types/estree": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", + "integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -8021,6 +8463,12 @@ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==" }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -8321,6 +8769,19 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -8397,6 +8858,81 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "optional": true }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -8630,6 +9166,15 @@ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==" }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/astring": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", @@ -8638,6 +9183,12 @@ "astring": "bin/astring" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/asynciterator.prototype": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", @@ -8946,6 +9497,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9217,6 +9777,99 @@ "node": ">= 0.8.0" } }, + "node_modules/checkpoint-client": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/checkpoint-client/-/checkpoint-client-1.1.27.tgz", + "integrity": "sha512-xstymfUalJOv6ZvTtmkwP4ORJN36ikT4PvrIoLe3wstbYf87XIXCcZrSmbFQOjyB0v1qbBnCsAscDpfdZlCkFA==", + "dev": true, + "dependencies": { + "ci-info": "3.8.0", + "env-paths": "2.2.1", + "make-dir": "4.0.0", + "ms": "2.1.3", + "node-fetch": "2.6.12", + "uuid": "9.0.0" + } + }, + "node_modules/checkpoint-client/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/checkpoint-client/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/checkpoint-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/checkpoint-client/node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/checkpoint-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "dev": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -9282,6 +9935,15 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -9817,6 +10479,12 @@ "minimist": "^1.1.0" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -9827,6 +10495,21 @@ "dot-prop": "^5.1.0" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10046,6 +10729,31 @@ "typescript": ">=4" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -10070,6 +10778,15 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -10391,6 +11108,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10861,6 +11615,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -12348,6 +13111,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -12502,6 +13282,12 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/fp-ts": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", + "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", + "dev": true + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -12542,6 +13328,12 @@ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -12555,6 +13347,36 @@ "node": ">=14.14" } }, + "node_modules/fs-jetpack": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz", + "integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/fs-jetpack/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/fs-jetpack/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -13092,6 +13914,31 @@ "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.11.0.tgz", "integrity": "sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ==" }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -13380,6 +14227,31 @@ "node": ">= 0.6" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -13454,6 +14326,39 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", + "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/imagescript": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/imagescript/-/imagescript-1.2.17.tgz", @@ -13882,6 +14787,15 @@ "node": ">=8" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -14075,6 +14989,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -14455,6 +15378,14 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz", + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -14479,6 +15410,54 @@ "node": "> 0.8" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leac": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", @@ -14863,6 +15842,24 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, "node_modules/lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", @@ -14910,6 +15907,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -15184,7 +16187,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, + "devOptional": true, "dependencies": { "semver": "^6.0.0" }, @@ -15199,7 +16202,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "optional": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" } @@ -16443,6 +17446,15 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/new-github-issue-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz", + "integrity": "sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/next": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz", @@ -16702,6 +17714,85 @@ "node": ">=0.10.0" } }, + "node_modules/npm-bundled": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", + "dev": true, + "dependencies": { + "glob": "^8.0.1", + "ignore-walk": "^5.0.1", + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-packlist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm-packlist/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-packlist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -17031,6 +18122,27 @@ "@node-rs/bcrypt": "^1.7.3" } }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17059,6 +18171,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -17407,6 +18547,70 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -17815,6 +19019,30 @@ "node": ">=16.13" } }, + "node_modules/prisma-extension-kysely": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prisma-extension-kysely/-/prisma-extension-kysely-2.1.0.tgz", + "integrity": "sha512-s1hujYjrNzfQc9Z79s93464mkTkkt9ZPqPELDRFe9jEmiIlM/JRXZtqIyyN3FM0GDkN/BDPVtpPtdC52XnjAcQ==", + "peerDependencies": { + "@prisma/client": "latest" + } + }, + "node_modules/prisma-kysely": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz", + "integrity": "sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==", + "dev": true, + "dependencies": { + "@mrleebo/prisma-ast": "^0.7.0", + "@prisma/generator-helper": "5.3.1", + "@prisma/internals": "5.3.1", + "typescript": "^5.2.2", + "zod": "^3.22.2" + }, + "bin": { + "prisma-kysely": "dist/bin.js" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -17836,6 +19064,37 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -19110,6 +20369,36 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -19387,6 +20676,12 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -19538,6 +20833,18 @@ "node": ">=0.10" } }, + "node_modules/replace-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/replace-string/-/replace-string-3.1.0.tgz", + "integrity": "sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -19653,6 +20960,15 @@ "node": ">=0.12" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -20123,6 +21439,12 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -20767,6 +22089,19 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -21038,6 +22373,105 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", + "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -21919,6 +23353,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/unist-util-generated": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", @@ -23171,6 +24617,61 @@ "resolved": "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz", "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==" }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", @@ -24754,12 +26255,15 @@ "license": "MIT", "dependencies": { "@prisma/client": "5.4.2", + "kysely": "^0.27.3", "prisma": "5.4.2", + "prisma-extension-kysely": "^2.1.0", "ts-pattern": "^5.0.6" }, "devDependencies": { "dotenv": "^16.3.1", "dotenv-cli": "^7.3.0", + "prisma-kysely": "^1.8.0", "ts-node": "^10.9.1", "typescript": "5.2.2" } diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index b9e290add..a9bbc2e65 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -1,5 +1,8 @@ import { PrismaClient } from '@prisma/client'; +import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely'; +import kyselyExtension from 'prisma-extension-kysely'; +import type { DB } from './generated/types.js'; import { getDatabaseUrl } from './helper'; declare global { @@ -12,10 +15,24 @@ if (!globalThis.prisma) { globalThis.prisma = new PrismaClient({ datasourceUrl: getDatabaseUrl() }); } -export const prisma = +const _prisma = globalThis.prisma || new PrismaClient({ datasourceUrl: getDatabaseUrl(), }); +export const prisma = _prisma.$extends( + kyselyExtension({ + kysely: (driver) => + new Kysely({ + dialect: { + createAdapter: () => new PostgresAdapter(), + createDriver: () => driver, + createIntrospector: (db) => new PostgresIntrospector(db), + createQueryCompiler: () => new PostgresQueryCompiler(), + }, + }), + }), +); + export const getPrismaClient = () => prisma; diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 199ce197a..76c6f0e0f 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -20,12 +20,15 @@ }, "dependencies": { "@prisma/client": "5.4.2", + "kysely": "^0.27.3", "prisma": "5.4.2", + "prisma-extension-kysely": "^2.1.0", "ts-pattern": "^5.0.6" }, "devDependencies": { "dotenv": "^16.3.1", "dotenv-cli": "^7.3.0", + "prisma-kysely": "^1.8.0", "ts-node": "^10.9.1", "typescript": "5.2.2" } diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index b1bf9f985..4acc8d70a 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -1,5 +1,5 @@ -generator client { - provider = "prisma-client-js" +generator kysely { + provider = "prisma-kysely" } datasource db { From fdbac9fc03f91e2dba604ed0158044bec35837fa Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:07:05 +0200 Subject: [PATCH 009/100] feat: update next-auth-options to use the kysely adapter --- package-lock.json | 332 ++++++++++++++++++++++++- packages/lib/next-auth/auth-options.ts | 17 +- packages/lib/package.json | 8 +- 3 files changed, 352 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49945a007..bd03a6627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -203,6 +203,72 @@ "nun": "bin/nun.mjs" } }, + "node_modules/@auth/core": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.0.tgz", + "integrity": "sha512-/fh/tb/L4NMSYcyPoo4Imn8vN6MskcVfgESF8/ndgtI4fhD/7u7i5fTVzWgNRZ4ebIEGHNDbWFRxaTu1NtQgvA==", + "dependencies": { + "@panva/hkdf": "^1.1.1", + "@types/cookie": "0.6.0", + "cookie": "0.6.0", + "jose": "^5.1.3", + "oauth4webapi": "^2.4.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/core/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@auth/core/node_modules/jose": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/core/node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/core/node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", @@ -8247,6 +8313,11 @@ "@types/estree": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/cross-spawn": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", @@ -8426,6 +8497,74 @@ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" }, + "node_modules/@types/pg": { + "version": "8.11.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.4.tgz", + "integrity": "sha512-yw3Bwbda6vO+NvI1Ue/YKOwtl31AYvvd/e73O3V4ZkNzuGpTDndLSyc0dQRB2xrQqDePd20pEGIfqSp/GH3pRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -9519,6 +9658,14 @@ "node": ">=0.10" } }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", @@ -17830,6 +17977,14 @@ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" }, + "node_modules/oauth4webapi": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.3.tgz", + "integrity": "sha512-9FkXEXfzVKzH63GUOZz1zMr3wBaICSzk6DLXx+CGdrQ10ItNk2ePWzYYc1fdmKq1ayGFb2aX97sRCoZ2s0mkDw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -17947,6 +18102,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -18218,6 +18379,11 @@ "pjv": "bin/pjv" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -18503,6 +18669,106 @@ "is-reference": "^3.0.0" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgpass/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -18825,6 +19091,47 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, "node_modules/posthog-js": { "version": "1.93.2", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.93.2.tgz", @@ -26187,6 +26494,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@auth/kysely-adapter": "^0.6.0", "@aws-sdk/client-s3": "^3.410.0", "@aws-sdk/cloudfront-signer": "^3.410.0", "@aws-sdk/s3-request-presigner": "^3.410.0", @@ -26204,12 +26512,14 @@ "@sindresorhus/slugify": "^2.2.1", "@upstash/redis": "^1.20.6", "@vvo/tzdb": "^6.117.0", + "kysely": "^0.26.3", "luxon": "^3.4.0", "nanoid": "^4.0.2", "next": "14.0.3", "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", + "pg": "^8.11.3", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -26217,7 +26527,27 @@ "zod": "^3.22.4" }, "devDependencies": { - "@types/luxon": "^3.3.1" + "@types/luxon": "^3.3.1", + "@types/pg": "^8.11.4" + } + }, + "packages/lib/node_modules/@auth/kysely-adapter": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@auth/kysely-adapter/-/kysely-adapter-0.6.0.tgz", + "integrity": "sha512-WTWkaoS4cD7s5p/IsB/6cgJcC+qvEo+D/ix7l9YHajj0M4ZgJMKuqgWDER+gllc761jxAYfLizJ9d9DjgFFTPA==", + "dependencies": { + "@auth/core": "0.28.0" + }, + "peerDependencies": { + "kysely": "^0.26.1" + } + }, + "packages/lib/node_modules/kysely": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz", + "integrity": "sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==", + "engines": { + "node": ">=14.0.0" } }, "packages/lib/node_modules/nanoid": { diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index 74a712576..acf614051 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -1,6 +1,7 @@ /// -import { PrismaAdapter } from '@next-auth/prisma-adapter'; +import { KyselyAdapter } from '@auth/kysely-adapter'; import { compare } from '@node-rs/bcrypt'; +import { Kysely, PostgresDialect } from 'kysely'; import { DateTime } from 'luxon'; import type { AuthOptions, Session, User } from 'next-auth'; import type { JWT } from 'next-auth/jwt'; @@ -8,9 +9,11 @@ import CredentialsProvider from 'next-auth/providers/credentials'; import type { GoogleProfile } from 'next-auth/providers/google'; import GoogleProvider from 'next-auth/providers/google'; import { env } from 'next-runtime-env'; +import { Pool } from 'pg'; import { prisma } from '@documenso/prisma'; import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client'; +import type { DB } from '@documenso/prisma/generated/types.js'; import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble'; import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa'; @@ -20,8 +23,18 @@ import { sendConfirmationToken } from '../server-only/user/send-confirmation-tok import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata'; import { ErrorCode } from './error-codes'; +// move this from here +const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL, + }), + }), +}); + export const NEXT_AUTH_OPTIONS: AuthOptions = { - adapter: PrismaAdapter(prisma), + //@ts-expect-error - https://github.com/nextauthjs/next-auth/issues/8660 + adapter: KyselyAdapter(db), secret: process.env.NEXTAUTH_SECRET ?? 'secret', session: { strategy: 'jwt', diff --git a/packages/lib/package.json b/packages/lib/package.json index 7a32b3058..df122ef29 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -16,6 +16,7 @@ "clean": "rimraf node_modules" }, "dependencies": { + "@auth/kysely-adapter": "^0.6.0", "@aws-sdk/client-s3": "^3.410.0", "@aws-sdk/cloudfront-signer": "^3.410.0", "@aws-sdk/s3-request-presigner": "^3.410.0", @@ -27,18 +28,20 @@ "@next-auth/prisma-adapter": "1.0.7", "@noble/ciphers": "0.4.0", "@noble/hashes": "1.3.2", + "@node-rs/bcrypt": "^1.10.0", "@pdf-lib/fontkit": "^1.1.1", "@scure/base": "^1.1.3", "@sindresorhus/slugify": "^2.2.1", "@upstash/redis": "^1.20.6", "@vvo/tzdb": "^6.117.0", - "@node-rs/bcrypt": "^1.10.0", + "kysely": "^0.26.3", "luxon": "^3.4.0", "nanoid": "^4.0.2", "next": "14.0.3", "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", + "pg": "^8.11.3", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -46,6 +49,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@types/luxon": "^3.3.1" + "@types/luxon": "^3.3.1", + "@types/pg": "^8.11.4" } } From 6b73899ecc19e7f7f8ece9a404d2a5662ce490de Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:46:53 +0200 Subject: [PATCH 010/100] chore: re-arrange stuff --- packages/lib/next-auth/auth-options.ts | 13 +------------ packages/lib/next-auth/kysely-db/db.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 packages/lib/next-auth/kysely-db/db.ts diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index acf614051..f04de55f8 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -1,7 +1,6 @@ /// import { KyselyAdapter } from '@auth/kysely-adapter'; import { compare } from '@node-rs/bcrypt'; -import { Kysely, PostgresDialect } from 'kysely'; import { DateTime } from 'luxon'; import type { AuthOptions, Session, User } from 'next-auth'; import type { JWT } from 'next-auth/jwt'; @@ -9,11 +8,9 @@ import CredentialsProvider from 'next-auth/providers/credentials'; import type { GoogleProfile } from 'next-auth/providers/google'; import GoogleProvider from 'next-auth/providers/google'; import { env } from 'next-runtime-env'; -import { Pool } from 'pg'; import { prisma } from '@documenso/prisma'; import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client'; -import type { DB } from '@documenso/prisma/generated/types.js'; import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble'; import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa'; @@ -22,15 +19,7 @@ import { getUserByEmail } from '../server-only/user/get-user-by-email'; import { sendConfirmationToken } from '../server-only/user/send-confirmation-token'; import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata'; import { ErrorCode } from './error-codes'; - -// move this from here -const db = new Kysely({ - dialect: new PostgresDialect({ - pool: new Pool({ - connectionString: process.env.DATABASE_URL, - }), - }), -}); +import { db } from './kysely-db/db'; export const NEXT_AUTH_OPTIONS: AuthOptions = { //@ts-expect-error - https://github.com/nextauthjs/next-auth/issues/8660 diff --git a/packages/lib/next-auth/kysely-db/db.ts b/packages/lib/next-auth/kysely-db/db.ts new file mode 100644 index 000000000..6f6f78ef9 --- /dev/null +++ b/packages/lib/next-auth/kysely-db/db.ts @@ -0,0 +1,14 @@ +import { KyselyAuth } from '@auth/kysely-adapter'; +import type { Codegen } from '@auth/kysely-adapter'; +import { PostgresDialect } from 'kysely'; +import { Pool } from 'pg'; + +import type { DB } from '@documenso/prisma/generated/types'; + +export const db = new KyselyAuth({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL, + }), + }), +}); From 462e1348a8b39cf5adbaa7e58900cdcf7d194a30 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:02:51 +0200 Subject: [PATCH 011/100] chore: test queries --- .../server-only/document/find-documents.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index f34cc4c2c..6ddafd97f 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,3 +1,4 @@ +import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { DateTime } from 'luxon'; import { P, match } from 'ts-pattern'; @@ -60,6 +61,22 @@ export const findDocuments = async ({ teamEmail: true, }, }); + + const teamQuery = await prisma.$kysely + .selectFrom('Team') + .selectAll('Team') + .where('Team.id', '=', teamId) + .select((eb) => [ + jsonObjectFrom( + eb + .selectFrom('TeamEmail') + .selectAll('TeamEmail') + .where('TeamEmail.teamId', '=', teamId), + ).as('teamEmail'), + ]) + .innerJoin('TeamMember', 'TeamMember.teamId', 'Team.id') + .where('TeamMember.userId', '=', userId) + .executeTakeFirstOrThrow(); } return { @@ -128,6 +145,45 @@ export const findDocuments = async ({ }; } + const dataQuery = await prisma.$kysely + .selectFrom('Document') + .selectAll('Document') + .select((eb) => [ + jsonObjectFrom( + eb + .selectFrom('User') + .select(['id', 'name', 'email']) + .whereRef('User.id', '=', 'Document.userId'), + ).as('User'), + jsonObjectFrom( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id'), + ).as('Recipient'), + jsonObjectFrom( + eb.selectFrom('Team').select(['id', 'url']).whereRef('Team.id', '=', 'Document.teamId'), + ).as('team'), + ]) + .where(({ eb, or, and, not, exists, selectFrom }) => + and([ + eb('Document.title', 'ilike', `${term}`), + or([ + eb('Document.status', '=', 'COMPLETED'), + and([ + not(eb('Document.status', 'ilike', 'COMPLETED')), + eb('Document.deletedAt', '=', null), + ]), + ]), + ]), + ) + .offset(Math.max(page - 1, 0) * perPage) + .limit(perPage) + .orderBy(orderByColumn, orderByDirection) + .execute(); + + console.log('dataQuery', dataQuery); + const [data, count] = await Promise.all([ prisma.document.findMany({ where: whereClause, From f520e0a7a6bb6e364cff73bde62f2ba131a96466 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:23:42 +0200 Subject: [PATCH 012/100] chore: converting to kysely --- .../server-only/document/find-documents.ts | 174 ++++++++++++++++-- 1 file changed, 156 insertions(+), 18 deletions(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 6ddafd97f..d5d0b8e7b 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,4 +1,5 @@ -import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { sql } from 'kysely'; +import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import { DateTime } from 'luxon'; import { P, match } from 'ts-pattern'; @@ -76,7 +77,7 @@ export const findDocuments = async ({ ]) .innerJoin('TeamMember', 'TeamMember.teamId', 'Team.id') .where('TeamMember.userId', '=', userId) - .executeTakeFirstOrThrow(); + .execute(); } return { @@ -145,7 +146,7 @@ export const findDocuments = async ({ }; } - const dataQuery = await prisma.$kysely + let dataQuery = prisma.$kysely .selectFrom('Document') .selectAll('Document') .select((eb) => [ @@ -155,7 +156,7 @@ export const findDocuments = async ({ .select(['id', 'name', 'email']) .whereRef('User.id', '=', 'Document.userId'), ).as('User'), - jsonObjectFrom( + jsonArrayFrom( eb .selectFrom('Recipient') .selectAll('Recipient') @@ -164,25 +165,160 @@ export const findDocuments = async ({ jsonObjectFrom( eb.selectFrom('Team').select(['id', 'url']).whereRef('Team.id', '=', 'Document.teamId'), ).as('team'), - ]) - .where(({ eb, or, and, not, exists, selectFrom }) => - and([ - eb('Document.title', 'ilike', `${term}`), - or([ - eb('Document.status', '=', 'COMPLETED'), + ]); + + if (term && term.length >= 1) { + dataQuery = dataQuery.where('Document.title', 'ilike', `%${term}%`); + } + + if (period) { + const daysAgo = parseInt(period.replace(/d$/, ''), 10); + const startOfPeriod = DateTime.now().minus({ days: daysAgo }).startOf('day'); + dataQuery = dataQuery.where('Document.createdAt', '>=', startOfPeriod.toJSDate()); + } + + if (senderIds && senderIds.length > 0) { + dataQuery = dataQuery.where('Document.userId', 'in', senderIds); + } + + if (team) { + console.log('team'); + } else if (user) { + if (ExtendedDocumentStatus.ALL) { + console.log('inside EXTENDED_DOCUMENT_STATUS.ALL'); + dataQuery = dataQuery.where(({ eb, or, and, exists }) => { + return or([ + eb('Document.userId', '=', user.id), + eb('Document.teamId', '=', null), and([ - not(eb('Document.status', 'ilike', 'COMPLETED')), - eb('Document.deletedAt', '=', null), + eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.COMPLETED}`), + exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', user.email), + ), ]), - ]), - ]), - ) + and([ + eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.PENDING}`), + exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', user.email), + ), + ]), + ]); + }); + } + // } else if (ExtendedDocumentStatus.INBOX) { + // dataQuery = dataQuery.where(({ eb, and, not, exists }) => { + // return and([ + // not(eb(sql`status::text`, '=', ExtendedDocumentStatus.DRAFT)), + // exists( + // eb + // .selectFrom('Recipient') + // .selectAll('Recipient') + // .whereRef('Recipient.documentId', '=', 'Document.id') + // .where('Recipient.email', '=', user.email) + // .where(sql`Recipient.signingStatus::text`, '=', SigningStatus.NOT_SIGNED) + // .where('Recipient.role', '<>', RecipientRole.CC), + // ), + // ]); + // }); + // } else if (ExtendedDocumentStatus.DRAFT) { + // dataQuery = dataQuery.where(({ eb, and }) => { + // return and([ + // eb('Document.userId', '=', user.id), + // eb('Document.teamId', '=', null), + // eb(sql`status::text`, '=', ExtendedDocumentStatus.DRAFT), + // ]); + // }); + // } else if (ExtendedDocumentStatus.PENDING) { + // dataQuery = dataQuery.where(({ eb, or, and, exists }) => { + // return or([ + // and([ + // eb('Document.userId', '=', user.id), + // eb('Document.teamId', '=', null), + // eb(sql`status::text`, '=', ExtendedDocumentStatus.PENDING), + // ]), + // and([ + // eb(sql`status::text`, '=', ExtendedDocumentStatus.PENDING), + // exists( + // eb + // .selectFrom('Recipient') + // .selectAll('Recipient') + // .whereRef('Recipient.documentId', '=', 'Document.id') + // .where('Recipient.email', '=', user.email) + // .where(sql`Recipient.signingStatus::text`, '=', SigningStatus.SIGNED) + // .where('Recipient.role', '<>', RecipientRole.CC), + // ), + // ]), + // ]); + // }); + // } else if (ExtendedDocumentStatus.COMPLETED) { + // dataQuery = dataQuery.where(({ eb, or, exists, and }) => { + // return or([ + // and([ + // eb('Document.userId', '=', user.id), + // eb('Document.teamId', '=', null), + // eb(sql`status::text`, '=', ExtendedDocumentStatus.COMPLETED), + // ]), + // and([ + // eb(sql`status::text`, '=', ExtendedDocumentStatus.COMPLETED), + // exists( + // eb + // .selectFrom('Recipient') + // .selectAll('Recipient') + // .whereRef('Recipient.documentId', '=', 'Document.id') + // .where('Recipient.email', '=', user.email), + // ), + // ]), + // ]); + // }); + // } + } else { + return { + data: [], + count: 0, + currentPage: 1, + perPage, + totalPages: 0, + }; + } + + // dataQuery = dataQuery.where(({ eb, or, and }) => + // and([ + // or([ + // eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.COMPLETED}`), + // and([ + // eb(sql`"Document"."status"::text`, '<>', sql`${ExtendedDocumentStatus.COMPLETED}`), + // eb('Document.deletedAt', '=', null), + // ]), + // ]), + // ]), + // ); + + const finalQuery = dataQuery .offset(Math.max(page - 1, 0) * perPage) .limit(perPage) - .orderBy(orderByColumn, orderByDirection) - .execute(); + .orderBy(orderByColumn, orderByDirection); - console.log('dataQuery', dataQuery); + console.log('\n'); + console.log('\n'); + console.log('\n'); + console.log('\n'); + console.log('\n'); + console.log('finalQuery', finalQuery.compile()); + console.log('\n'); + console.log('\n'); + console.log('\n'); + console.log('\n'); + console.log('\n'); + + console.log('finalQuery', await finalQuery.execute()); const [data, count] = await Promise.all([ prisma.document.findMany({ @@ -214,6 +350,8 @@ export const findDocuments = async ({ }), ]); + console.log('prisma query', data); + const maskedData = data.map((document) => maskRecipientTokensForDocument({ document, From 409d8aa5a2458cbfedec5232d84d22b8279e115a Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:13:03 +0300 Subject: [PATCH 013/100] chore: almost done? --- .../server-only/document/find-documents.ts | 344 +++++++++++++----- 1 file changed, 254 insertions(+), 90 deletions(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index d5d0b8e7b..50761bd87 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -182,16 +182,181 @@ export const findDocuments = async ({ } if (team) { - console.log('team'); + console.log('team acc'); + + if (ExtendedDocumentStatus.ALL === status) { + dataQuery = dataQuery.where((eb) => { + const ors = [eb('Document.teamId', '=', team.id)]; + + if (team.teamEmail) { + ors.push( + eb.and([ + eb.not(eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT)), + eb.exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', team.teamEmail.email), + ), + ]), + ); + + ors.push( + eb.exists( + eb + .selectFrom('User') + .selectAll('User') + .where('User.email', '=', team.teamEmail.email), + ), + ); + } + + return eb.or(ors); + }); + } else if (ExtendedDocumentStatus.INBOX === status) { + if (team.teamEmail) { + dataQuery = dataQuery.where((eb) => { + const ands = []; + + if (team.teamEmail) { + ands.push( + eb.and([ + eb.not( + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT), + ), + eb.exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', team.teamEmail.email) + .where( + sql`CAST("Recipient"."signingStatus" AS TEXT)`, + '=', + SigningStatus.NOT_SIGNED, + ) + .where(sql`CAST("Recipient"."role" AS TEXT)`, '!=', RecipientRole.CC), + ), + ]), + ); + } + + return eb.and(ands); + }); + } + } else if (ExtendedDocumentStatus.DRAFT === status) { + dataQuery = dataQuery.where((eb) => { + const ors = [ + eb.and([ + eb('Document.teamId', '=', team.id), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT), + ]), + ]; + + if (team.teamEmail) { + ors.push( + eb.and([ + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT), + eb.exists( + eb + .selectFrom('User') + .selectAll('User') + .whereRef('userId', '=', 'Document.id') + .where('User.email', '=', team.teamEmail.email), + ), + ]), + ); + } + + return eb.or(ors); + }); + } else if (ExtendedDocumentStatus.PENDING === status) { + dataQuery = dataQuery.where((eb) => { + const ors = [ + eb.and([ + eb('Document.teamId', '=', team.id), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.PENDING), + ]), + ]; + + if (team.teamEmail) { + ors.push( + eb.or([ + eb.and([ + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.PENDING), + eb.and([ + eb.exists( + eb + .selectFrom('User') + .selectAll('User') + .whereRef('userId', '=', 'Document.id') + .where('User.email', '=', team.teamEmail.email), + ), + eb.exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .where('Recipient.email', '=', team.teamEmail.email) + .where( + sql`CAST("Recipient"."signingStatus" AS TEXT)`, + '=', + SigningStatus.SIGNED, + ) + .where(sql`CAST("Recipient"."role" AS TEXT)`, '!=', RecipientRole.CC), + ), + ]), + ]), + ]), + ); + } + + return eb.or(ors); + }); + } else if (ExtendedDocumentStatus.COMPLETED === status) { + dataQuery = dataQuery.where((eb) => { + const ors = []; + + if (team.teamEmail) { + ors.push( + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED), + eb.or([ + eb('Document.teamId', '=', team.id), + eb.and([ + eb.exists( + eb + .selectFrom('User') + .selectAll('User') + .whereRef('userId', '=', 'Document.id') + .where('User.email', '=', team.teamEmail.email), + ), + eb.exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .where('Recipient.email', '=', team.teamEmail.email) + .where( + sql`CAST("Recipient"."signingStatus" AS TEXT)`, + '=', + SigningStatus.SIGNED, + ) + .where(sql`CAST("Recipient"."role" AS TEXT)`, '!=', RecipientRole.CC), + ), + ]), + ]), + ); + } + + return eb.and(ors); + }); + } } else if (user) { - if (ExtendedDocumentStatus.ALL) { - console.log('inside EXTENDED_DOCUMENT_STATUS.ALL'); + if (ExtendedDocumentStatus.ALL === status) { dataQuery = dataQuery.where(({ eb, or, and, exists }) => { return or([ - eb('Document.userId', '=', user.id), - eb('Document.teamId', '=', null), + and([eb('Document.userId', '=', user.id), eb('Document.teamId', 'is', null)]), and([ - eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.COMPLETED}`), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED), exists( eb .selectFrom('Recipient') @@ -201,7 +366,73 @@ export const findDocuments = async ({ ), ]), and([ - eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.PENDING}`), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.PENDING), + exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', user.email), + ), + ]), + ]); + }); + } else if (ExtendedDocumentStatus.INBOX === status) { + dataQuery = dataQuery.where(({ eb, and, not, exists }) => { + return and([ + not(eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT)), + exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', user.email) + .where(sql`CAST("Recipient"."signingStatus" AS TEXT)`, '=', SigningStatus.NOT_SIGNED) + .where(sql`CAST("Recipient"."role" AS TEXT)`, '!=', RecipientRole.CC), + ), + ]); + }); + } else if (ExtendedDocumentStatus.DRAFT === status) { + dataQuery = dataQuery.where(({ eb, and }) => { + return and([ + eb('Document.userId', '=', user.id), + eb('Document.teamId', 'is', null), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.DRAFT), + ]); + }); + } else if (ExtendedDocumentStatus.PENDING === status) { + dataQuery = dataQuery.where(({ eb, or, and, exists }) => { + return or([ + and([ + eb('Document.userId', '=', user.id), + eb('Document.teamId', 'is', null), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.PENDING), + ]), + and([ + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.PENDING), + exists( + eb + .selectFrom('Recipient') + .selectAll('Recipient') + .whereRef('Recipient.documentId', '=', 'Document.id') + .where('Recipient.email', '=', user.email) + .where(sql`CAST("Recipient"."signingStatus" AS TEXT)`, '=', SigningStatus.SIGNED) + .where(sql`CAST("Recipient"."role" AS TEXT)`, '!=', RecipientRole.CC), + ), + ]), + ]); + }); + } else if (ExtendedDocumentStatus.COMPLETED === status) { + console.log('completed bitches'); + dataQuery = dataQuery.where(({ eb, or, exists, and }) => { + return or([ + and([ + eb('Document.userId', '=', user.id), + eb('Document.teamId', 'is', null), + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED), + ]), + and([ + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED), exists( eb .selectFrom('Recipient') @@ -213,72 +444,6 @@ export const findDocuments = async ({ ]); }); } - // } else if (ExtendedDocumentStatus.INBOX) { - // dataQuery = dataQuery.where(({ eb, and, not, exists }) => { - // return and([ - // not(eb(sql`status::text`, '=', ExtendedDocumentStatus.DRAFT)), - // exists( - // eb - // .selectFrom('Recipient') - // .selectAll('Recipient') - // .whereRef('Recipient.documentId', '=', 'Document.id') - // .where('Recipient.email', '=', user.email) - // .where(sql`Recipient.signingStatus::text`, '=', SigningStatus.NOT_SIGNED) - // .where('Recipient.role', '<>', RecipientRole.CC), - // ), - // ]); - // }); - // } else if (ExtendedDocumentStatus.DRAFT) { - // dataQuery = dataQuery.where(({ eb, and }) => { - // return and([ - // eb('Document.userId', '=', user.id), - // eb('Document.teamId', '=', null), - // eb(sql`status::text`, '=', ExtendedDocumentStatus.DRAFT), - // ]); - // }); - // } else if (ExtendedDocumentStatus.PENDING) { - // dataQuery = dataQuery.where(({ eb, or, and, exists }) => { - // return or([ - // and([ - // eb('Document.userId', '=', user.id), - // eb('Document.teamId', '=', null), - // eb(sql`status::text`, '=', ExtendedDocumentStatus.PENDING), - // ]), - // and([ - // eb(sql`status::text`, '=', ExtendedDocumentStatus.PENDING), - // exists( - // eb - // .selectFrom('Recipient') - // .selectAll('Recipient') - // .whereRef('Recipient.documentId', '=', 'Document.id') - // .where('Recipient.email', '=', user.email) - // .where(sql`Recipient.signingStatus::text`, '=', SigningStatus.SIGNED) - // .where('Recipient.role', '<>', RecipientRole.CC), - // ), - // ]), - // ]); - // }); - // } else if (ExtendedDocumentStatus.COMPLETED) { - // dataQuery = dataQuery.where(({ eb, or, exists, and }) => { - // return or([ - // and([ - // eb('Document.userId', '=', user.id), - // eb('Document.teamId', '=', null), - // eb(sql`status::text`, '=', ExtendedDocumentStatus.COMPLETED), - // ]), - // and([ - // eb(sql`status::text`, '=', ExtendedDocumentStatus.COMPLETED), - // exists( - // eb - // .selectFrom('Recipient') - // .selectAll('Recipient') - // .whereRef('Recipient.documentId', '=', 'Document.id') - // .where('Recipient.email', '=', user.email), - // ), - // ]), - // ]); - // }); - // } } else { return { data: [], @@ -289,37 +454,36 @@ export const findDocuments = async ({ }; } - // dataQuery = dataQuery.where(({ eb, or, and }) => - // and([ - // or([ - // eb(sql`"Document"."status"::text`, '=', sql`${ExtendedDocumentStatus.COMPLETED}`), - // and([ - // eb(sql`"Document"."status"::text`, '<>', sql`${ExtendedDocumentStatus.COMPLETED}`), - // eb('Document.deletedAt', '=', null), - // ]), - // ]), - // ]), - // ); + dataQuery = dataQuery.where(({ eb, or, and, not }) => { + return and([ + or([ + eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED), + and([ + not(eb(sql`CAST("Document"."status" AS TEXT)`, '=', ExtendedDocumentStatus.COMPLETED)), + eb('Document.deletedAt', 'is', null), + ]), + ]), + ]); + }); - const finalQuery = dataQuery + const finalQuery = await dataQuery .offset(Math.max(page - 1, 0) * perPage) .limit(perPage) - .orderBy(orderByColumn, orderByDirection); + .orderBy(orderByColumn, orderByDirection) + .execute(); console.log('\n'); console.log('\n'); console.log('\n'); console.log('\n'); console.log('\n'); - console.log('finalQuery', finalQuery.compile()); + console.log('finalQuery', finalQuery); console.log('\n'); console.log('\n'); console.log('\n'); console.log('\n'); console.log('\n'); - console.log('finalQuery', await finalQuery.execute()); - const [data, count] = await Promise.all([ prisma.document.findMany({ where: whereClause, @@ -350,7 +514,7 @@ export const findDocuments = async ({ }), ]); - console.log('prisma query', data); + console.log('prisma query data', data); const maskedData = data.map((document) => maskRecipientTokensForDocument({ From 82792864de781fa58d38fe021fc5568db87a8a22 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:18:07 +0300 Subject: [PATCH 014/100] chore: remove unintended console logs --- packages/lib/server-only/document/find-documents.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 50761bd87..300b9cb1b 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -182,8 +182,6 @@ export const findDocuments = async ({ } if (team) { - console.log('team acc'); - if (ExtendedDocumentStatus.ALL === status) { dataQuery = dataQuery.where((eb) => { const ors = [eb('Document.teamId', '=', team.id)]; @@ -423,7 +421,6 @@ export const findDocuments = async ({ ]); }); } else if (ExtendedDocumentStatus.COMPLETED === status) { - console.log('completed bitches'); dataQuery = dataQuery.where(({ eb, or, exists, and }) => { return or([ and([ From 7f7e7da3afa054a412938bd437899107ae20e0e5 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:23:19 +0300 Subject: [PATCH 015/100] chore: format the final query data and return it --- .../lib/server-only/document/find-documents.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 300b9cb1b..c9c39046b 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -513,7 +513,23 @@ export const findDocuments = async ({ console.log('prisma query data', data); - const maskedData = data.map((document) => + const formattedFinalQuery = data.map((item) => ({ + id: item.id, + userId: item.userId, + title: item.title, + status: item.status, + documentDataId: item.documentDataId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + completedAt: item.completedAt, + deletedAt: item.deletedAt, + teamId: item.teamId, + team: item.team, + User: item.User, + Recipient: item.Recipient, + })); + + const maskedData = formattedFinalQuery.map((document) => maskRecipientTokensForDocument({ document, user, From 60c26a9f75999763ba7c997ff6274838d940ddda Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:53:40 +0300 Subject: [PATCH 016/100] chore: finished converting to kysely --- .../server-only/document/find-documents.ts | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index c9c39046b..547506907 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -469,18 +469,6 @@ export const findDocuments = async ({ .orderBy(orderByColumn, orderByDirection) .execute(); - console.log('\n'); - console.log('\n'); - console.log('\n'); - console.log('\n'); - console.log('\n'); - console.log('finalQuery', finalQuery); - console.log('\n'); - console.log('\n'); - console.log('\n'); - console.log('\n'); - console.log('\n'); - const [data, count] = await Promise.all([ prisma.document.findMany({ where: whereClause, @@ -511,23 +499,52 @@ export const findDocuments = async ({ }), ]); - console.log('prisma query data', data); + const formattedFinalQuery = finalQuery.map((item) => { + return { + id: item.id, + userId: item.userId, + title: item.title, + templateId: item.templateId, + status: item.status, + documentDataId: item.documentDataId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + completedAt: item.completedAt, + deletedAt: item.deletedAt, + teamId: item.teamId, + team: item.team + ? { + id: item.team.id, + url: item.team.url, + } + : null, + User: { + id: item.User?.id, + name: item.User?.name, // Ensure this can be string or null as per the expected type + email: item.User?.email, + }, + Recipient: Array.isArray(item.Recipient) + ? item.Recipient.map((recipient) => ({ + id: recipient?.id, + documentId: recipient?.documentId, + templateId: recipient?.templateId, + email: recipient?.email, + name: recipient?.name, + role: recipient?.role, + signingStatus: recipient?.signingStatus, + token: recipient?.token, + expired: recipient?.expired, + readStatus: recipient?.readStatus, + sendStatus: recipient?.sendStatus, + signedAt: recipient?.signedAt, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + })) + : [], + }; + }); - const formattedFinalQuery = data.map((item) => ({ - id: item.id, - userId: item.userId, - title: item.title, - status: item.status, - documentDataId: item.documentDataId, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - completedAt: item.completedAt, - deletedAt: item.deletedAt, - teamId: item.teamId, - team: item.team, - User: item.User, - Recipient: item.Recipient, - })); + console.log('formattedFinalQuery', formattedFinalQuery); const maskedData = formattedFinalQuery.map((document) => maskRecipientTokensForDocument({ From 02921e53de90bf5a69a9df7561235f4ae0412850 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:51:41 +0300 Subject: [PATCH 017/100] chore: trying to fix the issues --- .../server-only/document/find-documents.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 547506907..246e4521b 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -512,16 +512,20 @@ export const findDocuments = async ({ completedAt: item.completedAt, deletedAt: item.deletedAt, teamId: item.teamId, - team: item.team - ? { - id: item.team.id, - url: item.team.url, - } - : null, + team: + item.team && 'id' in item.team && 'url' in item.team + ? { + id: item.team.id, + url: item.team.url, + } + : null, User: { - id: item.User?.id, - name: item.User?.name, // Ensure this can be string or null as per the expected type - email: item.User?.email, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + id: (item.User as { id: number }).id, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + name: (item.User as { name: string | null }).name, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + email: (item.User as { email: string }).email, }, Recipient: Array.isArray(item.Recipient) ? item.Recipient.map((recipient) => ({ @@ -544,8 +548,6 @@ export const findDocuments = async ({ }; }); - console.log('formattedFinalQuery', formattedFinalQuery); - const maskedData = formattedFinalQuery.map((document) => maskRecipientTokensForDocument({ document, From 0f87dc047b475a65b9c76cf2735dad8c406fde28 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:27:46 +0300 Subject: [PATCH 018/100] fix: swagger documentation authentication (#1037) ## Summary by CodeRabbit - **Refactor** - Enhanced the API specification generation process to include operation IDs, security schemes, and security definitions more efficiently. --------- Co-authored-by: Lucas Smith --- packages/api/v1/openapi.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/api/v1/openapi.ts b/packages/api/v1/openapi.ts index af0582195..55ec4d7fd 100644 --- a/packages/api/v1/openapi.ts +++ b/packages/api/v1/openapi.ts @@ -2,16 +2,34 @@ import { generateOpenApi } from '@ts-rest/open-api'; import { ApiContractV1 } from './contract'; -export const OpenAPIV1 = generateOpenApi( - ApiContractV1, - { - info: { - title: 'Documenso API', - version: '1.0.0', - description: 'The Documenso API for retrieving, creating, updating and deleting documents.', +export const OpenAPIV1 = Object.assign( + generateOpenApi( + ApiContractV1, + { + info: { + title: 'Documenso API', + version: '1.0.0', + description: 'The Documenso API for retrieving, creating, updating and deleting documents.', + }, }, - }, + { + setOperationId: true, + }, + ), { - setOperationId: true, + components: { + securitySchemes: { + authorization: { + type: 'apiKey', + in: 'header', + name: 'Authorization', + }, + }, + }, + security: [ + { + authorization: [], + }, + ], }, ); From c8a09099a372568ed1c2b0ac47acac3ceb2be243 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:29:56 +0300 Subject: [PATCH 019/100] fix: mask recipient token (#1051) The searchDocuments function is used for the shortcuts commands, afaik. The function returns the documents that match the user query (if any), alongside all their recipients. The reason for that is so it can build the path for the document. E.g. if you're the document owner, the document path will be `..../documents/{id}`. But if you're a signer for example, the document path (link) will be `..../sign/{token}`. So instead of doing that on the frontend, I moved it to the backend. At least that's what I understood. If I'm wrong, please correct me. ## Summary by CodeRabbit - **New Features** - Enhanced the `CommandMenu` component to simplify search result generation and improve document link management based on user roles. - **Refactor** - Updated document search logic to include recipient token masking and refined document mapping. - **Style** - Minor formatting improvement in document routing code. --- .../(dashboard)/common/command-menu.tsx | 20 +++-------------- .../document/search-documents-with-keyword.ts | 22 ++++++++++++------- .../trpc/server/document-router/router.ts | 1 + 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index bdc6c2064..812efd4b9 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Loader, Monitor, Moon, Sun } from 'lucide-react'; -import { useSession } from 'next-auth/react'; import { useTheme } from 'next-themes'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -18,7 +17,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META, } from '@documenso/lib/constants/trpc'; -import type { Document, Recipient } from '@documenso/prisma/client'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { CommandDialog, @@ -71,7 +69,6 @@ export type CommandMenuProps = { export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const { setTheme } = useTheme(); - const { data: session } = useSession(); const router = useRouter(); @@ -93,17 +90,6 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { }, ); - const isOwner = useCallback( - (document: Document) => document.userId === session?.user.id, - [session?.user.id], - ); - - const getSigningLink = useCallback( - (recipients: Recipient[]) => - `/sign/${recipients.find((r) => r.email === session?.user.email)?.token}`, - [session?.user.email], - ); - const searchResults = useMemo(() => { if (!searchDocumentsData) { return []; @@ -111,10 +97,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { return searchDocumentsData.map((document) => ({ label: document.title, - path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient), - value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '), + path: document.path, + value: document.value, })); - }, [searchDocumentsData, isOwner, getSigningLink]); + }, [searchDocumentsData]); const currentPage = pages[pages.length - 1]; diff --git a/packages/lib/server-only/document/search-documents-with-keyword.ts b/packages/lib/server-only/document/search-documents-with-keyword.ts index 8125ae900..a9139f5d3 100644 --- a/packages/lib/server-only/document/search-documents-with-keyword.ts +++ b/packages/lib/server-only/document/search-documents-with-keyword.ts @@ -1,7 +1,6 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@documenso/prisma/client'; - -import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document'; +import type { Document, Recipient, User } from '@documenso/prisma/client'; export type SearchDocumentsWithKeywordOptions = { query: string; @@ -79,12 +78,19 @@ export const searchDocumentsWithKeyword = async ({ take: limit, }); - const maskedDocuments = documents.map((document) => - maskRecipientTokensForDocument({ - document, - user, - }), - ); + const isOwner = (document: Document, user: User) => document.userId === user.id; + const getSigningLink = (recipients: Recipient[], user: User) => + `/sign/${recipients.find((r) => r.email === user.email)?.token}`; + + const maskedDocuments = documents.map((document) => { + const { Recipient, ...documentWithoutRecipient } = document; + + return { + ...documentWithoutRecipient, + path: isOwner(document, user) ? `/documents/${document.id}` : getSigningLink(Recipient, user), + value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '), + }; + }); return maskedDocuments; }; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 3cc61bef2..d12002674 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -358,6 +358,7 @@ export const documentRouter = router({ query, userId: ctx.user.id, }); + return documents; } catch (err) { console.error(err); From aa4b6f1723c43ff6461950598a203222c2c127aa Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 15 Apr 2024 14:22:34 +0530 Subject: [PATCH 020/100] feat: updated mobile header (#1004) **Description:** - Updated mobile header with respect to latest designs ## Summary by CodeRabbit - **New Features** - Added a new `showText` property to the `MenuSwitcher` component to control text visibility. - Added a `textSectionClassName` property to the `AvatarWithText` component for conditional text section styling. - Updated the `CommandDialog` and `DialogContent` components with new positioning and styling properties. - **Style Updates** - Adjusted text size responsiveness in the `Hero` component for various screen sizes. - Modified text truncation and input styling in the `Widget` component. - Changed the width of the `SheetContent` element in `MobileNavigation` and adjusted footer layout. - **Documentation** - Added instructions for certificate placement in `SIGNING.md`. - **Refactor** - Standardized type imports across various components and utilities for improved type checking. --------- Signed-off-by: Adithya Krishna Signed-off-by: Adithya Krishna Co-authored-by: David Nguyen --- SIGNING.md | 3 +- apps/marketing/src/api/claim-plan/fetcher.ts | 3 +- .../src/app/(marketing)/open/bar-metrics.tsx | 1 + .../app/(marketing)/open/funding-raised.tsx | 1 + .../src/app/(marketing)/open/metric-card.tsx | 2 +- .../src/app/(marketing)/open/salary-bands.tsx | 2 +- .../app/(marketing)/oss-friends/container.tsx | 5 +- apps/marketing/src/app/robots.ts | 2 +- apps/marketing/src/app/sitemap.ts | 2 +- .../src/components/(marketing)/hero.tsx | 2 +- .../(marketing)/open-build-template-bento.tsx | 2 +- .../src/components/(marketing)/widget.tsx | 4 +- .../components/form/form-error-message.tsx | 2 +- .../src/components/ui/background.tsx | 2 +- apps/marketing/src/providers/next-theme.tsx | 2 +- .../(dashboard)/layout/menu-switcher.tsx | 5 +- .../(dashboard)/layout/mobile-navigation.tsx | 4 +- packages/ui/primitives/avatar.tsx | 5 +- packages/ui/primitives/command.tsx | 6 ++- packages/ui/primitives/dialog.tsx | 49 +++++++++++-------- 20 files changed, 62 insertions(+), 42 deletions(-) diff --git a/SIGNING.md b/SIGNING.md index d1942ed8a..d8f664cee 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -17,7 +17,8 @@ For the digital signature of your documents you need a signing certificate in .p `openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt` 4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**) -5. Place the certificate `/apps/web/resources/certificate.p12` + +5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created) ## Docker diff --git a/apps/marketing/src/api/claim-plan/fetcher.ts b/apps/marketing/src/api/claim-plan/fetcher.ts index 0e533be5e..629ab7270 100644 --- a/apps/marketing/src/api/claim-plan/fetcher.ts +++ b/apps/marketing/src/api/claim-plan/fetcher.ts @@ -1,4 +1,5 @@ -import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types'; +import type { TClaimPlanRequestSchema } from './types'; +import { ZClaimPlanResponseSchema } from './types'; export const claimPlan = async ({ name, diff --git a/apps/marketing/src/app/(marketing)/open/bar-metrics.tsx b/apps/marketing/src/app/(marketing)/open/bar-metrics.tsx index fb9c61f11..2d93b2e34 100644 --- a/apps/marketing/src/app/(marketing)/open/bar-metrics.tsx +++ b/apps/marketing/src/app/(marketing)/open/bar-metrics.tsx @@ -55,6 +55,7 @@ export const BarMetric = & { export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => { const formattedData = data.map((item) => ({ amount: Number(item.amount), + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions date: formatMonth(item.date as string), })); diff --git a/apps/marketing/src/app/(marketing)/open/metric-card.tsx b/apps/marketing/src/app/(marketing)/open/metric-card.tsx index 6235f4f5e..f7bf59e62 100644 --- a/apps/marketing/src/app/(marketing)/open/metric-card.tsx +++ b/apps/marketing/src/app/(marketing)/open/metric-card.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { cn } from '@documenso/ui/lib/utils'; diff --git a/apps/marketing/src/app/(marketing)/open/salary-bands.tsx b/apps/marketing/src/app/(marketing)/open/salary-bands.tsx index 31c254157..41754cff6 100644 --- a/apps/marketing/src/app/(marketing)/open/salary-bands.tsx +++ b/apps/marketing/src/app/(marketing)/open/salary-bands.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { cn } from '@documenso/ui/lib/utils'; import { diff --git a/apps/marketing/src/app/(marketing)/oss-friends/container.tsx b/apps/marketing/src/app/(marketing)/oss-friends/container.tsx index 0f1f66664..f2ea4e855 100644 --- a/apps/marketing/src/app/(marketing)/oss-friends/container.tsx +++ b/apps/marketing/src/app/(marketing)/oss-friends/container.tsx @@ -2,13 +2,14 @@ import Link from 'next/link'; -import { Variants, motion } from 'framer-motion'; +import type { Variants } from 'framer-motion'; +import { motion } from 'framer-motion'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card'; -import { TOSSFriendsSchema } from './schema'; +import type { TOSSFriendsSchema } from './schema'; const ContainerVariants: Variants = { initial: { diff --git a/apps/marketing/src/app/robots.ts b/apps/marketing/src/app/robots.ts index cc718ff25..a222a892e 100644 --- a/apps/marketing/src/app/robots.ts +++ b/apps/marketing/src/app/robots.ts @@ -1,4 +1,4 @@ -import { MetadataRoute } from 'next'; +import type { MetadataRoute } from 'next'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; diff --git a/apps/marketing/src/app/sitemap.ts b/apps/marketing/src/app/sitemap.ts index b9becde3b..4913402f9 100644 --- a/apps/marketing/src/app/sitemap.ts +++ b/apps/marketing/src/app/sitemap.ts @@ -1,4 +1,4 @@ -import { MetadataRoute } from 'next'; +import type { MetadataRoute } from 'next'; import { allBlogPosts, allGenericPages } from 'contentlayer/generated'; diff --git a/apps/marketing/src/components/(marketing)/hero.tsx b/apps/marketing/src/components/(marketing)/hero.tsx index f416cc4ca..5809bd695 100644 --- a/apps/marketing/src/components/(marketing)/hero.tsx +++ b/apps/marketing/src/components/(marketing)/hero.tsx @@ -96,7 +96,7 @@ export const Hero = ({ className, ...props }: HeroProps) => { variants={HeroTitleVariants} initial="initial" animate="animate" - className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]" + className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]" > Document signing, finally open source. diff --git a/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx b/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx index 3c76c3547..4d4d6ad8a 100644 --- a/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx +++ b/apps/marketing/src/components/(marketing)/open-build-template-bento.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import Image from 'next/image'; diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index 8b6c3cd8e..c4611746a 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -346,7 +346,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { {signatureText && (

{signatureText} @@ -360,7 +360,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { > , 'viewBox'>; diff --git a/apps/marketing/src/providers/next-theme.tsx b/apps/marketing/src/providers/next-theme.tsx index 6e9122e5a..d15114606 100644 --- a/apps/marketing/src/providers/next-theme.tsx +++ b/apps/marketing/src/providers/next-theme.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; -import { ThemeProviderProps } from 'next-themes/dist/types'; +import type { ThemeProviderProps } from 'next-themes/dist/types'; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children}; diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx index 95f959ab2..bb8429adc 100644 --- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx +++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx @@ -93,7 +93,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp diff --git a/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx b/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx index a6009e7b5..ff5428298 100644 --- a/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx +++ b/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx @@ -46,7 +46,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat return ( - +

- © {new Date().getFullYear()} Documenso, Inc. All rights reserved. + © {new Date().getFullYear()} Documenso, Inc.
All rights reserved.

diff --git a/packages/ui/primitives/avatar.tsx b/packages/ui/primitives/avatar.tsx index c80e3a658..aa2f522fe 100644 --- a/packages/ui/primitives/avatar.tsx +++ b/packages/ui/primitives/avatar.tsx @@ -55,6 +55,8 @@ type AvatarWithTextProps = { primaryText: React.ReactNode; secondaryText?: React.ReactNode; rightSideComponent?: React.ReactNode; + // Optional class to hide/show the text beside avatar + textSectionClassName?: string; }; const AvatarWithText = ({ @@ -64,6 +66,7 @@ const AvatarWithText = ({ primaryText, secondaryText, rightSideComponent, + textSectionClassName, }: AvatarWithTextProps) => (
{avatarFallback} -
+
{primaryText} {secondaryText}
diff --git a/packages/ui/primitives/command.tsx b/packages/ui/primitives/command.tsx index 89777d417..89084ac12 100644 --- a/packages/ui/primitives/command.tsx +++ b/packages/ui/primitives/command.tsx @@ -32,7 +32,11 @@ type CommandDialogProps = DialogProps & { const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => { return ( - + & { position?: 'start' | 'end' | 'center'; hideClose?: boolean; + /* Below prop is to add additional classes to the overlay */ + overlayClassName?: string; } ->(({ className, children, position = 'start', hideClose = false, ...props }, ref) => ( - - - - {children} - {!hideClose && ( - - - Close - - )} - - -)); +>( + ( + { className, children, overlayClassName, position = 'start', hideClose = false, ...props }, + ref, + ) => ( + + + + {children} + {!hideClose && ( + + + Close + + )} + + + ), +); DialogContent.displayName = DialogPrimitive.Content.displayName; From 0eeccfd64356389e448091612f1bec4a2cf0dc4e Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:18:06 +0300 Subject: [PATCH 021/100] feat: add myself as signer (#1091) ## Description This PR introduces the ability to add oneself as a signer by simply clicking a button, rather than filling the details manually. ### "Add Myself" in the document creation flow https://github.com/documenso/documenso/assets/25515812/0de762e3-563a-491f-a742-9078bf1d627d ### "Add Myself" in the document template creation flow https://github.com/documenso/documenso/assets/25515812/775bae01-3f5a-4b24-abbf-a47b14ec594a ## Related Issue Addresses [#113](https://github.com/documenso/backlog-internal/issues/113) ## Changes Made Added a new button that grabs the details of the logged-in user and fills the fields *(email, name, and role)* automatically when clicked. ## Testing Performed Tested the changes locally through the UI. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Introduced the ability for users to add themselves as signers within documents seamlessly. - **Enhancements** - Improved form handling logic to accommodate new self-signer functionality. - Enhanced user interface elements to support the addition of self as a signer, including a new "Add myself" button and disabling input fields during the process. --- .../primitives/document-flow/add-signers.tsx | 77 ++++++++++++++----- .../add-template-placeholder-recipients.tsx | 33 +++++++- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 7af4a06bc..27839a453 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -5,6 +5,7 @@ import React, { useId, useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { motion } from 'framer-motion'; import { InfoIcon, Plus, Trash } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useFieldArray, useForm } from 'react-hook-form'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; @@ -60,6 +61,8 @@ export const AddSignersFormPartial = ({ }: AddSignersFormProps) => { const { toast } = useToast(); const { remaining } = useLimits(); + const { data: session } = useSession(); + const user = session?.user; const initialId = useId(); @@ -135,6 +138,16 @@ export const AddSignersFormPartial = ({ ); }; + const onAddSelfSigner = () => { + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + actionAuth: undefined, + }); + }; + const onAddSigner = () => { appendSigner({ formId: nanoid(12), @@ -209,8 +222,12 @@ export const AddSignersFormPartial = ({ @@ -237,8 +254,12 @@ export const AddSignersFormPartial = ({ @@ -403,32 +424,46 @@ export const AddSignersFormPartial = ({ > - - {!alwaysShowAdvancedSettings && isDocumentEnterprise && ( -
- setShowAdvancedSettings(Boolean(value))} - /> - - -
- )} +
+ + {!alwaysShowAdvancedSettings && isDocumentEnterprise && ( +
+ setShowAdvancedSettings(Boolean(value))} + /> + + +
+ )} diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index 87ec48ad1..08cfc4957 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -5,6 +5,7 @@ import React, { useId, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; import { Plus, Trash } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { nanoid } from '@documenso/lib/universal/id'; @@ -41,6 +42,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ onSubmit, }: AddTemplatePlaceholderRecipientsFormProps) => { const initialId = useId(); + const { data: session } = useSession(); + const user = session?.user; const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() => recipients.length > 1 ? recipients.length + 1 : 2, ); @@ -50,6 +53,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ const { control, handleSubmit, + getValues, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema), @@ -85,6 +89,15 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ name: 'signers', }); + const onAddPlaceholderSelfRecipient = () => { + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + }); + }; + const onAddPlaceholderRecipient = () => { appendSigner({ formId: nanoid(12), @@ -203,11 +216,27 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ error={'signers__root' in errors && errors['signers__root']} /> -
- +
From db9899d29391f3cfaf7fdfd1b08b4d0dc86c1215 Mon Sep 17 00:00:00 2001 From: Deep Golani <54791570+deepgolani4@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:12:28 +0530 Subject: [PATCH 022/100] fix: duplicate modal instances from hotkey activation (#1058) ## Description Currently, when the command menu is opened using the Command+K hotkey, two modals are getting rendered. This is because the modals are mounted in two components: header and desktop-nav. Upon triggering the hotkey, both modals are rendered. ## Related Issue #1032 ## Changes Made The changes I made are in the desktop nav component. If the desktop nav receives the command menu state value and the state setter function, it will trigger only that. If not, it will trigger the state setter that is defined in the desktop nav. This way, we are preventing the modal from mounting two times. ## Testing Performed - Tested behaviour of command menu in the portal - Tested on browsers chrome, arc, safari, chrome, firefox ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Enhanced the navigation experience by integrating command menu state management directly within the `DesktopNav` component, allowing for smoother interactions and control. - **Refactor** - Simplified the handling of the command menu by removing the `CommandMenu` component and managing its functionality within `DesktopNav`. --------- Co-authored-by: David Nguyen --- .../components/(dashboard)/layout/desktop-nav.tsx | 15 +++++---------- .../src/components/(dashboard)/layout/header.tsx | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index 262e297d6..975ef7d0d 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -1,5 +1,3 @@ -'use client'; - import type { HTMLAttributes } from 'react'; import { useEffect, useState } from 'react'; @@ -12,8 +10,6 @@ import { getRootHref } from '@documenso/lib/utils/params'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { CommandMenu } from '../common/command-menu'; - const navigationLinks = [ { href: '/documents', @@ -25,13 +21,14 @@ const navigationLinks = [ }, ]; -export type DesktopNavProps = HTMLAttributes; +export type DesktopNavProps = HTMLAttributes & { + setIsCommandMenuOpen: (value: boolean) => void; +}; -export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { +export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => { const pathname = usePathname(); const params = useParams(); - const [open, setOpen] = useState(false); const [modifierKey, setModifierKey] = useState(() => 'Ctrl'); const rootHref = getRootHref(params, { returnEmptyRootString: true }); @@ -70,12 +67,10 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { ))}
- - -
- {uploadedFile ? ( - - - +
+
+
+
+
-
-
-
-
-
+

+ Uploaded Document +

-

- Uploaded Document -

- - - {uploadedFile.file.name} - - - - ) : ( - - )} -
+ + {uploadedFile.file.name} + + + + ) : ( + + )}
-
- + + + -
- - -
+ + + +
); From 3d3c53db023a5910792740bb52d575e79e841d9c Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:36:54 +0300 Subject: [PATCH 026/100] feat: add extra info for recipient roles (#1105) ## Description Add additional information for each role to help document owners understand what each role involves. ## Changes Made ![CleanShot 2024-04-16 at 10 24 19](https://github.com/documenso/documenso/assets/25515812/bac6cd7d-fbe2-4987-ac17-de08db882eda) ![CleanShot 2024-04-16 at 10 24 27](https://github.com/documenso/documenso/assets/25515812/1bd23021-e971-451a-8e36-df5db57687f7) ![CleanShot 2024-04-16 at 10 24 35](https://github.com/documenso/documenso/assets/25515812/e658e86e-7fa1-4a40-9ed9-317964388e61) ## Testing Performed Tested the changes locally. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Updated recipient role terminology and added tooltips in the `AddSignersFormPartial` component: - "Signer" changed to "Needs to sign" with tooltip - "Receives copy" changed to "Needs to approve" with tooltip - "Approver" changed to "Needs to view" with tooltip - "Viewer" changed to "Receives copy" with tooltip - Enhanced select dropdown options with icons and tooltips for different recipient roles in the `AddTemplatePlaceholderRecipients` component. --------- Co-authored-by: Timur Ercan --- .../primitives/document-flow/add-signers.tsx | 80 +++++++++++++++--- .../add-template-placeholder-recipients.tsx | 81 +++++++++++++++---- 2 files changed, 134 insertions(+), 27 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 27839a453..25169bcec 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -361,29 +361,83 @@ export const AddSignersFormPartial = ({
- {ROLE_ICONS[RecipientRole.SIGNER]} - Signer -
-
- - -
- {ROLE_ICONS[RecipientRole.CC]} - Receives copy +
+ {ROLE_ICONS[RecipientRole.SIGNER]} + Needs to sign +
+ + + + + +

+ The recipient is required to sign the document for it to be + completed. +

+
+
- {ROLE_ICONS[RecipientRole.APPROVER]} - Approver +
+ + {ROLE_ICONS[RecipientRole.APPROVER]} + + Needs to approve +
+ + + + + +

+ The recipient is required to approve the document for it to + be completed. +

+
+
- {ROLE_ICONS[RecipientRole.VIEWER]} - Viewer +
+ {ROLE_ICONS[RecipientRole.VIEWER]} + Needs to view +
+ + + + + +

+ The recipient is required to view the document for it to be + completed. +

+
+
+
+
+ + +
+
+ {ROLE_ICONS[RecipientRole.CC]} + Receives copy +
+ + + + + +

+ The recipient is not required to take any action and + receives a copy of the document after it is completed. +

+
+
diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index 08cfc4957..e415f1aac 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -4,7 +4,7 @@ import React, { useId, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; -import { Plus, Trash } from 'lucide-react'; +import { InfoIcon, Plus, Trash } from 'lucide-react'; import { useSession } from 'next-auth/react'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; @@ -25,6 +25,7 @@ import type { DocumentFlowStep } from '../document-flow/types'; import { ROLE_ICONS } from '../recipient-role-icons'; import { Select, SelectContent, SelectItem, SelectTrigger } from '../select'; import { useStep } from '../stepper'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; @@ -159,29 +160,81 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
- {ROLE_ICONS[RecipientRole.SIGNER]} - Signer -
-
- - -
- {ROLE_ICONS[RecipientRole.CC]} - Receives copy +
+ {ROLE_ICONS[RecipientRole.SIGNER]} + Needs to sign +
+ + + + + +

+ The recipient is required to sign the document for it to be + completed. +

+
+
- {ROLE_ICONS[RecipientRole.APPROVER]} - Approver +
+ {ROLE_ICONS[RecipientRole.APPROVER]} + Needs to approve +
+ + + + + +

+ The recipient is required to approve the document for it to be + completed. +

+
+
- {ROLE_ICONS[RecipientRole.VIEWER]} - Viewer +
+ {ROLE_ICONS[RecipientRole.VIEWER]} + Needs to view +
+ + + + + +

+ The recipient is required to view the document for it to be + completed. +

+
+
+
+
+ + +
+
+ {ROLE_ICONS[RecipientRole.CC]} + Receives copy +
+ + + + + +

+ The recipient is not required to take any action and receives a + copy of the document after it is completed. +

+
+
From f8ddb0f9225e5c63a74377f71ab6e65d94ebbbe7 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Thu, 18 Apr 2024 18:12:08 +0530 Subject: [PATCH 027/100] chore: update filename for bulk recipients --- packages/lib/server-only/document/send-completed-email.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts index f5cef426c..f841aef33 100644 --- a/packages/lib/server-only/document/send-completed-email.ts +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -130,7 +130,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo text: render(template, { plainText: true }), attachments: [ { - filename: document.title, + filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf', content: Buffer.from(completedDocument), }, ], From 6526377f1bd51d19b9e5677bd67c11cd612e5ddb Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 18 Apr 2024 21:56:31 +0700 Subject: [PATCH 028/100] feat: add visible completed fields --- .../documents/[id]/document-page-view.tsx | 24 ++- .../src/app/(signing)/sign/[token]/page.tsx | 11 +- .../sign/[token]/signing-page-view.tsx | 12 +- .../avatar/stack-avatars-with-tooltip.tsx | 156 ++++++------------ .../document/document-read-only-fields.tsx | 113 +++++++++++++ .../get-completed-fields-for-document.ts | 29 ++++ .../field/get-completed-fields-for-token.ts | 33 ++++ .../template/create-document-from-template.ts | 6 +- .../template/duplicate-template.ts | 6 +- packages/lib/types/fields.ts | 3 + .../migration.sql | 8 + packages/prisma/schema.prisma | 4 +- packages/ui/components/field/field.tsx | 4 +- packages/ui/primitives/popover.tsx | 64 ++++++- 14 files changed, 356 insertions(+), 117 deletions(-) create mode 100644 apps/web/src/components/document/document-read-only-fields.tsx create mode 100644 packages/lib/server-only/field/get-completed-fields-for-document.ts create mode 100644 packages/lib/server-only/field/get-completed-fields-for-token.ts create mode 100644 packages/lib/types/fields.ts create mode 100644 packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx index e20c88a27..3b89f63f5 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx @@ -8,6 +8,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag'; +import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; @@ -19,6 +20,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { DocumentHistorySheet } from '~/components/document/document-history-sheet'; +import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields'; import { DocumentStatus as DocumentStatusComponent, FRIENDLY_STATUS_MAP, @@ -83,11 +85,16 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) documentMeta.password = securePassword; } - const recipients = await getRecipientsForDocument({ - documentId, - teamId: team?.id, - userId: user.id, - }); + const [recipients, completedFields] = await Promise.all([ + getRecipientsForDocument({ + documentId, + teamId: team?.id, + userId: user.id, + }), + getCompletedFieldsForDocument({ + documentId, + }), + ]); const documentWithRecipients = { ...document, @@ -148,6 +155,13 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) + {document.status === DocumentStatus.PENDING && ( + + )} +
diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index e83f675ce..95c9b6512 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -6,6 +6,7 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; +import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures'; @@ -37,7 +38,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders); - const [document, fields, recipient] = await Promise.all([ + const [document, fields, recipient, completedFields] = await Promise.all([ getDocumentAndSenderByToken({ token, userId: user?.id, @@ -45,6 +46,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp }).catch(() => null), getFieldsForToken({ token }), getRecipientByToken({ token }).catch(() => null), + getCompletedFieldsForToken({ token }), ]); if (!document || !document.documentData || !recipient) { @@ -120,7 +122,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp signature={user?.email === recipient.email ? user.signature : undefined} > - + ); diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx index c04679956..4691d0d4c 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx @@ -4,12 +4,14 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-form import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token'; +import type { CompletedField } from '@documenso/lib/types/fields'; import type { Field, Recipient } from '@documenso/prisma/client'; import { FieldType, RecipientRole } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields'; import { truncateTitle } from '~/helpers/truncate-title'; import { DateField } from './date-field'; @@ -23,9 +25,15 @@ export type SigningPageViewProps = { document: DocumentAndSender; recipient: Recipient; fields: Field[]; + completedFields: CompletedField[]; }; -export const SigningPageView = ({ document, recipient, fields }: SigningPageViewProps) => { +export const SigningPageView = ({ + document, + recipient, + fields, + completedFields, +}: SigningPageViewProps) => { const truncatedTitle = truncateTitle(document.title); const { documentData, documentMeta } = document; @@ -70,6 +78,8 @@ export const SigningPageView = ({ document, recipient, fields }: SigningPageView
+ + {fields.map((field) => match(field.type) diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 10f7d1e6a..b6a13b911 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -1,12 +1,10 @@ 'use client'; -import { useRef, useState } from 'react'; - import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import type { Recipient } from '@documenso/prisma/client'; -import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; +import { PopoverHover } from '@documenso/ui/primitives/popover'; import { AvatarWithRecipient } from './avatar-with-recipient'; import { StackAvatar } from './stack-avatar'; @@ -23,11 +21,6 @@ export const StackAvatarsWithTooltip = ({ position, children, }: StackAvatarsWithTooltipProps) => { - const [open, setOpen] = useState(false); - - const isControlled = useRef(false); - const isMouseOverTimeout = useRef(null); - const waitingRecipients = recipients.filter( (recipient) => getRecipientType(recipient) === 'waiting', ); @@ -44,105 +37,62 @@ export const StackAvatarsWithTooltip = ({ (recipient) => getRecipientType(recipient) === 'unsigned', ); - const onMouseEnter = () => { - if (isMouseOverTimeout.current) { - clearTimeout(isMouseOverTimeout.current); - } - - if (isControlled.current) { - return; - } - - isMouseOverTimeout.current = setTimeout(() => { - setOpen((o) => (!o ? true : o)); - }, 200); - }; - - const onMouseLeave = () => { - if (isMouseOverTimeout.current) { - clearTimeout(isMouseOverTimeout.current); - } - - if (isControlled.current) { - return; - } - - setTimeout(() => { - setOpen((o) => (o ? false : o)); - }, 200); - }; - - const onOpenChange = (newOpen: boolean) => { - isControlled.current = newOpen; - - setOpen(newOpen); - }; - return ( - - - {children || } - - - - {completedRecipients.length > 0 && ( -
-

Completed

- {completedRecipients.map((recipient: Recipient) => ( -
- -
-

{recipient.email}

-

- {RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName} -

-
+ } + contentProps={{ + className: 'flex flex-col gap-y-5 py-2', + side: position, + }} + > + {completedRecipients.length > 0 && ( +
+

Completed

+ {completedRecipients.map((recipient: Recipient) => ( +
+ +
+

{recipient.email}

+

+ {RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName} +

- ))} -
- )} +
+ ))} +
+ )} - {waitingRecipients.length > 0 && ( -
-

Waiting

- {waitingRecipients.map((recipient: Recipient) => ( - - ))} -
- )} + {waitingRecipients.length > 0 && ( +
+

Waiting

+ {waitingRecipients.map((recipient: Recipient) => ( + + ))} +
+ )} - {openedRecipients.length > 0 && ( -
-

Opened

- {openedRecipients.map((recipient: Recipient) => ( - - ))} -
- )} + {openedRecipients.length > 0 && ( +
+

Opened

+ {openedRecipients.map((recipient: Recipient) => ( + + ))} +
+ )} - {uncompletedRecipients.length > 0 && ( -
-

Uncompleted

- {uncompletedRecipients.map((recipient: Recipient) => ( - - ))} -
- )} - - + {uncompletedRecipients.length > 0 && ( +
+

Uncompleted

+ {uncompletedRecipients.map((recipient: Recipient) => ( + + ))} +
+ )} + ); }; diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/web/src/components/document/document-read-only-fields.tsx new file mode 100644 index 000000000..530066fa8 --- /dev/null +++ b/apps/web/src/components/document/document-read-only-fields.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { useState } from 'react'; + +import { P, match } from 'ts-pattern'; + +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + convertToLocalSystemFormat, +} from '@documenso/lib/constants/date-formats'; +import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import type { CompletedField } from '@documenso/lib/types/fields'; +import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; +import type { DocumentMeta } from '@documenso/prisma/client'; +import { FieldType } from '@documenso/prisma/client'; +import { FieldRootContainer } from '@documenso/ui/components/field/field'; +import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; +import { Button } from '@documenso/ui/primitives/button'; +import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types'; +import { ElementVisible } from '@documenso/ui/primitives/element-visible'; +import { PopoverHover } from '@documenso/ui/primitives/popover'; + +export type DocumentReadOnlyFieldsProps = { + fields: CompletedField[]; + documentMeta?: DocumentMeta; +}; + +export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnlyFieldsProps) => { + const [hiddenFieldIds, setHiddenFieldIds] = useState>({}); + + const handleHideField = (fieldId: string) => { + setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true })); + }; + + return ( + + {fields.map( + (field) => + !hiddenFieldIds[field.secondaryId] && ( + +
+ + + {extractInitials(field.Recipient.name || field.Recipient.email)} + + + } + contentProps={{ + className: 'flex w-fit flex-col py-2.5 text-sm', + }} + > +

+ + {field.Recipient.name + ? `${field.Recipient.name} (${field.Recipient.email})` + : field.Recipient.email}{' '} + + inserted a {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()} +

+ + +
+
+ +
+ {match(field) + .with({ type: FieldType.SIGNATURE }, (field) => + field.Signature?.signatureImageAsBase64 ? ( + Signature + ) : ( +

+ {field.Signature?.typedSignature} +

+ ), + ) + .with({ type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, () => ( +

{field.customText}

+ )) + .with({ type: FieldType.DATE }, () => ( +

+ {convertToLocalSystemFormat( + field.customText, + documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + )} +

+ )) + .with({ type: FieldType.FREE_SIGNATURE }, () => null) + .exhaustive()} +
+
+ ), + )} +
+ ); +}; diff --git a/packages/lib/server-only/field/get-completed-fields-for-document.ts b/packages/lib/server-only/field/get-completed-fields-for-document.ts new file mode 100644 index 000000000..304be95ba --- /dev/null +++ b/packages/lib/server-only/field/get-completed-fields-for-document.ts @@ -0,0 +1,29 @@ +import { prisma } from '@documenso/prisma'; +import { SigningStatus } from '@documenso/prisma/client'; + +export type GetCompletedFieldsForDocumentOptions = { + documentId: number; +}; + +export const getCompletedFieldsForDocument = async ({ + documentId, +}: GetCompletedFieldsForDocumentOptions) => { + return await prisma.field.findMany({ + where: { + documentId, + Recipient: { + signingStatus: SigningStatus.SIGNED, + }, + inserted: true, + }, + include: { + Signature: true, + Recipient: { + select: { + name: true, + email: true, + }, + }, + }, + }); +}; diff --git a/packages/lib/server-only/field/get-completed-fields-for-token.ts b/packages/lib/server-only/field/get-completed-fields-for-token.ts new file mode 100644 index 000000000..d84fa1343 --- /dev/null +++ b/packages/lib/server-only/field/get-completed-fields-for-token.ts @@ -0,0 +1,33 @@ +import { prisma } from '@documenso/prisma'; +import { SigningStatus } from '@documenso/prisma/client'; + +export type GetCompletedFieldsForTokenOptions = { + token: string; +}; + +export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => { + return await prisma.field.findMany({ + where: { + Document: { + Recipient: { + some: { + token, + }, + }, + }, + Recipient: { + signingStatus: SigningStatus.SIGNED, + }, + inserted: true, + }, + include: { + Signature: true, + Recipient: { + select: { + name: true, + email: true, + }, + }, + }, + }); +}; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 8ae5fecaf..79a3f6f25 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -89,6 +89,10 @@ export const createDocumentFromTemplate = async ({ const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email); + if (!documentRecipient) { + throw new Error('Recipient not found.'); + } + return { type: field.type, page: field.page, @@ -99,7 +103,7 @@ export const createDocumentFromTemplate = async ({ customText: field.customText, inserted: field.inserted, documentId: document.id, - recipientId: documentRecipient?.id || null, + recipientId: documentRecipient.id, }; }), }); diff --git a/packages/lib/server-only/template/duplicate-template.ts b/packages/lib/server-only/template/duplicate-template.ts index 97b3f0a0b..963d78bde 100644 --- a/packages/lib/server-only/template/duplicate-template.ts +++ b/packages/lib/server-only/template/duplicate-template.ts @@ -81,6 +81,10 @@ export const duplicateTemplate = async ({ (doc) => doc.email === recipient?.email, ); + if (!duplicatedTemplateRecipient) { + throw new Error('Recipient not found.'); + } + return { type: field.type, page: field.page, @@ -91,7 +95,7 @@ export const duplicateTemplate = async ({ customText: field.customText, inserted: field.inserted, templateId: duplicatedTemplate.id, - recipientId: duplicatedTemplateRecipient?.id || null, + recipientId: duplicatedTemplateRecipient.id, }; }), }); diff --git a/packages/lib/types/fields.ts b/packages/lib/types/fields.ts new file mode 100644 index 000000000..1b999310d --- /dev/null +++ b/packages/lib/types/fields.ts @@ -0,0 +1,3 @@ +import type { getCompletedFieldsForToken } from '../server-only/field/get-completed-fields-for-token'; + +export type CompletedField = Awaited>[number]; diff --git a/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql b/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql new file mode 100644 index 000000000..62845de28 --- /dev/null +++ b/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `recipientId` on table `Field` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Field" ALTER COLUMN "recipientId" SET NOT NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 35d429779..c0e68d53f 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -386,7 +386,7 @@ model Field { secondaryId String @unique @default(cuid()) documentId Int? templateId Int? - recipientId Int? + recipientId Int type FieldType page Int positionX Decimal @default(0) @@ -397,7 +397,7 @@ model Field { inserted Boolean Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) - Recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade) + Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) Signature Signature? @@index([documentId]) diff --git a/packages/ui/components/field/field.tsx b/packages/ui/components/field/field.tsx index e40b2e3d9..ce62443de 100644 --- a/packages/ui/components/field/field.tsx +++ b/packages/ui/components/field/field.tsx @@ -19,6 +19,7 @@ export type FieldContainerPortalProps = { field: Field; className?: string; children: React.ReactNode; + cardClassName?: string; }; export function FieldContainerPortal({ @@ -44,7 +45,7 @@ export function FieldContainerPortal({ ); } -export function FieldRootContainer({ field, children }: FieldContainerPortalProps) { +export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) { const [isValidating, setIsValidating] = useState(false); const ref = React.useRef(null); @@ -78,6 +79,7 @@ export function FieldRootContainer({ field, children }: FieldContainerPortalProp { 'border-orange-300 ring-1 ring-orange-300': !field.inserted && isValidating, }, + cardClassName, )} ref={ref} data-inserted={field.inserted ? 'true' : 'false'} diff --git a/packages/ui/primitives/popover.tsx b/packages/ui/primitives/popover.tsx index e84f6cc6d..62462322b 100644 --- a/packages/ui/primitives/popover.tsx +++ b/packages/ui/primitives/popover.tsx @@ -30,4 +30,66 @@ const PopoverContent = React.forwardRef< PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent }; +type PopoverHoverProps = { + trigger: React.ReactNode; + children: React.ReactNode; + contentProps?: React.ComponentPropsWithoutRef; +}; + +const PopoverHover = ({ trigger, children, contentProps }: PopoverHoverProps) => { + const [open, setOpen] = React.useState(false); + + const isControlled = React.useRef(false); + const isMouseOver = React.useRef(false); + + const onMouseEnter = () => { + isMouseOver.current = true; + + if (isControlled.current) { + return; + } + + setOpen(true); + }; + + const onMouseLeave = () => { + isMouseOver.current = false; + + if (isControlled.current) { + return; + } + + setTimeout(() => { + setOpen(isMouseOver.current); + }, 200); + }; + + const onOpenChange = (newOpen: boolean) => { + isControlled.current = newOpen; + + setOpen(newOpen); + }; + + return ( + + + {trigger} + + + + {children} + + + ); +}; + +export { Popover, PopoverTrigger, PopoverContent, PopoverHover }; From 6e09a4700b065d23c3367b5b00321fd7654fe040 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 19 Apr 2024 16:17:32 +0700 Subject: [PATCH 029/100] fix: prevent signing draft documents (#1111) ## Description Currently users can sign and complete draft documents, which will result in a completed document in an invalid state. ## Changes Made - Prevent recipients from inserting or uninserting fields for draft documents - Prevent recipients from completing draft documents - Remove ability to copy signing tokens unless document is pending ## Summary by CodeRabbit - **New Features** - Enhanced document status visibility and control across various components in the application. Users can now see and interact with document statuses more dynamically in views like `DocumentPageView`, `DocumentEditPageView`, and `DocumentsDataTable`. - Improved document signing process with updated status checks, ensuring actions like signing, completing, and removing fields are only available under appropriate document statuses. - **Bug Fixes** - Adjusted document status validation logic in server-side operations to prevent actions on incorrectly stated documents, enhancing the overall security and functionality of document processing. --- .../documents/[id]/document-page-view.tsx | 6 +++- .../[id]/edit/document-edit-page-view.tsx | 6 +++- .../documents/data-table-action-dropdown.tsx | 2 +- .../app/(dashboard)/documents/data-table.tsx | 7 +++- .../src/app/(signing)/sign/[token]/page.tsx | 7 +++- .../avatar/avatar-with-recipient.tsx | 35 ++++++++++--------- .../avatar/stack-avatars-with-tooltip.tsx | 22 +++++++++--- .../document/complete-document-with-token.ts | 4 +-- .../field/remove-signed-field-with-token.ts | 4 +-- .../field/sign-field-with-token.ts | 8 ++--- packages/prisma/seed/documents.ts | 17 ++++----- 11 files changed, 77 insertions(+), 41 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx index e20c88a27..fc1022cfa 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx @@ -118,7 +118,11 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
- + {recipients.length} Recipient(s)
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx index 8a78ca9aa..5c2a64870 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx @@ -92,7 +92,11 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
- + {recipients.length} Recipient(s)
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index a43d37af7..c67890dfe 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -114,7 +114,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr Action - {recipient && recipient?.role !== RecipientRole.CC && ( + {!isDraft && recipient && recipient?.role !== RecipientRole.CC && ( {recipient?.role === RecipientRole.VIEWER && ( diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index ec595641e..c49cdf6ab 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -76,7 +76,12 @@ export const DocumentsDataTable = ({ { header: 'Recipient', accessorKey: 'recipient', - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { header: 'Status', diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index e83f675ce..bd46898af 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -47,7 +47,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp getRecipientByToken({ token }).catch(() => null), ]); - if (!document || !document.documentData || !recipient) { + if ( + !document || + !document.documentData || + !recipient || + document.status === DocumentStatus.DRAFT + ) { return notFound(); } diff --git a/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx b/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx index 69dd88d79..cd7cd2305 100644 --- a/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx +++ b/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx @@ -8,6 +8,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import type { Recipient } from '@documenso/prisma/client'; +import { DocumentStatus } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -15,18 +16,21 @@ import { StackAvatar } from './stack-avatar'; export type AvatarWithRecipientProps = { recipient: Recipient; + documentStatus: DocumentStatus; }; -export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) { +export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) { const [, copy] = useCopyToClipboard(); const { toast } = useToast(); + const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null; + const onRecipientClick = () => { - if (!recipient.token) { + if (!signingToken) { return; } - void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`).then(() => { + void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${signingToken}`).then(() => { toast({ title: 'Copied to clipboard', description: 'The signing link has been copied to your clipboard.', @@ -37,10 +41,10 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) { return (
-
-
-

{recipient.email}

-

- {RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName} -

-
+ +
+

{recipient.email}

+

+ {RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName} +

); diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 10f7d1e6a..7a269d036 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -5,7 +5,7 @@ import { useRef, useState } from 'react'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; -import type { Recipient } from '@documenso/prisma/client'; +import type { DocumentStatus, Recipient } from '@documenso/prisma/client'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { AvatarWithRecipient } from './avatar-with-recipient'; @@ -13,12 +13,14 @@ import { StackAvatar } from './stack-avatar'; import { StackAvatars } from './stack-avatars'; export type StackAvatarsWithTooltipProps = { + documentStatus: DocumentStatus; recipients: Recipient[]; position?: 'top' | 'bottom'; children?: React.ReactNode; }; export const StackAvatarsWithTooltip = ({ + documentStatus, recipients, position, children, @@ -120,7 +122,11 @@ export const StackAvatarsWithTooltip = ({

Waiting

{waitingRecipients.map((recipient: Recipient) => ( - + ))}
)} @@ -129,7 +135,11 @@ export const StackAvatarsWithTooltip = ({

Opened

{openedRecipients.map((recipient: Recipient) => ( - + ))}
)} @@ -138,7 +148,11 @@ export const StackAvatarsWithTooltip = ({

Uncompleted

{uncompletedRecipients.map((recipient: Recipient) => ( - + ))}
)} diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 8e3b56002..d16b83ea1 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -49,8 +49,8 @@ export const completeDocumentWithToken = async ({ const document = await getDocument({ token, documentId }); - if (document.status === DocumentStatus.COMPLETED) { - throw new Error(`Document ${document.id} has already been completed`); + if (document.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${document.id} must be pending`); } if (document.Recipient.length === 0) { diff --git a/packages/lib/server-only/field/remove-signed-field-with-token.ts b/packages/lib/server-only/field/remove-signed-field-with-token.ts index 6548ae0f1..46d04dd58 100644 --- a/packages/lib/server-only/field/remove-signed-field-with-token.ts +++ b/packages/lib/server-only/field/remove-signed-field-with-token.ts @@ -36,8 +36,8 @@ export const removeSignedFieldWithToken = async ({ throw new Error(`Document not found for field ${field.id}`); } - if (document.status === DocumentStatus.COMPLETED) { - throw new Error(`Document ${document.id} has already been completed`); + if (document.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${document.id} must be pending`); } if (recipient?.signingStatus === SigningStatus.SIGNED) { diff --git a/packages/lib/server-only/field/sign-field-with-token.ts b/packages/lib/server-only/field/sign-field-with-token.ts index b8a5ccf8f..359a5da68 100644 --- a/packages/lib/server-only/field/sign-field-with-token.ts +++ b/packages/lib/server-only/field/sign-field-with-token.ts @@ -58,14 +58,14 @@ export const signFieldWithToken = async ({ throw new Error(`Recipient not found for field ${field.id}`); } - if (document.status === DocumentStatus.COMPLETED) { - throw new Error(`Document ${document.id} has already been completed`); - } - if (document.deletedAt) { throw new Error(`Document ${document.id} has been deleted`); } + if (document.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${document.id} must be pending for signing`); + } + if (recipient?.signingStatus === SigningStatus.SIGNED) { throw new Error(`Recipient ${recipient.id} has already signed`); } diff --git a/packages/prisma/seed/documents.ts b/packages/prisma/seed/documents.ts index 6c1e698c5..2e6462daa 100644 --- a/packages/prisma/seed/documents.ts +++ b/packages/prisma/seed/documents.ts @@ -342,14 +342,15 @@ export const seedPendingDocumentWithFullFields = async ({ }, }); - const latestDocument = updateDocumentOptions - ? await prisma.document.update({ - where: { - id: document.id, - }, - data: updateDocumentOptions, - }) - : document; + const latestDocument = await prisma.document.update({ + where: { + id: document.id, + }, + data: { + ...updateDocumentOptions, + status: DocumentStatus.PENDING, + }, + }); return { document: latestDocument, From bd40e633920b2304e48dc12585d3999d35e20fe9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 19 Apr 2024 17:37:38 +0700 Subject: [PATCH 030/100] fix: update document deletion logic (#1100) --- .../app/(marketing)/singleplayer/client.tsx | 1 + .../[id]/document-page-view-dropdown.tsx | 30 +-- .../documents/[id]/document-page-view.tsx | 7 +- .../documents/data-table-action-dropdown.tsx | 41 ++-- .../app/(dashboard)/documents/data-table.tsx | 2 +- .../documents/delete-document-dialog.tsx | 104 ++++++--- .../documents/documents-page-view.tsx | 4 +- .../app/(dashboard)/documents/empty-state.tsx | 5 +- .../e2e/document-flow/signers-step.spec.ts | 4 +- .../document-flow/stepper-component.spec.ts | 2 +- .../e2e/documents/delete-documents.spec.ts | 172 +++++++++++++-- packages/app-tests/e2e/fixtures/documents.ts | 17 ++ .../e2e/teams/team-documents.spec.ts | 157 +++++++++++--- .../template-document-cancel.tsx | 4 + .../server-only/document/delete-document.ts | 197 ++++++++++++------ .../server-only/document/find-documents.ts | 55 ++++- .../lib/server-only/document/get-stats.ts | 11 +- .../migration.sql | 13 ++ packages/prisma/schema.prisma | 35 ++-- .../primitives/document-flow/add-signers.tsx | 4 +- 20 files changed, 651 insertions(+), 214 deletions(-) create mode 100644 packages/app-tests/e2e/fixtures/documents.ts create mode 100644 packages/prisma/migrations/20240408142543_add_recipient_document_delete/migration.sql diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index 3f1c11259..e20b94887 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -158,6 +158,7 @@ export const SinglePlayerClient = () => { expired: null, signedAt: null, readStatus: 'OPENED', + documentDeletedAt: null, signingStatus: 'NOT_SIGNED', sendStatus: 'NOT_SENT', role: 'SIGNER', diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx index 0fb592ea1..35dbaa8f1 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx @@ -19,7 +19,7 @@ import { useSession } from 'next-auth/react'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { DocumentStatus } from '@documenso/prisma/client'; -import type { Document, Recipient, Team, User } from '@documenso/prisma/client'; +import type { Document, Recipient, Team, TeamEmail, User } from '@documenso/prisma/client'; import { trpc as trpcClient } from '@documenso/trpc/client'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { @@ -41,7 +41,7 @@ export type DocumentPageViewDropdownProps = { Recipient: Recipient[]; team: Pick | null; }; - team?: Pick; + team?: Pick & { teamEmail: TeamEmail | null }; }; export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => { @@ -59,9 +59,10 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro const isOwner = document.User.id === session.user.id; const isDraft = document.status === DocumentStatus.DRAFT; + const isDeleted = document.deletedAt !== null; const isComplete = document.status === DocumentStatus.COMPLETED; - const isDocumentDeletable = isOwner; const isCurrentTeamDocument = team && document.team?.url === team.url; + const canManageDocument = Boolean(isOwner || isCurrentTeamDocument); const documentsPath = formatDocumentsPath(team?.url); @@ -127,7 +128,10 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro Duplicate - setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}> + setDeleteDialogOpen(true)} + disabled={Boolean(!canManageDocument && team?.teamEmail) || isDeleted} + > Delete @@ -154,15 +158,15 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro /> - {isDocumentDeletable && ( - - )} + + {isDuplicateDialogOpen && ( { @@ -127,6 +128,8 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
)} + + {document.deletedAt && Document deleted}
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index c67890dfe..aed95662b 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -15,7 +15,6 @@ import { Pencil, Share, Trash2, - XCircle, } from 'lucide-react'; import { useSession } from 'next-auth/react'; @@ -45,7 +44,7 @@ export type DataTableActionDropdownProps = { Recipient: Recipient[]; team: Pick | null; }; - team?: Pick; + team?: Pick & { teamEmail?: string }; }; export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownProps) => { @@ -67,8 +66,8 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr // const isPending = row.status === DocumentStatus.PENDING; const isComplete = row.status === DocumentStatus.COMPLETED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; - const isDocumentDeletable = isOwner; const isCurrentTeamDocument = team && row.team?.url === team.url; + const canManageDocument = Boolean(isOwner || isCurrentTeamDocument); const documentsPath = formatDocumentsPath(team?.url); @@ -107,7 +106,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr return ( - + @@ -141,7 +140,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr )} - + Edit @@ -158,14 +157,18 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr Duplicate - + {/* No point displaying this if there's no functionality. */} + {/* Void - + */} - setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}> + setDeleteDialogOpen(true)} + disabled={Boolean(!canManageDocument && team?.teamEmail)} + > - Delete + {canManageDocument ? 'Delete' : 'Hide'} Share @@ -186,16 +189,16 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr /> - {isDocumentDeletable && ( - - )} + + {isDuplicateDialogOpen && ( ; showSenderColumn?: boolean; - team?: Pick; + team?: Pick & { teamEmail?: string }; }; export const DocumentsDataTable = ({ diff --git a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx index 59fd21e60..558d39558 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -2,8 +2,11 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; +import { match } from 'ts-pattern'; + import { DocumentStatus } from '@documenso/prisma/client'; import { trpc as trpcReact } from '@documenso/trpc/react'; +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -23,6 +26,7 @@ type DeleteDocumentDialogProps = { status: DocumentStatus; documentTitle: string; teamId?: number; + canManageDocument: boolean; }; export const DeleteDocumentDialog = ({ @@ -32,6 +36,7 @@ export const DeleteDocumentDialog = ({ status, documentTitle, teamId, + canManageDocument, }: DeleteDocumentDialogProps) => { const router = useRouter(); @@ -83,47 +88,82 @@ export const DeleteDocumentDialog = ({ !isLoading && onOpenChange(value)}> - Are you sure you want to delete "{documentTitle}"? + Are you sure? - Please note that this action is irreversible. Once confirmed, your document will be - permanently deleted. + You are about to {canManageDocument ? 'delete' : 'hide'}{' '} + "{documentTitle}" - {status !== DocumentStatus.DRAFT && ( -
- -
+ {canManageDocument ? ( + + {match(status) + .with(DocumentStatus.DRAFT, () => ( + + Please note that this action is irreversible. Once confirmed, + this document will be permanently deleted. + + )) + .with(DocumentStatus.PENDING, () => ( + +

+ Please note that this action is irreversible. +

+ +

Once confirmed, the following will occur:

+ +
    +
  • Document will be permanently deleted
  • +
  • Document signing process will be cancelled
  • +
  • All inserted signatures will be voided
  • +
  • All recipients will be notified
  • +
+
+ )) + .with(DocumentStatus.COMPLETED, () => ( + +

By deleting this document, the following will occur:

+ +
    +
  • The document will be hidden from your account
  • +
  • Recipients will still retain their copy of the document
  • +
+
+ )) + .exhaustive()} +
+ ) : ( + + + Please contact support if you would like to revert this action. + + + )} + + {status !== DocumentStatus.DRAFT && canManageDocument && ( + )} -
- + - -
+
diff --git a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx index 9059b8e88..84f6bfe3f 100644 --- a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx @@ -41,7 +41,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa const page = Number(searchParams.page) || 1; const perPage = Number(searchParams.perPage) || 20; const senderIds = parseToIntegerArray(searchParams.senderIds ?? ''); - const currentTeam = team ? { id: team.id, url: team.url } : undefined; + const currentTeam = team + ? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email } + : undefined; const getStatOptions: GetStatsInput = { user, diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx index b6d2f74e2..e1af23bf2 100644 --- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -37,7 +37,10 @@ export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => { })); return ( -
+
diff --git a/packages/app-tests/e2e/document-flow/signers-step.spec.ts b/packages/app-tests/e2e/document-flow/signers-step.spec.ts index 30d6ba11f..8676d05ed 100644 --- a/packages/app-tests/e2e/document-flow/signers-step.spec.ts +++ b/packages/app-tests/e2e/document-flow/signers-step.spec.ts @@ -45,7 +45,7 @@ test.describe('[EE_ONLY]', () => { await page .getByRole('textbox', { name: 'Email', exact: true }) .fill('recipient2@documenso.com'); - await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); // Display advanced settings. await page.getByLabel('Show advanced settings').click(); @@ -82,7 +82,7 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => { await page.getByPlaceholder('Name').fill('Recipient 1'); await page.getByRole('button', { name: 'Add Signer' }).click(); await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com'); - await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); // Advanced settings should not be visible for non EE users. await expect(page.getByLabel('Show advanced settings')).toBeHidden(); diff --git a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts index c2ae0618c..07aee6a30 100644 --- a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts +++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts @@ -136,7 +136,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie await page.getByPlaceholder('Name').fill('User 1'); await page.getByRole('button', { name: 'Add Signer' }).click(); await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com'); - await page.getByRole('textbox', { name: 'Name', exact: true }).fill('User 2'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2'); await page.getByRole('button', { name: 'Continue' }).click(); diff --git a/packages/app-tests/e2e/documents/delete-documents.spec.ts b/packages/app-tests/e2e/documents/delete-documents.spec.ts index 3658f1bc9..32f385df5 100644 --- a/packages/app-tests/e2e/documents/delete-documents.spec.ts +++ b/packages/app-tests/e2e/documents/delete-documents.spec.ts @@ -8,6 +8,7 @@ import { import { seedUser } from '@documenso/prisma/seed/users'; import { apiSignin, apiSignout } from '../fixtures/authentication'; +import { checkDocumentTabCount } from '../fixtures/documents'; test.describe.configure({ mode: 'serial' }); @@ -74,7 +75,7 @@ test('[DOCUMENTS]: deleting a completed document should not remove it from recip email: sender.email, }); - // open actions menu + // Open document action menu. await page .locator('tr', { hasText: 'Document 1 - Completed' }) .getByRole('cell', { name: 'Download' }) @@ -115,7 +116,7 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients' email: sender.email, }); - // open actions menu + // Open document action menu. await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click(); // delete document @@ -135,20 +136,11 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients' }); await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible(); - - await page.goto(`/sign/${recipient.token}`); - await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible(); - - await page.goto('/documents'); - await page.waitForURL('/documents'); - await apiSignout({ page }); } }); -test('[DOCUMENTS]: deleting a draft document should remove it without additional prompting', async ({ - page, -}) => { +test('[DOCUMENTS]: deleting draft documents should permanently remove it', async ({ page }) => { const { sender } = await seedDeleteDocumentsTestRequirements(); await apiSignin({ @@ -156,11 +148,10 @@ test('[DOCUMENTS]: deleting a draft document should remove it without additional email: sender.email, }); - // open actions menu + // Open document action menu. await page .locator('tr', { hasText: 'Document 1 - Draft' }) - .getByRole('cell', { name: 'Edit' }) - .getByRole('button') + .getByTestId('document-table-action-btn') .click(); // delete document @@ -169,4 +160,155 @@ test('[DOCUMENTS]: deleting a draft document should remove it without additional await page.getByRole('button', { name: 'Delete' }).click(); await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible(); + + // Check document counts. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 1); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 0); + await checkDocumentTabCount(page, 'All', 2); +}); + +test('[DOCUMENTS]: deleting pending documents should permanently remove it', async ({ page }) => { + const { sender } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + // Open document action menu. + await page + .locator('tr', { hasText: 'Document 1 - Pending' }) + .getByTestId('document-table-action-btn') + .click(); + + // Delete document. + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible(); + + // Check document counts. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 0); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 1); + await checkDocumentTabCount(page, 'All', 2); +}); + +test('[DOCUMENTS]: deleting completed documents as an owner should hide it from only the owner', async ({ + page, +}) => { + const { sender, recipients } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + // Open document action menu. + await page + .locator('tr', { hasText: 'Document 1 - Completed' }) + .getByTestId('document-table-action-btn') + .click(); + + // Delete document. + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + // Check document counts. + await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible(); + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 1); + await checkDocumentTabCount(page, 'Completed', 0); + await checkDocumentTabCount(page, 'Draft', 1); + await checkDocumentTabCount(page, 'All', 2); + + // Sign into the recipient account. + await apiSignout({ page }); + await apiSignin({ + page, + email: recipients[0].email, + }); + + // Check document counts. + await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).toBeVisible(); + await checkDocumentTabCount(page, 'Inbox', 1); + await checkDocumentTabCount(page, 'Pending', 0); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 0); + await checkDocumentTabCount(page, 'All', 2); +}); + +test('[DOCUMENTS]: deleting documents as a recipient should only hide it for them', async ({ + page, +}) => { + const { sender, recipients } = await seedDeleteDocumentsTestRequirements(); + const recipientA = recipients[0]; + const recipientB = recipients[1]; + + await apiSignin({ + page, + email: recipientA.email, + }); + + // Open document action menu. + await page + .locator('tr', { hasText: 'Document 1 - Completed' }) + .getByTestId('document-table-action-btn') + .click(); + + // Delete document. + await page.getByRole('menuitem', { name: 'Hide' }).click(); + await page.getByRole('button', { name: 'Hide' }).click(); + + // Open document action menu. + await page + .locator('tr', { hasText: 'Document 1 - Pending' }) + .getByTestId('document-table-action-btn') + .click(); + + // Delete document. + await page.getByRole('menuitem', { name: 'Hide' }).click(); + await page.getByRole('button', { name: 'Hide' }).click(); + + // Check document counts. + await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible(); + await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible(); + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 0); + await checkDocumentTabCount(page, 'Completed', 0); + await checkDocumentTabCount(page, 'Draft', 0); + await checkDocumentTabCount(page, 'All', 0); + + // Sign into the sender account. + await apiSignout({ page }); + await apiSignin({ + page, + email: sender.email, + }); + + // Check document counts for sender. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 1); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 1); + await checkDocumentTabCount(page, 'All', 3); + + // Sign into the other recipient account. + await apiSignout({ page }); + await apiSignin({ + page, + email: recipientB.email, + }); + + // Check document counts for other recipient. + await checkDocumentTabCount(page, 'Inbox', 1); + await checkDocumentTabCount(page, 'Pending', 0); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 0); + await checkDocumentTabCount(page, 'All', 2); }); diff --git a/packages/app-tests/e2e/fixtures/documents.ts b/packages/app-tests/e2e/fixtures/documents.ts new file mode 100644 index 000000000..f7e0bd391 --- /dev/null +++ b/packages/app-tests/e2e/fixtures/documents.ts @@ -0,0 +1,17 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +export const checkDocumentTabCount = async (page: Page, tabName: string, count: number) => { + await page.getByRole('tab', { name: tabName }).click(); + + if (tabName !== 'All') { + await expect(page.getByRole('tab', { name: tabName })).toContainText(count.toString()); + } + + if (count === 0) { + await expect(page.getByTestId('empty-document-state')).toBeVisible(); + return; + } + + await expect(page.getByRole('main')).toContainText(`Showing ${count}`); +}; diff --git a/packages/app-tests/e2e/teams/team-documents.spec.ts b/packages/app-tests/e2e/teams/team-documents.spec.ts index 8f70befc8..6cea6445d 100644 --- a/packages/app-tests/e2e/teams/team-documents.spec.ts +++ b/packages/app-tests/e2e/teams/team-documents.spec.ts @@ -1,4 +1,3 @@ -import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { DocumentStatus } from '@documenso/prisma/client'; @@ -7,24 +6,10 @@ import { seedTeamEmail, unseedTeam, unseedTeamEmail } from '@documenso/prisma/se import { seedUser } from '@documenso/prisma/seed/users'; import { apiSignin, apiSignout } from '../fixtures/authentication'; +import { checkDocumentTabCount } from '../fixtures/documents'; test.describe.configure({ mode: 'parallel' }); -const checkDocumentTabCount = async (page: Page, tabName: string, count: number) => { - await page.getByRole('tab', { name: tabName }).click(); - - if (tabName !== 'All') { - await expect(page.getByRole('tab', { name: tabName })).toContainText(count.toString()); - } - - if (count === 0) { - await expect(page.getByRole('main')).toContainText(`Nothing to do`); - return; - } - - await expect(page.getByRole('main')).toContainText(`Showing ${count}`); -}; - test('[TEAMS]: check team documents count', async ({ page }) => { const { team, teamMember2 } = await seedTeamDocuments(); @@ -245,24 +230,6 @@ test('[TEAMS]: check team documents count with external team email', async ({ pa await unseedTeam(team.url); }); -test('[TEAMS]: delete pending team document', async ({ page }) => { - const { team, teamMember2: currentUser } = await seedTeamDocuments(); - - await apiSignin({ - page, - email: currentUser.email, - redirectPath: `/t/${team.url}/documents?status=PENDING`, - }); - - await page.getByRole('row').getByRole('button').nth(1).click(); - - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); - await page.getByRole('button', { name: 'Delete' }).click(); - - await checkDocumentTabCount(page, 'Pending', 1); -}); - test('[TEAMS]: resend pending team document', async ({ page }) => { const { team, teamMember2: currentUser } = await seedTeamDocuments(); @@ -280,3 +247,125 @@ test('[TEAMS]: resend pending team document', async ({ page }) => { await expect(page.getByRole('status')).toContainText('Document re-sent'); }); + +test('[TEAMS]: delete draft team document', async ({ page }) => { + const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: teamMember3.email, + redirectPath: `/t/${team.url}/documents?status=DRAFT`, + }); + + await page.getByRole('row').getByRole('button').nth(1).click(); + + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + + await checkDocumentTabCount(page, 'Draft', 1); + + // Should be hidden for all team members. + await apiSignout({ page }); + + // Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same. + for (const user of [team.owner, teamEmailMember]) { + await apiSignin({ + page, + email: user.email, + redirectPath: `/t/${team.url}/documents`, + }); + + // Check document counts. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 2); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 1); + await checkDocumentTabCount(page, 'All', 4); + + await apiSignout({ page }); + } + + await unseedTeam(team.url); +}); + +test('[TEAMS]: delete pending team document', async ({ page }) => { + const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: teamMember3.email, + redirectPath: `/t/${team.url}/documents?status=PENDING`, + }); + + await page.getByRole('row').getByRole('button').nth(1).click(); + + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + await checkDocumentTabCount(page, 'Pending', 1); + + // Should be hidden for all team members. + await apiSignout({ page }); + + // Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same. + for (const user of [team.owner, teamEmailMember]) { + await apiSignin({ + page, + email: user.email, + redirectPath: `/t/${team.url}/documents`, + }); + + // Check document counts. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 1); + await checkDocumentTabCount(page, 'Completed', 1); + await checkDocumentTabCount(page, 'Draft', 2); + await checkDocumentTabCount(page, 'All', 4); + + await apiSignout({ page }); + } + + await unseedTeam(team.url); +}); + +test('[TEAMS]: delete completed team document', async ({ page }) => { + const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: teamMember3.email, + redirectPath: `/t/${team.url}/documents?status=COMPLETED`, + }); + + await page.getByRole('row').getByRole('button').nth(2).click(); + + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + await checkDocumentTabCount(page, 'Completed', 0); + + // Should be hidden for all team members. + await apiSignout({ page }); + + // Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same. + for (const user of [team.owner, teamEmailMember]) { + await apiSignin({ + page, + email: user.email, + redirectPath: `/t/${team.url}/documents`, + }); + + // Check document counts. + await checkDocumentTabCount(page, 'Inbox', 0); + await checkDocumentTabCount(page, 'Pending', 2); + await checkDocumentTabCount(page, 'Completed', 0); + await checkDocumentTabCount(page, 'Draft', 2); + await checkDocumentTabCount(page, 'All', 4); + + await apiSignout({ page }); + } + + await unseedTeam(team.url); +}); diff --git a/packages/email/template-components/template-document-cancel.tsx b/packages/email/template-components/template-document-cancel.tsx index 885cb6c80..dff275de2 100644 --- a/packages/email/template-components/template-document-cancel.tsx +++ b/packages/email/template-components/template-document-cancel.tsx @@ -23,6 +23,10 @@ export const TemplateDocumentCancel = ({
"{documentName}" + + All signatures have been voided. + + You don't need to sign it anymore. diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index b0b1ad682..a097d76e9 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -6,6 +6,7 @@ import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import { prisma } from '@documenso/prisma'; +import type { Document, DocumentMeta, Recipient, User } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; @@ -27,110 +28,178 @@ export const deleteDocument = async ({ teamId, requestMetadata, }: DeleteDocumentOptions) => { + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + const document = await prisma.document.findUnique({ where: { id, - ...(teamId - ? { - team: { - id: teamId, - members: { - some: { - userId, - }, - }, - }, - } - : { - userId, - teamId: null, - }), }, include: { Recipient: true, documentMeta: true, - User: true, + team: { + select: { + members: true, + }, + }, }, }); - if (!document) { + if (!document || (teamId !== undefined && teamId !== document.teamId)) { throw new Error('Document not found'); } - const { status, User: user } = document; + const isUserOwner = document.userId === userId; + const isUserTeamMember = document.team?.members.some((member) => member.userId === userId); + const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email); - // if the document is a draft, hard-delete - if (status === DocumentStatus.DRAFT) { + if (!isUserOwner && !isUserTeamMember && !userRecipient) { + throw new Error('Not allowed'); + } + + // Handle hard or soft deleting the actual document if user has permission. + if (isUserOwner || isUserTeamMember) { + await handleDocumentOwnerDelete({ + document, + user, + requestMetadata, + }); + } + + // Continue to hide the document from the user if they are a recipient. + if (userRecipient?.documentDeletedAt === null) { + await prisma.recipient.update({ + where: { + documentId_email: { + documentId: document.id, + email: user.email, + }, + }, + data: { + documentDeletedAt: new Date().toISOString(), + }, + }); + } + + // Return partial document for API v1 response. + return { + id: document.id, + userId: document.userId, + teamId: document.teamId, + title: document.title, + status: document.status, + documentDataId: document.documentDataId, + createdAt: document.createdAt, + updatedAt: document.updatedAt, + completedAt: document.completedAt, + }; +}; + +type HandleDocumentOwnerDeleteOptions = { + document: Document & { + Recipient: Recipient[]; + documentMeta: DocumentMeta | null; + }; + user: User; + requestMetadata?: RequestMetadata; +}; + +const handleDocumentOwnerDelete = async ({ + document, + user, + requestMetadata, +}: HandleDocumentOwnerDeleteOptions) => { + if (document.deletedAt) { + return; + } + + // Soft delete completed documents. + if (document.status === DocumentStatus.COMPLETED) { return await prisma.$transaction(async (tx) => { - // Currently redundant since deleting a document will delete the audit logs. - // However may be useful if we disassociate audit lgos and documents if required. await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId: id, + documentId: document.id, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, user, requestMetadata, data: { - type: 'HARD', + type: 'SOFT', }, }), }); - return await tx.document.delete({ where: { id, status: DocumentStatus.DRAFT } }); + return await tx.document.update({ + where: { + id: document.id, + }, + data: { + deletedAt: new Date().toISOString(), + }, + }); }); } - // if the document is pending, send cancellation emails to all recipients - if (status === DocumentStatus.PENDING && document.Recipient.length > 0) { - await Promise.all( - document.Recipient.map(async (recipient) => { - const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; - - const template = createElement(DocumentCancelTemplate, { - documentName: document.title, - inviterName: user.name || undefined, - inviterEmail: user.email, - assetBaseUrl, - }); - - await mailer.sendMail({ - to: { - address: recipient.email, - name: recipient.name, - }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: 'Document Cancelled', - html: render(template), - text: render(template, { plainText: true }), - }); - }), - ); - } - - // If the document is not a draft, only soft-delete. - return await prisma.$transaction(async (tx) => { + // Hard delete draft and pending documents. + const deletedDocument = await prisma.$transaction(async (tx) => { + // Currently redundant since deleting a document will delete the audit logs. + // However may be useful if we disassociate audit logs and documents if required. await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId: id, + documentId: document.id, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, user, requestMetadata, data: { - type: 'SOFT', + type: 'HARD', }, }), }); - return await tx.document.update({ + return await tx.document.delete({ where: { - id, - }, - data: { - deletedAt: new Date().toISOString(), + id: document.id, + status: { + not: DocumentStatus.COMPLETED, + }, }, }); }); + + // Send cancellation emails to recipients. + await Promise.all( + document.Recipient.map(async (recipient) => { + const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; + + const template = createElement(DocumentCancelTemplate, { + documentName: document.title, + inviterName: user.name || undefined, + inviterEmail: user.email, + assetBaseUrl, + }); + + await mailer.sendMail({ + to: { + address: recipient.email, + name: recipient.name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: 'Document Cancelled', + html: render(template), + text: render(template, { plainText: true }), + }); + }), + ); + + return deletedDocument; }; diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index f34cc4c2c..c8b06236b 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -94,24 +94,65 @@ export const findDocuments = async ({ }; } - const whereClause: Prisma.DocumentWhereInput = { - ...termFilters, - ...filters, + let deletedFilter: Prisma.DocumentWhereInput = { AND: { OR: [ { - status: ExtendedDocumentStatus.COMPLETED, + userId: user.id, + deletedAt: null, }, { - status: { - not: ExtendedDocumentStatus.COMPLETED, + Recipient: { + some: { + email: user.email, + documentDeletedAt: null, + }, }, - deletedAt: null, }, ], }, }; + if (team) { + deletedFilter = { + AND: { + OR: team.teamEmail + ? [ + { + teamId: team.id, + deletedAt: null, + }, + { + User: { + email: team.teamEmail.email, + }, + deletedAt: null, + }, + { + Recipient: { + some: { + email: team.teamEmail.email, + documentDeletedAt: null, + }, + }, + }, + ] + : [ + { + teamId: team.id, + deletedAt: null, + }, + ], + }, + }; + } + + const whereClause: Prisma.DocumentWhereInput = { + ...termFilters, + ...filters, + ...deletedFilter, + }; + if (period) { const daysAgo = parseInt(period.replace(/d$/, ''), 10); diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index db38fa79d..1afdbcbf2 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -72,6 +72,7 @@ type GetCountsOption = { const getCounts = async ({ user, createdAt }: GetCountsOption) => { return Promise.all([ + // Owner counts. prisma.document.groupBy({ by: ['status'], _count: { @@ -84,6 +85,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => { deletedAt: null, }, }), + // Not signed counts. prisma.document.groupBy({ by: ['status'], _count: { @@ -95,12 +97,13 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => { some: { email: user.email, signingStatus: SigningStatus.NOT_SIGNED, + documentDeletedAt: null, }, }, createdAt, - deletedAt: null, }, }), + // Has signed counts. prisma.document.groupBy({ by: ['status'], _count: { @@ -120,9 +123,9 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => { some: { email: user.email, signingStatus: SigningStatus.SIGNED, + documentDeletedAt: null, }, }, - deletedAt: null, }, { status: ExtendedDocumentStatus.COMPLETED, @@ -130,6 +133,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => { some: { email: user.email, signingStatus: SigningStatus.SIGNED, + documentDeletedAt: null, }, }, }, @@ -198,6 +202,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { some: { email: teamEmail, signingStatus: SigningStatus.NOT_SIGNED, + documentDeletedAt: null, }, }, deletedAt: null, @@ -219,6 +224,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { some: { email: teamEmail, signingStatus: SigningStatus.SIGNED, + documentDeletedAt: null, }, }, deletedAt: null, @@ -229,6 +235,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { some: { email: teamEmail, signingStatus: SigningStatus.SIGNED, + documentDeletedAt: null, }, }, deletedAt: null, diff --git a/packages/prisma/migrations/20240408142543_add_recipient_document_delete/migration.sql b/packages/prisma/migrations/20240408142543_add_recipient_document_delete/migration.sql new file mode 100644 index 000000000..6bbb11cd9 --- /dev/null +++ b/packages/prisma/migrations/20240408142543_add_recipient_document_delete/migration.sql @@ -0,0 +1,13 @@ +-- AlterTable +ALTER TABLE "Recipient" ADD COLUMN "documentDeletedAt" TIMESTAMP(3); + +-- Hard delete all PENDING documents that have been soft deleted +DELETE FROM "Document" WHERE "deletedAt" IS NOT NULL AND "status" = 'PENDING'; + +-- Update all recipients who are the owner of the document and where the document has deletedAt set to not null +UPDATE "Recipient" +SET "documentDeletedAt" = "Document"."deletedAt" +FROM "Document", "User" +WHERE "Recipient"."documentId" = "Document"."id" +AND "Recipient"."email" = "User"."email" +AND "Document"."deletedAt" IS NOT NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 35d429779..8971f837f 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -347,23 +347,24 @@ enum RecipientRole { } model Recipient { - id Int @id @default(autoincrement()) - documentId Int? - templateId Int? - email String @db.VarChar(255) - name String @default("") @db.VarChar(255) - token String - expired DateTime? - signedAt DateTime? - authOptions Json? - role RecipientRole @default(SIGNER) - readStatus ReadStatus @default(NOT_OPENED) - signingStatus SigningStatus @default(NOT_SIGNED) - sendStatus SendStatus @default(NOT_SENT) - Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) - Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) - Field Field[] - Signature Signature[] + id Int @id @default(autoincrement()) + documentId Int? + templateId Int? + email String @db.VarChar(255) + name String @default("") @db.VarChar(255) + token String + documentDeletedAt DateTime? + expired DateTime? + signedAt DateTime? + authOptions Json? + role RecipientRole @default(SIGNER) + readStatus ReadStatus @default(NOT_OPENED) + signingStatus SigningStatus @default(NOT_SIGNED) + sendStatus SendStatus @default(NOT_SENT) + Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) + Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) + Field Field[] + Signature Signature[] @@unique([documentId, email]) @@unique([templateId, email]) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 25169bcec..b796f4328 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -247,9 +247,7 @@ export const AddSignersFormPartial = ({ 'col-span-4': showAdvancedSettings, })} > - {!showAdvancedSettings && index === 0 && ( - Name - )} + {!showAdvancedSettings && index === 0 && Name} Date: Fri, 19 Apr 2024 13:45:33 +0300 Subject: [PATCH 031/100] feat: update emails for self-signer (#1108) ## Description Updated the email content based on whether the document owner is a recipient or not. If the document owner is a recipient (self-signer): * the email subject will be `Please view/sign/approve your document` * the email header will be `Please view/sign/approve your document ""` * the email content will be `You have initiated the document "" that requires you to view/sign/approve it.` Otherwise: * the email subject will be `Please view/sign/approve this document` * the email header will be ` has invited you to view/sign/approve ""` * the email content will be ` has invited you to view/sign/approve the document "".` ## Related Issue Related to #1091 ## Testing Performed Tested the feature with a different number of recipients (including and excluding the document owner - self-signer). Tested both the sending and resending functionality. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## UI Screenshots ![CleanShot 2024-04-18 at 12 26 11@2x](https://github.com/documenso/documenso/assets/25515812/ca80f625-befb-4cbc-a541-f2186379d2e8) ![CleanShot 2024-04-18 at 12 27 40@2x](https://github.com/documenso/documenso/assets/25515812/8bcbb6fc-ba98-4fa1-8538-2d062febd27b) ![CleanShot 2024-04-18 at 12 27 53@2x](https://github.com/documenso/documenso/assets/25515812/25d77d98-b5ec-4270-8ffa-43774fe70526) ![CleanShot 2024-04-18 at 12 30 00@2x](https://github.com/documenso/documenso/assets/25515812/a90bb8e3-3ea8-42ff-9971-559b3e81ae6f) ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced the document invitation components to support scenarios where the recipient is also the sender, providing customized email content and subject lines. - Introduced new properties in email templates to improve clarity and relevance based on the user's role in the document signing process. - **Refactor** - Updated components to use a more flexible `headerContent` property for displaying invitation headers, replacing previous individual inviter details. --- .../template-document-invite.tsx | 17 +++++++++++++++-- packages/email/templates/document-invite.tsx | 7 ++++++- .../server-only/document/resend-document.tsx | 17 +++++++++++++++-- .../lib/server-only/document/send-document.tsx | 17 +++++++++++++++-- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx index b958e9029..b99b1a1b4 100644 --- a/packages/email/template-components/template-document-invite.tsx +++ b/packages/email/template-components/template-document-invite.tsx @@ -11,6 +11,7 @@ export interface TemplateDocumentInviteProps { signDocumentLink: string; assetBaseUrl: string; role: RecipientRole; + selfSigner: boolean; } export const TemplateDocumentInvite = ({ @@ -19,6 +20,7 @@ export const TemplateDocumentInvite = ({ signDocumentLink, assetBaseUrl, role, + selfSigner, }: TemplateDocumentInviteProps) => { const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role]; @@ -28,8 +30,19 @@ export const TemplateDocumentInvite = ({
- {inviterName} has invited you to {actionVerb.toLowerCase()} -
"{documentName}" + {selfSigner ? ( + <> + {`Please ${actionVerb.toLowerCase()} your document`} +
+ {`"${documentName}"`} + + ) : ( + <> + {`${inviterName} has invited you to ${actionVerb.toLowerCase()}`} +
+ {`"${documentName}"`} + + )}
diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index d3bceb872..52a40d804 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -22,6 +22,7 @@ import { TemplateFooter } from '../template-components/template-footer'; export type DocumentInviteEmailTemplateProps = Partial & { customBody?: string; role: RecipientRole; + selfSigner?: boolean; }; export const DocumentInviteEmailTemplate = ({ @@ -32,10 +33,13 @@ export const DocumentInviteEmailTemplate = ({ assetBaseUrl = 'http://localhost:3002', customBody, role, + selfSigner = false, }: DocumentInviteEmailTemplateProps) => { const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase(); - const previewText = `${inviterName} has invited you to ${action} ${documentName}`; + const previewText = selfSigner + ? `Please ${action} your document ${documentName}` + : `${inviterName} has invited you to ${action} ${documentName}`; const getAssetUrl = (path: string) => { return new URL(path, assetBaseUrl).toString(); @@ -71,6 +75,7 @@ export const DocumentInviteEmailTemplate = ({ signDocumentLink={signDocumentLink} assetBaseUrl={assetBaseUrl} role={role} + selfSigner={selfSigner} />
diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index ebf140007..500c5395a 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -88,6 +88,11 @@ export const resendDocument = async ({ const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; const { email, name } = recipient; + const selfSigner = email === user.email; + + const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[ + recipient.role + ].actionVerb.toLowerCase()} it.`; const customEmailTemplate = { 'signer.name': name, @@ -104,12 +109,20 @@ export const resendDocument = async ({ inviterEmail: user.email, assetBaseUrl, signDocumentLink, - customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate), + customBody: renderCustomEmailTemplate( + selfSigner ? selfSignerCustomEmail : customEmail?.message || '', + customEmailTemplate, + ), role: recipient.role, + selfSigner, }); const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; + const emailSubject = selfSigner + ? `Reminder: Please ${actionVerb.toLowerCase()} your document` + : `Reminder: Please ${actionVerb.toLowerCase()} this document`; + await prisma.$transaction( async (tx) => { await mailer.sendMail({ @@ -123,7 +136,7 @@ export const resendDocument = async ({ }, subject: customEmail?.subject ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) - : `Please ${actionVerb.toLowerCase()} this document`, + : emailSubject, html: render(template), text: render(template, { plainText: true }), }); diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index acbcc499f..5bb7e2352 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -127,6 +127,11 @@ export const sendDocument = async ({ const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; const { email, name } = recipient; + const selfSigner = email === user.email; + + const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[ + recipient.role + ].actionVerb.toLowerCase()} it.`; const customEmailTemplate = { 'signer.name': name, @@ -143,12 +148,20 @@ export const sendDocument = async ({ inviterEmail: user.email, assetBaseUrl, signDocumentLink, - customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate), + customBody: renderCustomEmailTemplate( + selfSigner ? selfSignerCustomEmail : customEmail?.message || '', + customEmailTemplate, + ), role: recipient.role, + selfSigner, }); const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; + const emailSubject = selfSigner + ? `Please ${actionVerb.toLowerCase()} your document` + : `Please ${actionVerb.toLowerCase()} this document`; + await prisma.$transaction( async (tx) => { await mailer.sendMail({ @@ -162,7 +175,7 @@ export const sendDocument = async ({ }, subject: customEmail?.subject ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) - : `Please ${actionVerb.toLowerCase()} this document`, + : emailSubject, html: render(template), text: render(template, { plainText: true }), }); From f6e6dac46c681f6666ec90cd5245482f4030c0a5 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 19 Apr 2024 17:58:32 +0700 Subject: [PATCH 032/100] fix: update migration to drop invalid fields --- .../migration.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql b/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql index 62845de28..ee027d90e 100644 --- a/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql +++ b/packages/prisma/migrations/20240418140819_remove_impossible_field_optional_states/migration.sql @@ -4,5 +4,8 @@ - Made the column `recipientId` on table `Field` required. This step will fail if there are existing NULL values in that column. */ +-- Drop all Fields where the recipientId is null +DELETE FROM "Field" WHERE "recipientId" IS NULL; + -- AlterTable ALTER TABLE "Field" ALTER COLUMN "recipientId" SET NOT NULL; From 4b90adde6baf5714f9bb27f655af0981c4309d7a Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:04:11 +0300 Subject: [PATCH 033/100] feat: download completed docs via api (#1078) ## Description Allow users to download a completed document via API. ## Testing Performed Tested the code locally by trying to download both draft and completed docs. Works as expected. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Implemented functionality to download signed documents directly from the app. --- packages/api/v1/contract.ts | 12 ++++++ packages/api/v1/implementation.ts | 67 ++++++++++++++++++++++++++++++- packages/api/v1/schema.ts | 4 ++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts index 162cdcf9d..ca2b6e2f5 100644 --- a/packages/api/v1/contract.ts +++ b/packages/api/v1/contract.ts @@ -11,6 +11,7 @@ import { ZDeleteDocumentMutationSchema, ZDeleteFieldMutationSchema, ZDeleteRecipientMutationSchema, + ZDownloadDocumentSuccessfulSchema, ZGetDocumentsQuerySchema, ZSendDocumentForSigningMutationSchema, ZSuccessfulDocumentResponseSchema, @@ -51,6 +52,17 @@ export const ApiContractV1 = c.router( summary: 'Get a single document', }, + downloadSignedDocument: { + method: 'GET', + path: '/api/v1/documents/:id/download', + responses: { + 200: ZDownloadDocumentSuccessfulSchema, + 401: ZUnsuccessfulResponseSchema, + 404: ZUnsuccessfulResponseSchema, + }, + summary: 'Download a signed document when the storage transport is S3', + }, + createDocument: { method: 'POST', path: '/api/v1/documents', diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index d9bc1a6d7..8ee0350bd 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -23,7 +23,10 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/ import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putFile } from '@documenso/lib/universal/upload/put-file'; -import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; +import { + getPresignGetUrl, + getPresignPostUrl, +} from '@documenso/lib/universal/upload/server-actions'; import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { ApiContractV1 } from './contract'; @@ -83,6 +86,68 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { } }), + downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => { + const { id: documentId } = args.params; + + try { + if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') { + return { + status: 500, + body: { + message: 'Please make sure the storage transport is set to S3.', + }, + }; + } + + const document = await getDocumentById({ + id: Number(documentId), + userId: user.id, + teamId: team?.id, + }); + + if (!document || !document.documentDataId) { + return { + status: 404, + body: { + message: 'Document not found', + }, + }; + } + + if (DocumentDataType.S3_PATH !== document.documentData.type) { + return { + status: 400, + body: { + message: 'Invalid document data type', + }, + }; + } + + if (document.status !== DocumentStatus.COMPLETED) { + return { + status: 400, + body: { + message: 'Document is not completed yet.', + }, + }; + } + + const { url } = await getPresignGetUrl(document.documentData.data); + + return { + status: 200, + body: { downloadUrl: url }, + }; + } catch (err) { + return { + status: 500, + body: { + message: 'Error downloading the document. Please try again.', + }, + }; + } + }), + deleteDocument: authenticatedMiddleware(async (args, user, team) => { const { id: documentId } = args.params; diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index 01f6e2d58..be0ea1271 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -53,6 +53,10 @@ export const ZUploadDocumentSuccessfulSchema = z.object({ key: z.string(), }); +export const ZDownloadDocumentSuccessfulSchema = z.object({ + downloadUrl: z.string(), +}); + export type TUploadDocumentSuccessfulSchema = z.infer; export const ZCreateDocumentMutationSchema = z.object({ From afaeba97393094017dc1c85f544ea4b6bf4cc16d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 22 Apr 2024 13:31:49 +0700 Subject: [PATCH 034/100] fix: resize fields --- .../document/document-read-only-fields.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/web/src/components/document/document-read-only-fields.tsx index 530066fa8..95a907d8f 100644 --- a/apps/web/src/components/document/document-read-only-fields.tsx +++ b/apps/web/src/components/document/document-read-only-fields.tsx @@ -75,7 +75,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
-
+
{match(field) .with({ type: FieldType.SIGNATURE }, (field) => field.Signature?.signatureImageAsBase64 ? ( @@ -90,18 +90,17 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl

), ) - .with({ type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, () => ( -

{field.customText}

- )) - .with({ type: FieldType.DATE }, () => ( -

- {convertToLocalSystemFormat( - field.customText, - documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, - documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, - )} -

- )) + .with( + { type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, + () => field.customText, + ) + .with({ type: FieldType.DATE }, () => + convertToLocalSystemFormat( + field.customText, + documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + ), + ) .with({ type: FieldType.FREE_SIGNATURE }, () => null) .exhaustive()}
From 0eee57078101fc31cfb656f768187aa25413514e Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:33:40 +0300 Subject: [PATCH 035/100] fix: complete document when all recipients are CC --- .../documents/[id]/edit-document.tsx | 13 ++++++++++++- .../trpc/server/document-router/router.ts | 19 +++++++++++++++++++ .../trpc/server/document-router/schema.ts | 6 ++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 2e2f0c889..fbc700219 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -8,6 +8,7 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META, } from '@documenso/lib/constants/trpc'; +import { DocumentStatus, RecipientRole } from '@documenso/prisma/client'; import type { DocumentWithDetails } from '@documenso/prisma/types/document'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -68,6 +69,10 @@ export const EditDocumentForm = ({ const { Recipient: recipients, Field: fields } = document; + const allRecipientsAreCC = recipients.every((recipient) => recipient.role === RecipientRole.CC); + + const { mutateAsync: updateDocumentStatus } = trpc.document.updateDocument.useMutation(); + const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, onSuccess: (newData) => { @@ -248,8 +253,14 @@ export const EditDocumentForm = ({ const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { const { subject, message } = data.meta; - try { + if (allRecipientsAreCC) { + await updateDocumentStatus({ + documentId: document.id, + data: { status: DocumentStatus.COMPLETED }, + }); + } + await sendDocument({ documentId: document.id, teamId: team?.id, diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index d12002674..fa9d95262 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -16,6 +16,7 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/ import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; +import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings'; import { updateTitle } from '@documenso/lib/server-only/document/update-title'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; @@ -36,6 +37,7 @@ import { ZSetPasswordForDocumentMutationSchema, ZSetSettingsForDocumentMutationSchema, ZSetTitleForDocumentMutationSchema, + ZUpdateDocumentMutationSchema, } from './schema'; export const documentRouter = router({ @@ -132,6 +134,23 @@ export const documentRouter = router({ } }), + updateDocument: authenticatedProcedure + .input(ZUpdateDocumentMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { documentId, data } = input; + + await updateDocument({ documentId, data, userId: ctx.user.id }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to delete this document. Please try again later.', + }); + } + }), + deleteDocument: authenticatedProcedure .input(ZDeleteDocumentMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 483d32e50..4936aae37 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -48,6 +48,12 @@ export const ZCreateDocumentMutationSchema = z.object({ teamId: z.number().optional(), }); +export const ZUpdateDocumentMutationSchema = z.object({ + documentId: z.number().min(1), + teamId: z.number().optional(), + data: z.any(), +}); + export type TCreateDocumentMutationSchema = z.infer; export const ZSetSettingsForDocumentMutationSchema = z.object({ From 4d5365bddcd94dacad4fd6c4df3e84652b6f9504 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:24:58 +0300 Subject: [PATCH 036/100] fix: complete document when all recipients are CC --- .../documents/[id]/edit-document.tsx | 12 ------------ .../server-only/document/send-document.tsx | 6 +++++- .../trpc/server/document-router/router.ts | 19 ------------------- .../trpc/server/document-router/schema.ts | 6 ------ 4 files changed, 5 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index fbc700219..e175ae18a 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -8,7 +8,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META, } from '@documenso/lib/constants/trpc'; -import { DocumentStatus, RecipientRole } from '@documenso/prisma/client'; import type { DocumentWithDetails } from '@documenso/prisma/types/document'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -69,10 +68,6 @@ export const EditDocumentForm = ({ const { Recipient: recipients, Field: fields } = document; - const allRecipientsAreCC = recipients.every((recipient) => recipient.role === RecipientRole.CC); - - const { mutateAsync: updateDocumentStatus } = trpc.document.updateDocument.useMutation(); - const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, onSuccess: (newData) => { @@ -254,13 +249,6 @@ export const EditDocumentForm = ({ const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { const { subject, message } = data.meta; try { - if (allRecipientsAreCC) { - await updateDocumentStatus({ - documentId: document.id, - data: { status: DocumentStatus.COMPLETED }, - }); - } - await sendDocument({ documentId: document.id, teamId: team?.id, diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 5bb7e2352..6b9ab8037 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -211,6 +211,10 @@ export const sendDocument = async ({ }), ); + const allRecipientsAreCC = document.Recipient.every( + (recipient) => recipient.role === RecipientRole.CC, + ); + const updatedDocument = await prisma.$transaction(async (tx) => { if (document.status === DocumentStatus.DRAFT) { await tx.documentAuditLog.create({ @@ -229,7 +233,7 @@ export const sendDocument = async ({ id: documentId, }, data: { - status: DocumentStatus.PENDING, + status: allRecipientsAreCC ? DocumentStatus.COMPLETED : DocumentStatus.PENDING, }, include: { Recipient: true, diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index fa9d95262..d12002674 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -16,7 +16,6 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/ import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; -import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings'; import { updateTitle } from '@documenso/lib/server-only/document/update-title'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; @@ -37,7 +36,6 @@ import { ZSetPasswordForDocumentMutationSchema, ZSetSettingsForDocumentMutationSchema, ZSetTitleForDocumentMutationSchema, - ZUpdateDocumentMutationSchema, } from './schema'; export const documentRouter = router({ @@ -134,23 +132,6 @@ export const documentRouter = router({ } }), - updateDocument: authenticatedProcedure - .input(ZUpdateDocumentMutationSchema) - .mutation(async ({ input, ctx }) => { - try { - const { documentId, data } = input; - - await updateDocument({ documentId, data, userId: ctx.user.id }); - } catch (err) { - console.error(err); - - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'We were unable to delete this document. Please try again later.', - }); - } - }), - deleteDocument: authenticatedProcedure .input(ZDeleteDocumentMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 4936aae37..483d32e50 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -48,12 +48,6 @@ export const ZCreateDocumentMutationSchema = z.object({ teamId: z.number().optional(), }); -export const ZUpdateDocumentMutationSchema = z.object({ - documentId: z.number().min(1), - teamId: z.number().optional(), - data: z.any(), -}); - export type TCreateDocumentMutationSchema = z.infer; export const ZSetSettingsForDocumentMutationSchema = z.object({ From bb43547a459dc3ef3ae0d02ccf2607b674373f67 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:39:47 +0300 Subject: [PATCH 037/100] fix: complete document when all recipients are CC --- .../lib/server-only/document/send-document.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 6b9ab8037..8d92de3a3 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -4,6 +4,8 @@ import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; +import { sealDocument } from '@documenso/lib/server-only/document/seal-document'; +import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; @@ -215,6 +217,17 @@ export const sendDocument = async ({ (recipient) => recipient.role === RecipientRole.CC, ); + if (allRecipientsAreCC) { + const updatedDocument = await updateDocument({ + documentId, + userId, + teamId, + data: { status: DocumentStatus.COMPLETED }, + }); + + return await sealDocument({ documentId: updatedDocument.id, requestMetadata }); + } + const updatedDocument = await prisma.$transaction(async (tx) => { if (document.status === DocumentStatus.DRAFT) { await tx.documentAuditLog.create({ @@ -233,7 +246,7 @@ export const sendDocument = async ({ id: documentId, }, data: { - status: allRecipientsAreCC ? DocumentStatus.COMPLETED : DocumentStatus.PENDING, + status: DocumentStatus.PENDING, }, include: { Recipient: true, From d7959950e21684286caad13cbddacaa46b4d559e Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:41:34 +0300 Subject: [PATCH 038/100] fix: edit-document line --- apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index e175ae18a..2e2f0c889 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -248,6 +248,7 @@ export const EditDocumentForm = ({ const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { const { subject, message } = data.meta; + try { await sendDocument({ documentId: document.id, From 87423e240af3c915b7ec70eca865f3fc1f3a5fe1 Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 24 Apr 2024 17:32:11 +1000 Subject: [PATCH 039/100] chore: update foreign key constraints --- .../migration.sql | 23 +++++++++++++++++++ packages/prisma/schema.prisma | 10 ++++---- 2 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 packages/prisma/migrations/20240424072655_update_foreign_key_constraints/migration.sql diff --git a/packages/prisma/migrations/20240424072655_update_foreign_key_constraints/migration.sql b/packages/prisma/migrations/20240424072655_update_foreign_key_constraints/migration.sql new file mode 100644 index 000000000..89c38943d --- /dev/null +++ b/packages/prisma/migrations/20240424072655_update_foreign_key_constraints/migration.sql @@ -0,0 +1,23 @@ +-- DropForeignKey +ALTER TABLE "PasswordResetToken" DROP CONSTRAINT "PasswordResetToken_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Signature" DROP CONSTRAINT "Signature_fieldId_fkey"; + +-- DropForeignKey +ALTER TABLE "Team" DROP CONSTRAINT "Team_ownerUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "TeamMember" DROP CONSTRAINT "TeamMember_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Signature" ADD CONSTRAINT "Signature_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "Field"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Team" ADD CONSTRAINT "Team_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8971f837f..97b6e9eeb 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -98,7 +98,7 @@ model PasswordResetToken { createdAt DateTime @default(now()) expiry DateTime userId Int - User User @relation(fields: [userId], references: [id]) + User User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Passkey { @@ -415,7 +415,7 @@ model Signature { typedSignature String? Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) - Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict) + Field Field @relation(fields: [fieldId], references: [id], onDelete: Cascade) @@index([recipientId]) } @@ -457,7 +457,7 @@ model Team { emailVerification TeamEmailVerification? transferVerification TeamTransferVerification? - owner User @relation(fields: [ownerUserId], references: [id]) + owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade) subscription Subscription? document Document[] @@ -483,7 +483,7 @@ model TeamMember { createdAt DateTime @default(now()) role TeamMemberRole userId Int - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) @@unique([userId, teamId]) @@ -564,5 +564,5 @@ model SiteSettings { data Json lastModifiedByUserId Int? lastModifiedAt DateTime @default(now()) - lastModifiedByUser User? @relation(fields: [lastModifiedByUserId], references: [id]) + lastModifiedByUser User? @relation(fields: [lastModifiedByUserId], references: [id], onDelete: SetNull) } From 713cd09a063d1d97db6e753fb0b2edef29b5357f Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 24 Apr 2024 19:07:18 +1000 Subject: [PATCH 040/100] fix: downgrade playwright --- package-lock.json | 74 +++++++++++++++++++++++---------------- package.json | 2 +- packages/lib/package.json | 4 +-- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb03b3a67..83d9523c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "eslint-config-custom": "*", "husky": "^9.0.11", "lint-staged": "^15.2.2", - "playwright": "^1.43.0", + "playwright": "1.41.0", "prettier": "^2.5.1", "rimraf": "^5.0.1", "turbo": "^1.9.3" @@ -4702,19 +4702,6 @@ "node": ">=14" } }, - "node_modules/@playwright/browser-chromium": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.43.0.tgz", - "integrity": "sha512-F0S4KIqSqQqm9EgsdtWjaJRpgP8cD2vWZHPSB41YI00PtXUobiv/3AnYISeL7wNuTanND7giaXQ4SIjkcIq3KQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "playwright-core": "1.43.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@playwright/test": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", @@ -17673,11 +17660,11 @@ } }, "node_modules/playwright": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", - "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==", + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.0.tgz", + "integrity": "sha512-XOsfl5ZtAik/T9oek4V0jAypNlaCNzuKOwVhqhgYT3os6kH34PzbRb74F0VWcLYa5WFdnmxl7qyAHBXvPv7lqQ==", "dependencies": { - "playwright-core": "1.43.0" + "playwright-core": "1.41.0" }, "bin": { "playwright": "cli.js" @@ -17689,17 +17676,6 @@ "fsevents": "2.3.2" } }, - "node_modules/playwright-core": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", - "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/playwright/node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -17713,6 +17689,17 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.0.tgz", + "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -24981,7 +24968,7 @@ "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", - "playwright": "^1.43.0", + "playwright": "1.41.0", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -24989,10 +24976,23 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/browser-chromium": "^1.43.0", + "@playwright/browser-chromium": "1.41.0", "@types/luxon": "^3.3.1" } }, + "packages/lib/node_modules/@playwright/browser-chromium": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.41.0.tgz", + "integrity": "sha512-TaHfh3rDsz4+tVKdMMo4kdFOk8/4U6cPyMXHhoiJVmhOhjHXjR0qPMoa5gz5jDGl478cn5SoXmtgKPgTDFuS0g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.41.0" + }, + "engines": { + "node": ">=16" + } + }, "packages/lib/node_modules/nanoid": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", @@ -25010,6 +25010,18 @@ "node": "^14 || ^16 || >=18" } }, + "packages/lib/node_modules/playwright-core": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.0.tgz", + "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "packages/prettier-config": { "name": "@documenso/prettier-config", "version": "0.0.0", diff --git a/package.json b/package.json index 396b2ecfd..70ed541e1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "eslint-config-custom": "*", "husky": "^9.0.11", "lint-staged": "^15.2.2", - "playwright": "^1.43.0", + "playwright": "1.41.0", "prettier": "^2.5.1", "rimraf": "^5.0.1", "turbo": "^1.9.3" diff --git a/packages/lib/package.json b/packages/lib/package.json index 1aa7e431e..5e40e047b 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -39,7 +39,7 @@ "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", - "playwright": "^1.43.0", + "playwright": "1.41.0", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -48,6 +48,6 @@ }, "devDependencies": { "@types/luxon": "^3.3.1", - "@playwright/browser-chromium": "^1.43.0" + "@playwright/browser-chromium": "1.41.0" } } \ No newline at end of file From 41ed6c9ad7f34c9ea430808a8e4f5cf95d9e7037 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 24 Apr 2024 19:49:10 +0700 Subject: [PATCH 041/100] fix: disable cert download when document not complete --- .../documents/[id]/logs/document-logs-page-view.tsx | 6 +++++- .../documents/[id]/logs/download-certificate-button.tsx | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx index 2d786b9c9..0556fcd2d 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx @@ -133,7 +133,11 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
- +
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx index 49a330b94..1f2028358 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx @@ -2,6 +2,7 @@ import { DownloadIcon } from 'lucide-react'; +import { DocumentStatus } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -10,11 +11,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; export type DownloadCertificateButtonProps = { className?: string; documentId: number; + documentStatus: DocumentStatus; }; export const DownloadCertificateButton = ({ className, documentId, + documentStatus, }: DownloadCertificateButtonProps) => { const { toast } = useToast(); @@ -69,6 +72,7 @@ export const DownloadCertificateButton = ({ className={cn('w-full sm:w-auto', className)} loading={isLoading} variant="outline" + disabled={documentStatus !== DocumentStatus.COMPLETED} onClick={() => void onDownloadCertificatesClick()} > {!isLoading && } From e4cf9c82518a6f38ca1626c8899c89abe92efa46 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 24 Apr 2024 19:51:18 +0700 Subject: [PATCH 042/100] fix: add server logic --- packages/trpc/server/document-router/router.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index d12002674..64f3c2480 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -4,6 +4,7 @@ import { DateTime } from 'luxon'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; +import { AppError } from '@documenso/lib/errors/app-error'; import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt'; import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { createDocument } from '@documenso/lib/server-only/document/create-document'; @@ -20,6 +21,7 @@ import { updateDocumentSettings } from '@documenso/lib/server-only/document/upda import { updateTitle } from '@documenso/lib/server-only/document/update-title'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { DocumentStatus } from '@documenso/prisma/client'; import { authenticatedProcedure, procedure, router } from '../trpc'; import { @@ -413,6 +415,10 @@ export const documentRouter = router({ teamId, }); + if (document.status !== DocumentStatus.COMPLETED) { + throw new AppError('DOCUMENT_NOT_COMPLETE'); + } + const encrypted = encryptSecondaryData({ data: document.id.toString(), expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(), From 4de122f814b4cbb50ddb9b838f42c5d198102489 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 24 Apr 2024 20:07:38 +0700 Subject: [PATCH 043/100] fix: hide account action reauth --- .../primitives/document-flow/add-settings.tsx | 22 ++++++++++++------- .../primitives/document-flow/add-signers.tsx | 16 ++++++++------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-settings.tsx b/packages/ui/primitives/document-flow/add-settings.tsx index ea962dee5..ce52e03c2 100644 --- a/packages/ui/primitives/document-flow/add-settings.tsx +++ b/packages/ui/primitives/document-flow/add-settings.tsx @@ -9,7 +9,11 @@ import { useForm } from 'react-hook-form'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; -import { DocumentAccessAuth, DocumentActionAuth } from '@documenso/lib/types/document-auth'; +import { + DocumentAccessAuth, + DocumentActionAuth, + DocumentAuth, +} from '@documenso/lib/types/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; @@ -216,9 +220,9 @@ export const AddSettingsFormPartial = ({

    -
  • + {/*
  • Require account - The recipient must be signed in -
  • + */}
  • Require passkey - The recipient must have an account and passkey configured via their settings @@ -242,11 +246,13 @@ export const AddSettingsFormPartial = ({ - {Object.values(DocumentActionAuth).map((authType) => ( - - {DOCUMENT_AUTH_TYPES[authType].value} - - ))} + {Object.values(DocumentActionAuth) + .filter((auth) => auth !== DocumentAuth.ACCOUNT) + .map((authType) => ( + + {DOCUMENT_AUTH_TYPES[authType].value} + + ))} {/* Note: -1 is remapped in the Zod schema to the required value. */} None diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index b796f4328..2f9f2f234 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -302,10 +302,10 @@ export const AddSignersFormPartial = ({ global action signing authentication method configured in the "General Settings" step
  • -
  • + {/*
  • Require account - The recipient must be signed in -
  • + */}
  • Require passkey - The recipient must have an account and passkey configured via their settings @@ -326,11 +326,13 @@ export const AddSignersFormPartial = ({ {/* Note: -1 is remapped in the Zod schema to the required value. */} Inherit authentication method - {Object.values(RecipientActionAuth).map((authType) => ( - - {DOCUMENT_AUTH_TYPES[authType].value} - - ))} + {Object.values(RecipientActionAuth) + .filter((auth) => auth !== RecipientActionAuth.ACCOUNT) + .map((authType) => ( + + {DOCUMENT_AUTH_TYPES[authType].value} + + ))} From e1573465f6e66b67b47bb83bcb4e5fa0899a3711 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 25 Apr 2024 23:32:59 +0700 Subject: [PATCH 044/100] fix: hide team webhooks from users --- packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts index 121fc670d..0877d878f 100644 --- a/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts +++ b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts @@ -4,6 +4,7 @@ export const getWebhooksByUserId = async (userId: number) => { return await prisma.webhook.findMany({ where: { userId, + teamId: null, }, orderBy: { createdAt: 'desc', From 40808066069d189ad21ddaf06fdaf9fee612dd56 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 26 Apr 2024 02:17:56 +0000 Subject: [PATCH 045/100] fix: minor updates --- .../lib/server-only/document/send-document.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 8d92de3a3..9f68ed29b 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -213,11 +213,11 @@ export const sendDocument = async ({ }), ); - const allRecipientsAreCC = document.Recipient.every( + const allRecipientsHaveNoActionToTake = document.Recipient.every( (recipient) => recipient.role === RecipientRole.CC, ); - if (allRecipientsAreCC) { + if (allRecipientsHaveNoActionToTake) { const updatedDocument = await updateDocument({ documentId, userId, @@ -225,7 +225,17 @@ export const sendDocument = async ({ data: { status: DocumentStatus.COMPLETED }, }); - return await sealDocument({ documentId: updatedDocument.id, requestMetadata }); + await sealDocument({ documentId: updatedDocument.id, requestMetadata }); + + // Keep the return type the same for the `sendDocument` method + return await prisma.document.findFirstOrThrow({ + where: { + id: documentId, + }, + include: { + Recipient: true, + }, + }); } const updatedDocument = await prisma.$transaction(async (tx) => { From 88dedc98298a29d9c0438d9491ab45f3fc38e315 Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 26 Apr 2024 13:18:31 +1000 Subject: [PATCH 046/100] fix: use cdp and upgrade playwright again --- package-lock.json | 26 +++++++++---------- package.json | 2 +- packages/lib/package.json | 4 +-- .../htmltopdf/get-certificate-pdf.ts | 4 ++- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83d9523c5..e9e822cf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "eslint-config-custom": "*", "husky": "^9.0.11", "lint-staged": "^15.2.2", - "playwright": "1.41.0", + "playwright": "1.43.0", "prettier": "^2.5.1", "rimraf": "^5.0.1", "turbo": "^1.9.3" @@ -17660,11 +17660,11 @@ } }, "node_modules/playwright": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.0.tgz", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", "integrity": "sha512-XOsfl5ZtAik/T9oek4V0jAypNlaCNzuKOwVhqhgYT3os6kH34PzbRb74F0VWcLYa5WFdnmxl7qyAHBXvPv7lqQ==", "dependencies": { - "playwright-core": "1.41.0" + "playwright-core": "1.43.0" }, "bin": { "playwright": "cli.js" @@ -17690,8 +17690,8 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.0.tgz", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", "bin": { "playwright-core": "cli.js" @@ -24968,7 +24968,7 @@ "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", - "playwright": "1.41.0", + "playwright": "1.43.0", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -24976,18 +24976,18 @@ "zod": "^3.22.4" }, "devDependencies": { - "@playwright/browser-chromium": "1.41.0", + "@playwright/browser-chromium": "1.43.0", "@types/luxon": "^3.3.1" } }, "packages/lib/node_modules/@playwright/browser-chromium": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.41.0.tgz", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.43.0.tgz", "integrity": "sha512-TaHfh3rDsz4+tVKdMMo4kdFOk8/4U6cPyMXHhoiJVmhOhjHXjR0qPMoa5gz5jDGl478cn5SoXmtgKPgTDFuS0g==", "dev": true, "hasInstallScript": true, "dependencies": { - "playwright-core": "1.41.0" + "playwright-core": "1.43.0" }, "engines": { "node": ">=16" @@ -25011,8 +25011,8 @@ } }, "packages/lib/node_modules/playwright-core": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.0.tgz", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", "dev": true, "bin": { diff --git a/package.json b/package.json index 70ed541e1..3480aae28 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "eslint-config-custom": "*", "husky": "^9.0.11", "lint-staged": "^15.2.2", - "playwright": "1.41.0", + "playwright": "1.43.0", "prettier": "^2.5.1", "rimraf": "^5.0.1", "turbo": "^1.9.3" diff --git a/packages/lib/package.json b/packages/lib/package.json index 5e40e047b..c6144df92 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -39,7 +39,7 @@ "next-auth": "4.24.5", "oslo": "^0.17.0", "pdf-lib": "^1.17.1", - "playwright": "1.41.0", + "playwright": "1.43.0", "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", @@ -48,6 +48,6 @@ }, "devDependencies": { "@types/luxon": "^3.3.1", - "@playwright/browser-chromium": "1.41.0" + "@playwright/browser-chromium": "1.43.0" } } \ No newline at end of file diff --git a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts index a7182410e..dee40d41a 100644 --- a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts +++ b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts @@ -18,7 +18,9 @@ export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions let browser: Browser; if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) { - browser = await chromium.connect(process.env.NEXT_PRIVATE_BROWSERLESS_URL); + // !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version. + // !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors. + browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL); } else { browser = await chromium.launch(); } From 481d739c37e7ba147b9c3120a1bac47ea0f7919c Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 26 Apr 2024 13:25:16 +1000 Subject: [PATCH 047/100] chore: update package-lock --- package-lock.json | 84 ++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9e822cf2..479463b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4702,13 +4702,26 @@ "node": ">=14" } }, + "node_modules/@playwright/browser-chromium": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.43.0.tgz", + "integrity": "sha512-F0S4KIqSqQqm9EgsdtWjaJRpgP8cD2vWZHPSB41YI00PtXUobiv/3AnYISeL7wNuTanND7giaXQ4SIjkcIq3KQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.43.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@playwright/test": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", - "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", "dev": true, "dependencies": { - "playwright": "1.40.0" + "playwright": "1.43.1" }, "bin": { "playwright": "cli.js" @@ -4732,12 +4745,12 @@ } }, "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", - "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", "dev": true, "dependencies": { - "playwright-core": "1.40.0" + "playwright-core": "1.43.1" }, "bin": { "playwright": "cli.js" @@ -4750,9 +4763,9 @@ } }, "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", - "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -17662,7 +17675,7 @@ "node_modules/playwright": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", - "integrity": "sha512-XOsfl5ZtAik/T9oek4V0jAypNlaCNzuKOwVhqhgYT3os6kH34PzbRb74F0VWcLYa5WFdnmxl7qyAHBXvPv7lqQ==", + "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==", "dependencies": { "playwright-core": "1.43.0" }, @@ -17676,6 +17689,17 @@ "fsevents": "2.3.2" } }, + "node_modules/playwright-core": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", + "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/playwright/node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -17689,17 +17713,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", - "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -24980,19 +24993,6 @@ "@types/luxon": "^3.3.1" } }, - "packages/lib/node_modules/@playwright/browser-chromium": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.43.0.tgz", - "integrity": "sha512-TaHfh3rDsz4+tVKdMMo4kdFOk8/4U6cPyMXHhoiJVmhOhjHXjR0qPMoa5gz5jDGl478cn5SoXmtgKPgTDFuS0g==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "playwright-core": "1.43.0" - }, - "engines": { - "node": ">=16" - } - }, "packages/lib/node_modules/nanoid": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", @@ -25010,18 +25010,6 @@ "node": "^14 || ^16 || >=18" } }, - "packages/lib/node_modules/playwright-core": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", - "integrity": "sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, "packages/prettier-config": { "name": "@documenso/prettier-config", "version": "0.0.0", From 20edee7f1a2293344827f20deee372381aa25021 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 26 Apr 2024 16:01:09 +0700 Subject: [PATCH 048/100] fix: ssr feature flags (#1119) ## Description Feature flags are broken on SSR due to this error ``` TypeError: fetch failed at Object.fetch (node:internal/deps/undici/undici:11731:11) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) { cause: RequestContentLengthMismatchError: Request body length does not match content-length header at write (node:internal/deps/undici/undici:8590:41) at _resume (node:internal/deps/undici/undici:8563:33) at resume (node:internal/deps/undici/undici:8459:7) at [dispatch] (node:internal/deps/undici/undici:7704:11) at Client.Intercept (node:internal/deps/undici/undici:7377:20) at Client.dispatch (node:internal/deps/undici/undici:6023:44) at [dispatch] (node:internal/deps/undici/undici:6254:32) at Pool.dispatch (node:internal/deps/undici/undici:6023:44) at [dispatch] (node:internal/deps/undici/undici:9343:27) at Agent.Intercept (node:internal/deps/undici/undici:7377:20) { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' } } ``` I've removed content-length header since it isn't mandatory to my knowledge for get requests. ## Changes - Add fallback local flags when individual flag request fails - Add error logging - Remove `content-length` from headers being passed to Posthog --- packages/lib/universal/get-feature-flag.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/lib/universal/get-feature-flag.ts b/packages/lib/universal/get-feature-flag.ts index f4650f691..92f186ab3 100644 --- a/packages/lib/universal/get-feature-flag.ts +++ b/packages/lib/universal/get-feature-flag.ts @@ -17,6 +17,7 @@ export const getFlag = async ( options?: GetFlagOptions, ): Promise => { const requestHeaders = options?.requestHeaders ?? {}; + delete requestHeaders['content-length']; if (!isFeatureFlagEnabled()) { return LOCAL_FEATURE_FLAGS[flag] ?? true; @@ -25,7 +26,7 @@ export const getFlag = async ( const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`); url.searchParams.set('flag', flag); - const response = await fetch(url, { + return await fetch(url, { headers: { ...requestHeaders, }, @@ -35,9 +36,10 @@ export const getFlag = async ( }) .then(async (res) => res.json()) .then((res) => ZFeatureFlagValueSchema.parse(res)) - .catch(() => false); - - return response; + .catch((err) => { + console.error(err); + return LOCAL_FEATURE_FLAGS[flag] ?? false; + }); }; /** @@ -50,6 +52,7 @@ export const getAllFlags = async ( options?: GetFlagOptions, ): Promise> => { const requestHeaders = options?.requestHeaders ?? {}; + delete requestHeaders['content-length']; if (!isFeatureFlagEnabled()) { return LOCAL_FEATURE_FLAGS; @@ -67,7 +70,10 @@ export const getAllFlags = async ( }) .then(async (res) => res.json()) .then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res)) - .catch(() => LOCAL_FEATURE_FLAGS); + .catch((err) => { + console.error(err); + return LOCAL_FEATURE_FLAGS; + }); }; /** @@ -89,7 +95,10 @@ export const getAllAnonymousFlags = async (): Promise res.json()) .then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res)) - .catch(() => LOCAL_FEATURE_FLAGS); + .catch((err) => { + console.error(err); + return LOCAL_FEATURE_FLAGS; + }); }; interface GetFlagOptions { From 364c49992776fff2d804a3e6f38294b72b94485b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sat, 27 Apr 2024 15:21:46 +0700 Subject: [PATCH 049/100] fix: increase trpc max duration --- apps/web/src/pages/api/trpc/[trpc].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts index c43291ea1..3db86c50d 100644 --- a/apps/web/src/pages/api/trpc/[trpc].ts +++ b/apps/web/src/pages/api/trpc/[trpc].ts @@ -3,7 +3,7 @@ import { createTrpcContext } from '@documenso/trpc/server/context'; import { appRouter } from '@documenso/trpc/server/router'; export const config = { - maxDuration: 60, + maxDuration: 90, api: { bodyParser: { sizeLimit: '50mb', From 74b9bc786bcfed756d5f1f676c79ec919b2c34ee Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sat, 27 Apr 2024 18:29:52 +0700 Subject: [PATCH 050/100] fix: extend --- apps/web/src/pages/api/trpc/[trpc].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts index 3db86c50d..ba79244b5 100644 --- a/apps/web/src/pages/api/trpc/[trpc].ts +++ b/apps/web/src/pages/api/trpc/[trpc].ts @@ -3,7 +3,7 @@ import { createTrpcContext } from '@documenso/trpc/server/context'; import { appRouter } from '@documenso/trpc/server/router'; export const config = { - maxDuration: 90, + maxDuration: 120, api: { bodyParser: { sizeLimit: '50mb', From c98c1b94674c2ea73fc89ba782f35566dd5b6b2f Mon Sep 17 00:00:00 2001 From: Sumit Bisht Date: Sun, 28 Apr 2024 19:35:57 +0530 Subject: [PATCH 051/100] added teams url under a team name in teams section --- .../components/(dashboard)/layout/menu-switcher.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx index bb8429adc..4b90b5c93 100644 --- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx +++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx @@ -170,11 +170,20 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
    {teams.map((team) => ( - + + 1 && 'group-hover:hidden')}> + {formatSecondaryAvatarText(team)} + + 1 && 'group-hover:opacity-75')} + >{`/t/${team.url}`} + + } rightSideComponent={ isPathTeamUrl(team.url) && ( From 80c03fcf3f096c645ebd7b5d85191a9816e54068 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 29 Apr 2024 04:28:13 +0530 Subject: [PATCH 052/100] feat: show time in documents table --- apps/web/src/app/(dashboard)/documents/data-table.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index fa02a1ae2..d86e2940d 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -3,6 +3,7 @@ import { useTransition } from 'react'; import { Loader } from 'lucide-react'; +import { DateTime } from 'luxon'; import { useSession } from 'next-auth/react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; @@ -62,7 +63,9 @@ export const DocumentsDataTable = ({ { header: 'Created', accessorKey: 'createdAt', - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { header: 'Title', From 345e42537acad672dc36ef4fafba012db23c38c3 Mon Sep 17 00:00:00 2001 From: Mythie Date: Mon, 29 Apr 2024 12:42:22 +1000 Subject: [PATCH 053/100] fix: include all document meta when using the public api --- packages/api/v1/implementation.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 8ee0350bd..253803fc8 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -229,6 +229,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { requestMetadata: extractNextApiRequestMetadata(args.req), }); + await upsertDocumentMeta({ + documentId: document.id, + userId: user.id, + ...body.meta, + requestMetadata: extractNextApiRequestMetadata(args.req), + }); + const recipients = await setRecipientsForDocument({ userId: user.id, teamId: team?.id, @@ -324,10 +331,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { await upsertDocumentMeta({ documentId: document.id, userId: user.id, - subject: body.meta.subject, - message: body.meta.message, - dateFormat: body.meta.dateFormat, - timezone: body.meta.timezone, + ...body.meta, requestMetadata: extractNextApiRequestMetadata(args.req), }); } From e82e4025400660e860c9eb090b8942ad015b0777 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 29 Apr 2024 17:10:56 +0530 Subject: [PATCH 054/100] feat: remove the existing empty signer if its the only one --- .../primitives/document-flow/add-signers.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 2f9f2f234..b83e5064c 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -138,16 +138,6 @@ export const AddSignersFormPartial = ({ ); }; - const onAddSelfSigner = () => { - appendSigner({ - formId: nanoid(12), - name: user?.name ?? '', - email: user?.email ?? '', - role: RecipientRole.SIGNER, - actionAuth: undefined, - }); - }; - const onAddSigner = () => { appendSigner({ formId: nanoid(12), @@ -174,6 +164,17 @@ export const AddSignersFormPartial = ({ removeSigner(index); }; + const onAddSelfSigner = () => { + if(signers.length !== 0) onRemoveSigner(0); + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + actionAuth: undefined, + }); + }; + const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && event.target instanceof HTMLInputElement) { onAddSigner(); @@ -302,10 +303,10 @@ export const AddSignersFormPartial = ({ global action signing authentication method configured in the "General Settings" step
  • - {/*
  • +
  • Require account - The recipient must be signed in -
  • */} +
  • Require passkey - The recipient must have an account and passkey configured via their settings @@ -326,13 +327,11 @@ export const AddSignersFormPartial = ({ {/* Note: -1 is remapped in the Zod schema to the required value. */} Inherit authentication method - {Object.values(RecipientActionAuth) - .filter((auth) => auth !== RecipientActionAuth.ACCOUNT) - .map((authType) => ( - - {DOCUMENT_AUTH_TYPES[authType].value} - - ))} + {Object.values(RecipientActionAuth).map((authType) => ( + + {DOCUMENT_AUTH_TYPES[authType].value} + + ))} From 917c83fc5f68a8c861a6bf3ee2449e3cd15a7593 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 29 Apr 2024 17:30:01 +0530 Subject: [PATCH 055/100] chore: refactor removal logic --- packages/ui/primitives/document-flow/add-signers.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index b83e5064c..b62d4c433 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -116,6 +116,7 @@ export const AddSignersFormPartial = ({ const onFormSubmit = form.handleSubmit(onSubmit); + const { append: appendSigner, fields: signers, @@ -124,7 +125,7 @@ export const AddSignersFormPartial = ({ control, name: 'signers', }); - + const hasBeenSentToRecipientId = (id?: number) => { if (!id) { return false; @@ -165,7 +166,10 @@ export const AddSignersFormPartial = ({ }; const onAddSelfSigner = () => { - if(signers.length !== 0) onRemoveSigner(0); + const lastSignerIndex = signers.length - 1; + if(!signers[lastSignerIndex].name || !signers[lastSignerIndex].email){ + onRemoveSigner(lastSignerIndex) + } appendSigner({ formId: nanoid(12), name: user?.name ?? '', From 6b5750c7bf8e8af051872bd49068381b11b8b3de Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 29 Apr 2024 17:48:00 +0530 Subject: [PATCH 056/100] chore: revert previous changes --- .../primitives/document-flow/add-signers.tsx | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index b62d4c433..2f9f2f234 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -116,7 +116,6 @@ export const AddSignersFormPartial = ({ const onFormSubmit = form.handleSubmit(onSubmit); - const { append: appendSigner, fields: signers, @@ -125,7 +124,7 @@ export const AddSignersFormPartial = ({ control, name: 'signers', }); - + const hasBeenSentToRecipientId = (id?: number) => { if (!id) { return false; @@ -139,6 +138,16 @@ export const AddSignersFormPartial = ({ ); }; + const onAddSelfSigner = () => { + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + actionAuth: undefined, + }); + }; + const onAddSigner = () => { appendSigner({ formId: nanoid(12), @@ -165,20 +174,6 @@ export const AddSignersFormPartial = ({ removeSigner(index); }; - const onAddSelfSigner = () => { - const lastSignerIndex = signers.length - 1; - if(!signers[lastSignerIndex].name || !signers[lastSignerIndex].email){ - onRemoveSigner(lastSignerIndex) - } - appendSigner({ - formId: nanoid(12), - name: user?.name ?? '', - email: user?.email ?? '', - role: RecipientRole.SIGNER, - actionAuth: undefined, - }); - }; - const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && event.target instanceof HTMLInputElement) { onAddSigner(); @@ -307,10 +302,10 @@ export const AddSignersFormPartial = ({ global action signing authentication method configured in the "General Settings" step
  • -
  • + {/*
  • Require account - The recipient must be signed in -
  • + */}
  • Require passkey - The recipient must have an account and passkey configured via their settings @@ -331,11 +326,13 @@ export const AddSignersFormPartial = ({ {/* Note: -1 is remapped in the Zod schema to the required value. */} Inherit authentication method - {Object.values(RecipientActionAuth).map((authType) => ( - - {DOCUMENT_AUTH_TYPES[authType].value} - - ))} + {Object.values(RecipientActionAuth) + .filter((auth) => auth !== RecipientActionAuth.ACCOUNT) + .map((authType) => ( + + {DOCUMENT_AUTH_TYPES[authType].value} + + ))} From bde0f5893fa0f5a1b0f338cb978cbddb85c6b4eb Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 29 Apr 2024 17:49:50 +0530 Subject: [PATCH 057/100] feat: update add self signer logic --- .../primitives/document-flow/add-signers.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 2f9f2f234..3227cdeb6 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -138,16 +138,6 @@ export const AddSignersFormPartial = ({ ); }; - const onAddSelfSigner = () => { - appendSigner({ - formId: nanoid(12), - name: user?.name ?? '', - email: user?.email ?? '', - role: RecipientRole.SIGNER, - actionAuth: undefined, - }); - }; - const onAddSigner = () => { appendSigner({ formId: nanoid(12), @@ -174,6 +164,20 @@ export const AddSignersFormPartial = ({ removeSigner(index); }; + const onAddSelfSigner = () => { + const lastSignerIndex = signers.length - 1; + if(!signers[lastSignerIndex].name || !signers[lastSignerIndex].email){ + onRemoveSigner(lastSignerIndex) + } + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + actionAuth: undefined, + }); + }; + const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && event.target instanceof HTMLInputElement) { onAddSigner(); From 97d334a1da63b8e0284d2697f914631fa64671eb Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 29 Apr 2024 20:15:40 +0700 Subject: [PATCH 058/100] fix: force users to have a Stripe customer on sign in --- apps/web/src/pages/api/auth/[...nextauth].ts | 32 ++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts index 365b6ec40..5217f4f8b 100644 --- a/apps/web/src/pages/api/auth/[...nextauth].ts +++ b/apps/web/src/pages/api/auth/[...nextauth].ts @@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import NextAuth from 'next-auth'; +import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; +import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { prisma } from '@documenso/prisma'; @@ -18,15 +20,27 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { error: '/signin', }, events: { - signIn: async ({ user }) => { - await prisma.userSecurityAuditLog.create({ - data: { - userId: user.id, - ipAddress, - userAgent, - type: UserSecurityAuditLogType.SIGN_IN, - }, - }); + signIn: async ({ user: { id: userId } }) => { + const [user] = await Promise.all([ + await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }), + await prisma.userSecurityAuditLog.create({ + data: { + userId: userId, + ipAddress, + userAgent, + type: UserSecurityAuditLogType.SIGN_IN, + }, + }), + ]); + + // Create the Stripe customer and attach it to the user if it doesn't exist. + if (user.customerId === null && IS_BILLING_ENABLED()) { + await getStripeCustomerByUser(user); + } }, signOut: async ({ token }) => { const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id; From db9e605031ed5a57e5c86f28d5845860fbf15707 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 30 Apr 2024 01:32:58 +0530 Subject: [PATCH 059/100] chore: fix lint issues Signed-off-by: Adithya Krishna --- packages/ui/primitives/document-flow/add-signers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 3227cdeb6..2b3a9592d 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -166,8 +166,8 @@ export const AddSignersFormPartial = ({ const onAddSelfSigner = () => { const lastSignerIndex = signers.length - 1; - if(!signers[lastSignerIndex].name || !signers[lastSignerIndex].email){ - onRemoveSigner(lastSignerIndex) + if (!signers[lastSignerIndex].name || !signers[lastSignerIndex].email) { + onRemoveSigner(lastSignerIndex); } appendSigner({ formId: nanoid(12), From 0e16a86e74702986fe4e3146a775db9293617795 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 30 Apr 2024 11:55:01 +0530 Subject: [PATCH 060/100] chore: updated dark mode text --- .../(unauthenticated)/articles/signature-disclosure/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx b/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx index 878332f35..c56f53702 100644 --- a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx +++ b/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx @@ -5,7 +5,7 @@ import { Button } from '@documenso/ui/primitives/button'; export default function SignatureDisclosure() { return (
    -
    +

    Electronic Signature Disclosure

    Welcome

    From 6df525b670f0b6e571b22ab83fa2cc1110890da1 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 30 Apr 2024 12:05:42 +0530 Subject: [PATCH 061/100] feat: updated signer logic Signed-off-by: Adithya Krishna --- .../primitives/document-flow/add-signers.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 2b3a9592d..2203b7e6d 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -110,6 +110,8 @@ export const AddSignersFormPartial = ({ const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings); const { + setValue, + getValues, formState: { errors, isSubmitting }, control, } = form; @@ -164,18 +166,29 @@ export const AddSignersFormPartial = ({ removeSigner(index); }; + const emptySignerIndex = signers.findIndex( + (signer) => + !getValues(`signers.${signers.indexOf(signer)}.name`) || + !getValues(`signers.${signers.indexOf(signer)}.email`), + ); + const onAddSelfSigner = () => { const lastSignerIndex = signers.length - 1; - if (!signers[lastSignerIndex].name || !signers[lastSignerIndex].email) { + if (!signers[lastSignerIndex].name && !signers[lastSignerIndex].email) { onRemoveSigner(lastSignerIndex); } - appendSigner({ - formId: nanoid(12), - name: user?.name ?? '', - email: user?.email ?? '', - role: RecipientRole.SIGNER, - actionAuth: undefined, - }); + if (emptySignerIndex !== -1) { + setValue(`signers.${emptySignerIndex}.name`, user?.name ?? ''); + setValue(`signers.${emptySignerIndex}.email`, user?.email ?? ''); + } else { + appendSigner({ + formId: nanoid(12), + name: user?.name ?? '', + email: user?.email ?? '', + role: RecipientRole.SIGNER, + actionAuth: undefined, + }); + } }; const onKeyDown = (event: React.KeyboardEvent) => { From 8622e688534ffc374aee85715625a12262a38c20 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 30 Apr 2024 15:50:22 +0700 Subject: [PATCH 062/100] fix: add logging --- apps/web/src/pages/api/auth/[...nextauth].ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts index 5217f4f8b..04f30ef45 100644 --- a/apps/web/src/pages/api/auth/[...nextauth].ts +++ b/apps/web/src/pages/api/auth/[...nextauth].ts @@ -39,7 +39,9 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { // Create the Stripe customer and attach it to the user if it doesn't exist. if (user.customerId === null && IS_BILLING_ENABLED()) { - await getStripeCustomerByUser(user); + await getStripeCustomerByUser(user).catch((err) => { + console.error(err); + }); } }, signOut: async ({ token }) => { From cfec366c1af9db7e602f0d1e4e9f1df99d47a0b9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 30 Apr 2024 15:54:24 +0700 Subject: [PATCH 063/100] fix: refactor --- apps/web/src/pages/api/auth/[...nextauth].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts index 04f30ef45..31f6e9ea3 100644 --- a/apps/web/src/pages/api/auth/[...nextauth].ts +++ b/apps/web/src/pages/api/auth/[...nextauth].ts @@ -29,7 +29,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { }), await prisma.userSecurityAuditLog.create({ data: { - userId: userId, + userId, ipAddress, userAgent, type: UserSecurityAuditLogType.SIGN_IN, From 6974a76ed48611fd9d202148283006c0e6f45d81 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 30 Apr 2024 18:47:49 +0530 Subject: [PATCH 064/100] chore: fix button styling --- .../template-flow/add-template-placeholder-recipients.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index e415f1aac..d285fbe44 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -282,6 +282,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ + -
    - - -
    -
    - ))} -
- - - - - - - - + + + + + ); diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index b1e069e35..ee8bf5996 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -19,7 +19,7 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient'; -import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; +import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -286,7 +286,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`; - const document = await createDocumentFromTemplate({ + const document = await createDocumentFromTemplateLegacy({ templateId, userId: user.id, teamId: team?.id, diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index a298d1e38..7d75c4f65 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -189,7 +189,14 @@ test('[TEMPLATES]: use template', async ({ page }) => { // Use personal template. await page.getByRole('button', { name: 'Use Template' }).click(); - await page.getByRole('button', { name: 'Create Document' }).click(); + + // Enter template values. + await page.getByPlaceholder('recipient.1@documenso.com').click(); + await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email); + await page.getByPlaceholder('Recipient 1').click(); + await page.getByPlaceholder('Recipient 1').fill('name'); + + await page.getByRole('button', { name: 'Create as draft' }).click(); await page.waitForURL(/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL('/documents'); @@ -200,7 +207,14 @@ test('[TEMPLATES]: use template', async ({ page }) => { // Use team template. await page.getByRole('button', { name: 'Use Template' }).click(); - await page.getByRole('button', { name: 'Create Document' }).click(); + + // Enter template values. + await page.getByPlaceholder('recipient.1@documenso.com').click(); + await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email); + await page.getByPlaceholder('Recipient 1').click(); + await page.getByPlaceholder('Recipient 1').fill('name'); + + await page.getByRole('button', { name: 'Create as draft' }).click(); await page.waitForURL(/\/t\/.+\/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL(`/t/${team.url}/documents`); diff --git a/packages/lib/constants/template.ts b/packages/lib/constants/template.ts new file mode 100644 index 000000000..80dee97cf --- /dev/null +++ b/packages/lib/constants/template.ts @@ -0,0 +1 @@ +export const TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i; diff --git a/packages/lib/errors/app-error.ts b/packages/lib/errors/app-error.ts index 120df5ed6..b48e45d54 100644 --- a/packages/lib/errors/app-error.ts +++ b/packages/lib/errors/app-error.ts @@ -1,4 +1,5 @@ import { TRPCError } from '@trpc/server'; +import { match } from 'ts-pattern'; import { z } from 'zod'; import { TRPCClientError } from '@documenso/trpc/client'; @@ -149,4 +150,24 @@ export class AppError extends Error { return null; } } + + static toRestAPIError(err: unknown): { + status: 400 | 401 | 404 | 500; + body: { message: string }; + } { + const error = AppError.parseError(err); + + const status = match(error.code) + .with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => 400 as const) + .with(AppErrorCode.UNAUTHORIZED, () => 401 as const) + .with(AppErrorCode.NOT_FOUND, () => 404 as const) + .otherwise(() => 500 as const); + + return { + status, + body: { + message: status !== 500 ? error.message : 'Something went wrong', + }, + }; + } } diff --git a/packages/lib/server-only/template/create-document-from-template-legacy.ts b/packages/lib/server-only/template/create-document-from-template-legacy.ts new file mode 100644 index 000000000..fadbae4c3 --- /dev/null +++ b/packages/lib/server-only/template/create-document-from-template-legacy.ts @@ -0,0 +1,144 @@ +import { nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import type { RecipientRole } from '@documenso/prisma/client'; + +export type CreateDocumentFromTemplateLegacyOptions = { + templateId: number; + userId: number; + teamId?: number; + recipients?: { + name?: string; + email: string; + role?: RecipientRole; + }[]; +}; + +/** + * Legacy server function for /api/v1 + */ +export const createDocumentFromTemplateLegacy = async ({ + templateId, + userId, + teamId, + recipients, +}: CreateDocumentFromTemplateLegacyOptions) => { + const template = await prisma.template.findUnique({ + where: { + id: templateId, + ...(teamId + ? { + team: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + } + : { + userId, + teamId: null, + }), + }, + include: { + Recipient: true, + Field: true, + templateDocumentData: true, + }, + }); + + if (!template) { + throw new Error('Template not found.'); + } + + const documentData = await prisma.documentData.create({ + data: { + type: template.templateDocumentData.type, + data: template.templateDocumentData.data, + initialData: template.templateDocumentData.initialData, + }, + }); + + const document = await prisma.document.create({ + data: { + userId, + teamId: template.teamId, + title: template.title, + documentDataId: documentData.id, + Recipient: { + create: template.Recipient.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + role: recipient.role, + token: nanoid(), + })), + }, + }, + + include: { + Recipient: { + orderBy: { + id: 'asc', + }, + }, + documentData: true, + }, + }); + + await prisma.field.createMany({ + data: template.Field.map((field) => { + const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId); + + const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email); + + if (!documentRecipient) { + throw new Error('Recipient not found.'); + } + + return { + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: field.customText, + inserted: field.inserted, + documentId: document.id, + recipientId: documentRecipient.id, + }; + }), + }); + + if (recipients && recipients.length > 0) { + document.Recipient = await Promise.all( + recipients.map(async (recipient, index) => { + const existingRecipient = document.Recipient.at(index); + + return await prisma.recipient.upsert({ + where: { + documentId_email: { + documentId: document.id, + email: existingRecipient?.email ?? recipient.email, + }, + }, + update: { + name: recipient.name, + email: recipient.email, + role: recipient.role, + }, + create: { + documentId: document.id, + email: recipient.email, + name: recipient.name, + role: recipient.role, + token: nanoid(), + }, + }); + }), + ); + } + + return document; +}; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 79a3f6f25..7cd098d6d 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,16 +1,29 @@ import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; -import type { RecipientRole } from '@documenso/prisma/client'; +import type { Field } from '@documenso/prisma/client'; +import { type Recipient, WebhookTriggerEvents } from '@documenso/prisma/client'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; +import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; +import type { RequestMetadata } from '../../universal/extract-request-metadata'; +import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; +import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; + +type FinalRecipient = Pick & { + templateRecipientId: number; + fields: Field[]; +}; export type CreateDocumentFromTemplateOptions = { templateId: number; userId: number; teamId?: number; - recipients?: { + recipients: { + id: number; name?: string; email: string; - role?: RecipientRole; }[]; + requestMetadata?: RequestMetadata; }; export const createDocumentFromTemplate = async ({ @@ -18,7 +31,14 @@ export const createDocumentFromTemplate = async ({ userId, teamId, recipients, + requestMetadata, }: CreateDocumentFromTemplateOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + const template = await prisma.template.findUnique({ where: { id: templateId, @@ -39,16 +59,42 @@ export const createDocumentFromTemplate = async ({ }), }, include: { - Recipient: true, - Field: true, + Recipient: { + include: { + Field: true, + }, + }, templateDocumentData: true, }, }); if (!template) { - throw new Error('Template not found.'); + throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found'); } + if (recipients.length !== template.Recipient.length) { + throw new AppError(AppErrorCode.INVALID_BODY, 'Invalid number of recipients.'); + } + + const finalRecipients: FinalRecipient[] = template.Recipient.map((templateRecipient) => { + const foundRecipient = recipients.find((recipient) => recipient.id === templateRecipient.id); + + if (!foundRecipient) { + throw new AppError( + AppErrorCode.INVALID_BODY, + `Missing template recipient with ID ${templateRecipient.id}`, + ); + } + + return { + templateRecipientId: templateRecipient.id, + fields: templateRecipient.Field, + name: foundRecipient.name ?? '', + email: foundRecipient.email, + role: templateRecipient.role, + }; + }); + const documentData = await prisma.documentData.create({ data: { type: template.templateDocumentData.type, @@ -57,85 +103,82 @@ export const createDocumentFromTemplate = async ({ }, }); - const document = await prisma.document.create({ - data: { - userId, - teamId: template.teamId, - title: template.title, - documentDataId: documentData.id, - Recipient: { - create: template.Recipient.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - token: nanoid(), - })), - }, - }, - - include: { - Recipient: { - orderBy: { - id: 'asc', + return await prisma.$transaction(async (tx) => { + const document = await tx.document.create({ + data: { + userId, + teamId: template.teamId, + title: template.title, + documentDataId: documentData.id, + Recipient: { + createMany: { + data: finalRecipients.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + role: recipient.role, + token: nanoid(), + })), + }, }, }, - documentData: true, - }, - }); + include: { + Recipient: { + orderBy: { + id: 'asc', + }, + }, + documentData: true, + }, + }); - await prisma.field.createMany({ - data: template.Field.map((field) => { - const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId); + let fieldsToCreate: Omit[] = []; - const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email); + Object.values(finalRecipients).forEach(({ email, fields }) => { + const recipient = document.Recipient.find((recipient) => recipient.email === email); - if (!documentRecipient) { + if (!recipient) { throw new Error('Recipient not found.'); } - return { - type: field.type, - page: field.page, - positionX: field.positionX, - positionY: field.positionY, - width: field.width, - height: field.height, - customText: field.customText, - inserted: field.inserted, + fieldsToCreate = fieldsToCreate.concat( + fields.map((field) => ({ + documentId: document.id, + recipientId: recipient.id, + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: '', + inserted: false, + })), + ); + }); + + await tx.field.createMany({ + data: fieldsToCreate, + }); + + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, documentId: document.id, - recipientId: documentRecipient.id, - }; - }), - }); - - if (recipients && recipients.length > 0) { - document.Recipient = await Promise.all( - recipients.map(async (recipient, index) => { - const existingRecipient = document.Recipient.at(index); - - return await prisma.recipient.upsert({ - where: { - documentId_email: { - documentId: document.id, - email: existingRecipient?.email ?? recipient.email, - }, - }, - update: { - name: recipient.name, - email: recipient.email, - role: recipient.role, - }, - create: { - documentId: document.id, - email: recipient.email, - name: recipient.name, - role: recipient.role, - token: nanoid(), - }, - }); + user, + requestMetadata, + data: { + title: document.title, + }, }), - ); - } + }); - return document; + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_CREATED, + data: document, + userId, + teamId, + }); + + return document; + }); }; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 4ed567b2b..3cca69548 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -1,10 +1,14 @@ import { TRPCError } from '@trpc/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; +import { AppError } from '@documenso/lib/errors/app-error'; +import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createTemplate } from '@documenso/lib/server-only/template/create-template'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template'; +import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import type { Document } from '@documenso/prisma/client'; import { authenticatedProcedure, router } from '../trpc'; import { @@ -49,19 +53,34 @@ export const templateRouter = router({ throw new Error('You have reached your document limit.'); } - return await createDocumentFromTemplate({ + const requestMetadata = extractNextApiRequestMetadata(ctx.req); + + let document: Document = await createDocumentFromTemplate({ templateId, teamId, userId: ctx.user.id, recipients: input.recipients, + requestMetadata, }); + + if (input.sendDocument) { + document = await sendDocument({ + documentId: document.id, + userId: ctx.user.id, + teamId, + requestMetadata, + }).catch((err) => { + console.error(err); + + throw new AppError('DOCUMENT_SEND_FAILED'); + }); + } + + return document; } catch (err) { console.error(err); - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'We were unable to create this document. Please try again later.', - }); + throw AppError.parseErrorToTRPCError(err); } }), diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 3f16d7b39..ce1489ac3 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,7 +1,5 @@ import { z } from 'zod'; -import { RecipientRole } from '@documenso/prisma/client'; - export const ZCreateTemplateMutationSchema = z.object({ title: z.string().min(1).trim(), teamId: z.number().optional(), @@ -14,12 +12,16 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ recipients: z .array( z.object({ + id: z.number(), email: z.string().email(), - name: z.string(), - role: z.nativeEnum(RecipientRole), + name: z.string().optional(), }), ) - .optional(), + .refine((recipients) => { + const emails = recipients.map((signer) => signer.email); + return new Set(emails).size === emails.length; + }, 'Recipients must have unique emails'), + sendDocument: z.boolean().optional(), }); export const ZDuplicateTemplateMutationSchema = z.object({ diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index d285fbe44..cd48158c4 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -103,6 +103,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ appendSigner({ formId: nanoid(12), name: `Recipient ${placeholderRecipientCount}`, + // Update TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX if this is ever changed. email: `recipient.${placeholderRecipientCount}@documenso.com`, role: RecipientRole.SIGNER, }); From e50ccca766c4b1f48bf9fbace78c7482d353bd95 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 7 May 2024 17:22:24 +0700 Subject: [PATCH 073/100] fix: allow template recipients to be filled (#1148) ## Description Update the template flow to allow for entering recipient placeholder emails and names ## Changes Made - General refactoring - Added advanced recipient settings for future usage --- .../templates/[id]/edit-template.tsx | 2 + .../templates/[id]/template-page-view.tsx | 2 +- .../templates/use-template-dialog.tsx | 39 +- packages/lib/constants/template.ts | 3 +- .../recipient-action-auth-select.tsx | 80 ++++ .../recipient/recipient-role-select.tsx | 97 +++++ .../primitives/document-flow/add-signers.tsx | 166 +------- .../add-template-placeholder-recipients.tsx | 373 +++++++++--------- ...d-template-placeholder-recipients.types.ts | 6 + 9 files changed, 418 insertions(+), 350 deletions(-) create mode 100644 packages/ui/components/recipient/recipient-action-auth-select.tsx create mode 100644 packages/ui/components/recipient/recipient-role-select.tsx diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx index f8c7f9a43..d9da6c27c 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -141,6 +141,8 @@ export const EditTemplateForm = ({ recipients={recipients} fields={fields} onSubmit={onAddTemplatePlaceholderFormSubmit} + // Todo: Add when we setup template settings. + isTemplateOwnerEnterprise={false} /> ({ @@ -98,20 +105,18 @@ export function UseTemplateDialog({ defaultValues: { sendDocument: false, recipients: recipients.map((recipient) => { - const isRecipientPlaceholder = recipient.email.match(TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX); + const isRecipientEmailPlaceholder = recipient.email.match( + TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX, + ); - if (isRecipientPlaceholder) { - return { - id: recipient.id, - name: '', - email: '', - }; - } + const isRecipientNamePlaceholder = recipient.name.match( + TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX, + ); return { id: recipient.id, - name: recipient.name, - email: recipient.email, + name: !isRecipientNamePlaceholder ? recipient.name : '', + email: !isRecipientEmailPlaceholder ? recipient.email : '', }; }), }, @@ -158,8 +163,14 @@ export function UseTemplateDialog({ name: 'recipients', }); + useEffect(() => { + if (!open) { + form.reset(); + } + }, [open, form]); + return ( - + !form.formState.isSubmitting && setOpen(value)}> -
+ + ))} + -
- - -
- - ))} - - + - +
+ -
- - -
+ +
+ + {!alwaysShowAdvancedSettings && isTemplateOwnerEnterprise && ( +
+ setShowAdvancedSettings(Boolean(value))} + /> + + +
+ )} + + diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts index d2ffc090b..18df2d33b 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts @@ -1,5 +1,8 @@ import { z } from 'zod'; +import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth'; + +import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types'; import { RecipientRole } from '.prisma/client'; export const ZAddTemplatePlacholderRecipientsFormSchema = z @@ -11,6 +14,9 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z email: z.string().min(1).email(), name: z.string(), role: z.nativeEnum(RecipientRole), + actionAuth: ZMapNegativeOneToUndefinedSchema.pipe( + ZRecipientActionAuthTypesSchema.optional(), + ), }), ), }) From 5d5d0210fa22fdc16f4c1869d7de6139a225fe33 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 8 May 2024 10:22:26 +0530 Subject: [PATCH 074/100] chore: update github actions (#1085) **Description:** This PR updates and adds a new action to assign `status: assigned` label --------- Signed-off-by: Adithya Krishna --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/e2e-tests.yml | 2 +- .github/workflows/issue-assignee-check.yml | 2 +- .github/workflows/issue-labeler.yml | 25 ++++++++++++++++++++++ .github/workflows/pr-review-reminder.yml | 4 ++-- .github/workflows/stale.yml | 2 +- 7 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/issue-labeler.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bebca8e85..6101b0180 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 314dc7b7b..b948e560d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,9 +33,9 @@ jobs: - uses: ./.github/actions/cache-build - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 12a7d9521..22705c2d6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -33,7 +33,7 @@ jobs: - name: Run Playwright tests run: npm run ci - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: test-results diff --git a/.github/workflows/issue-assignee-check.yml b/.github/workflows/issue-assignee-check.yml index dbd321509..b601a8dc3 100644 --- a/.github/workflows/issue-assignee-check.yml +++ b/.github/workflows/issue-assignee-check.yml @@ -27,7 +27,7 @@ jobs: - name: Check Assigned User's Issue Count id: parse-comment - uses: actions/github-script@v5 + uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml new file mode 100644 index 000000000..34d7a478f --- /dev/null +++ b/.github/workflows/issue-labeler.yml @@ -0,0 +1,25 @@ +name: Auto Label Assigned Issues + +on: + issues: + types: [assigned] + +jobs: + label-when-assigned: + runs-on: ubuntu-latest + steps: + - name: Label issue + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const issue = context.issue; + // To run only on issues and not on PR + if (github.context.payload.issue.pull_request === undefined) { + const labelResponse = await github.rest.issues.addLabels({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + labels: ['status: assigned'] + }); + } diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index 78f927e61..c81d9a34e 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -2,14 +2,14 @@ name: 'PR Review Reminder' on: pull_request: - types: ['opened', 'reopened', 'ready_for_review', 'review_requested'] + types: ['opened', 'ready_for_review'] permissions: pull-requests: write jobs: checkPRs: - if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'reopened' || 'ready_for_review' || 'review_requested') + if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review') runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3e829d24b..a18e33f87 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v4 + - uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-pr-stale: 90 From 2ba0f48c6186af435aa8948c9a00a89b7013e0da Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 8 May 2024 08:03:21 +0300 Subject: [PATCH 075/100] fix: unauthorized access error api tokens page team (#1134) --- .../t/[teamUrl]/settings/tokens/page.tsx | 22 ++++++++++++++++++- .../public-api/get-all-team-tokens.ts | 8 ++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx index eedae29d1..7602ac70f 100644 --- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx +++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx @@ -1,7 +1,10 @@ import { DateTime } from 'luxon'; +import { match } from 'ts-pattern'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import type { GetTeamTokensResponse } from '@documenso/lib/server-only/public-api/get-all-team-tokens'; import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens'; import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { Button } from '@documenso/ui/primitives/button'; @@ -23,7 +26,24 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) { const team = await getTeamByUrl({ userId: user.id, teamUrl }); - const tokens = await getTeamTokens({ userId: user.id, teamId: team.id }); + let tokens: GetTeamTokensResponse | null = null; + + try { + tokens = await getTeamTokens({ userId: user.id, teamId: team.id }); + } catch (err) { + const error = AppError.parseError(err); + + return ( +
+

API Tokens

+

+ {match(error.code) + .with(AppErrorCode.UNAUTHORIZED, () => error.message) + .otherwise(() => 'Something went wrong.')} +

+
+ ); + } return (
diff --git a/packages/lib/server-only/public-api/get-all-team-tokens.ts b/packages/lib/server-only/public-api/get-all-team-tokens.ts index 86c13ed1d..35285336b 100644 --- a/packages/lib/server-only/public-api/get-all-team-tokens.ts +++ b/packages/lib/server-only/public-api/get-all-team-tokens.ts @@ -1,3 +1,4 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; import { TeamMemberRole } from '@documenso/prisma/client'; @@ -6,6 +7,8 @@ export type GetUserTokensOptions = { teamId: number; }; +export type GetTeamTokensResponse = Awaited>; + export const getTeamTokens = async ({ userId, teamId }: GetUserTokensOptions) => { const teamMember = await prisma.teamMember.findFirst({ where: { @@ -15,7 +18,10 @@ export const getTeamTokens = async ({ userId, teamId }: GetUserTokensOptions) => }); if (teamMember?.role !== TeamMemberRole.ADMIN) { - throw new Error('You do not have permission to view tokens for this team'); + throw new AppError( + AppErrorCode.UNAUTHORIZED, + 'You do not have the required permissions to view this page.', + ); } return await prisma.apiToken.findMany({ From 98672560ca0db215e3816bc383061edc287e5feb Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 8 May 2024 17:02:01 +0530 Subject: [PATCH 076/100] chore: update self signer logic Signed-off-by: Adithya Krishna --- packages/ui/primitives/document-flow/add-signers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 682a3fe7a..cfee325e1 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -169,7 +169,7 @@ export const AddSignersFormPartial = ({ const onAddSelfSigner = () => { const lastSignerIndex = signers.length - 1; - if (!signers[lastSignerIndex].name && !signers[lastSignerIndex].email) { + if (signers[lastSignerIndex].email || signers[lastSignerIndex].name) { onRemoveSigner(lastSignerIndex); } if (emptySignerIndex !== -1) { @@ -268,7 +268,7 @@ export const AddSignersFormPartial = ({ disabled={ isSubmitting || hasBeenSentToRecipientId(signer.nativeId) || - signers[index].email === user?.email + signers[index].name === user?.name } onKeyDown={onKeyDown} /> From cc4efddabf8f20dffcc89d3cf8de33a8a1fdd38d Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 8 May 2024 17:03:57 +0530 Subject: [PATCH 077/100] chore: updated triage label --- .github/workflows/issue-opened.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index ed9f2811a..92b559d11 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -17,5 +17,5 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - labels: ["needs triage"] + labels: ["status: triage"] }) From bbcbc56e70f4683ffb1f784fa683eee780c428a9 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 8 May 2024 19:17:47 +0530 Subject: [PATCH 078/100] feat: 12h format --- apps/web/src/app/(dashboard)/documents/data-table.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index d86e2940d..c079e0165 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -64,7 +64,10 @@ export const DocumentsDataTable = ({ header: 'Created', accessorKey: 'createdAt', cell: ({ row }) => ( - + ), }, { From 2f86bb523b21f94ac4a7af657aba2ce98fbdca7b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 10 May 2024 19:45:19 +0700 Subject: [PATCH 079/100] feat: add template enhancements (#1154) ## Description General enhancements for templates. ## Changes Made Added the following changes to the template flow: - Allow adding document meta settings - Allow adding email settings - Allow adding document access & action authentication - Allow adding recipient action authentication - Save the state between template steps similar to how it works for documents Other changes: - Extract common fields between document and template flows - Remove the title field from "Use template" since we now have it as part of the template flow - Add new API endpoint for generating templates ## Testing Performed Added E2E tests for templates and creating documents from templates --- .../documents/[id]/edit-document.tsx | 1 + .../[id]/edit/document-edit-page-view.tsx | 10 +- .../templates/[id]/edit-template.tsx | 160 +++++++-- .../templates/[id]/template-page-view.tsx | 30 +- .../templates/new-template-dialog.tsx | 172 ++------- packages/api/v1/contract.ts | 20 ++ packages/api/v1/implementation.ts | 82 +++++ packages/api/v1/schema.ts | 54 +++ .../e2e/document-flow/settings-step.spec.ts | 21 +- .../e2e/document-flow/signers-step.spec.ts | 24 +- .../template-settings-step.spec.ts | 167 +++++++++ .../template-signers-step.spec.ts | 106 ++++++ .../create-document-from-template.spec.ts | 285 +++++++++++++++ packages/lib/schemas/common.ts | 12 + .../field/set-fields-for-template.ts | 39 ++- .../recipient/set-recipients-for-template.ts | 120 +++++-- .../template/create-document-from-template.ts | 89 ++++- .../get-template-with-details-by-id.ts | 38 ++ .../template/update-template-settings.ts | 139 ++++++++ .../migration.sql | 22 ++ packages/prisma/schema.prisma | 22 +- packages/prisma/seed/templates.ts | 27 ++ packages/prisma/types/template.ts | 19 + packages/trpc/server/admin-router/router.ts | 6 +- packages/trpc/server/field-router/router.ts | 2 +- .../trpc/server/recipient-router/router.ts | 4 +- .../trpc/server/recipient-router/schema.ts | 2 + .../trpc/server/template-router/router.ts | 52 +++ .../trpc/server/template-router/schema.ts | 36 +- .../document-global-auth-access-select.tsx | 66 ++++ .../document-global-auth-action-select.tsx | 80 +++++ .../document-send-email-message-helper.tsx | 34 ++ .../recipient/recipient-role-select.tsx | 152 ++++---- .../primitives/document-flow/add-settings.tsx | 113 +----- .../primitives/document-flow/add-subject.tsx | 28 +- .../add-template-placeholder-recipients.tsx | 18 +- .../template-flow/add-template-settings.tsx | 326 ++++++++++++++++++ .../add-template-settings.types.tsx | 35 ++ 38 files changed, 2103 insertions(+), 510 deletions(-) create mode 100644 packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts create mode 100644 packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts create mode 100644 packages/app-tests/e2e/templates/create-document-from-template.spec.ts create mode 100644 packages/lib/schemas/common.ts create mode 100644 packages/lib/server-only/template/get-template-with-details-by-id.ts create mode 100644 packages/lib/server-only/template/update-template-settings.ts create mode 100644 packages/prisma/migrations/20240508150017_add_template_settings/migration.sql create mode 100644 packages/prisma/types/template.ts create mode 100644 packages/ui/components/document/document-global-auth-access-select.tsx create mode 100644 packages/ui/components/document/document-global-auth-action-select.tsx create mode 100644 packages/ui/components/document/document-send-email-message-helper.tsx create mode 100644 packages/ui/primitives/template-flow/add-template-settings.tsx create mode 100644 packages/ui/primitives/template-flow/add-template-settings.types.tsx diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 2e2f0c889..1ad3d382b 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -332,6 +332,7 @@ export const EditDocumentForm = ({ isDocumentPdfLoaded={isDocumentPdfLoaded} onSubmit={onAddSettingsFormSubmit} /> + diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx index d9da6c27c..21be26129 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -1,10 +1,14 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import type { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client'; +import { + DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + SKIP_QUERY_BATCH_META, +} from '@documenso/lib/constants/trpc'; +import type { TemplateWithDetails } from '@documenso/prisma/types/template'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; @@ -19,52 +23,135 @@ import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template- import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types'; import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients'; import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types'; +import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-settings'; +import type { TAddTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types'; import { useToast } from '@documenso/ui/primitives/use-toast'; +import { useOptionalCurrentTeam } from '~/providers/team'; + export type EditTemplateFormProps = { className?: string; - user: User; - template: Template; - recipients: Recipient[]; - fields: Field[]; - documentData: DocumentData; + initialTemplate: TemplateWithDetails; + isEnterprise: boolean; templateRootPath: string; }; -type EditTemplateStep = 'signers' | 'fields'; -const EditTemplateSteps: EditTemplateStep[] = ['signers', 'fields']; +type EditTemplateStep = 'settings' | 'signers' | 'fields'; +const EditTemplateSteps: EditTemplateStep[] = ['settings', 'signers', 'fields']; export const EditTemplateForm = ({ + initialTemplate, className, - template, - recipients, - fields, - user: _user, - documentData, + isEnterprise, templateRootPath, }: EditTemplateFormProps) => { const { toast } = useToast(); const router = useRouter(); - const [step, setStep] = useState('signers'); + const team = useOptionalCurrentTeam(); + + const [step, setStep] = useState('settings'); + + const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false); + + const utils = trpc.useUtils(); + + const { data: template, refetch: refetchTemplate } = + trpc.template.getTemplateWithDetailsById.useQuery( + { + id: initialTemplate.id, + }, + { + initialData: initialTemplate, + ...SKIP_QUERY_BATCH_META, + }, + ); + + const { Recipient: recipients, Field: fields, templateDocumentData } = template; const documentFlow: Record = { + settings: { + title: 'General', + description: 'Configure general settings for the template.', + stepIndex: 1, + }, signers: { title: 'Add Placeholders', description: 'Add all relevant placeholders for each recipient.', - stepIndex: 1, + stepIndex: 2, }, fields: { title: 'Add Fields', description: 'Add all relevant fields for each recipient.', - stepIndex: 2, + stepIndex: 3, }, }; const currentDocumentFlow = documentFlow[step]; - const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation(); - const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation(); + const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplateSettings.useMutation({ + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + onSuccess: (newData) => { + utils.template.getTemplateWithDetailsById.setData( + { + id: initialTemplate.id, + }, + (oldData) => ({ ...(oldData || initialTemplate), ...newData }), + ); + }, + }); + + const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({ + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + onSuccess: (newData) => { + utils.template.getTemplateWithDetailsById.setData( + { + id: initialTemplate.id, + }, + (oldData) => ({ ...(oldData || initialTemplate), ...newData }), + ); + }, + }); + + const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({ + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + onSuccess: (newData) => { + utils.template.getTemplateWithDetailsById.setData( + { + id: initialTemplate.id, + }, + (oldData) => ({ ...(oldData || initialTemplate), ...newData }), + ); + }, + }); + + const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => { + try { + await updateTemplateSettings({ + templateId: template.id, + teamId: team?.id, + data: { + title: data.title, + globalAccessAuth: data.globalAccessAuth ?? null, + globalActionAuth: data.globalActionAuth ?? null, + }, + meta: data.meta, + }); + + // Router refresh is here to clear the router cache for when navigating to /documents. + router.refresh(); + + setStep('signers'); + } catch (err) { + console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while updating the document settings.', + variant: 'destructive', + }); + } + }; const onAddTemplatePlaceholderFormSubmit = async ( data: TAddTemplatePlacholderRecipientsFormSchema, @@ -72,9 +159,11 @@ export const EditTemplateForm = ({ try { await addTemplateSigners({ templateId: template.id, + teamId: team?.id, signers: data.signers, }); + // Router refresh is here to clear the router cache for when navigating to /documents. router.refresh(); setStep('fields'); @@ -100,6 +189,9 @@ export const EditTemplateForm = ({ duration: 5000, }); + // Router refresh is here to clear the router cache for when navigating to /documents. + router.refresh(); + router.push(templateRootPath); } catch (err) { toast({ @@ -110,6 +202,15 @@ export const EditTemplateForm = ({ } }; + /** + * Refresh the data in the background when steps change. + */ + useEffect(() => { + void refetchTemplate(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step]); + return (
- + setIsDocumentPdfLoaded(true)} + /> @@ -135,14 +240,25 @@ export const EditTemplateForm = ({ currentStep={currentDocumentFlow.stepIndex} setCurrentStep={(step) => setStep(EditTemplateSteps[step - 1])} > + + null); @@ -44,18 +43,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps) redirect(templateRootPath); } - const { templateDocumentData } = template; - - const [templateRecipients, templateFields] = await Promise.all([ - getRecipientsForTemplate({ - templateId, - userId: user.id, - }), - getFieldsForTemplate({ - templateId, - userId: user.id, - }), - ]); + const isTemplateEnterprise = await isUserEnterprise({ + userId: user.id, + teamId: team?.id, + }); return (
@@ -74,12 +65,9 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
); diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx index 1a6e34584..ec9cb5911 100644 --- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -1,21 +1,16 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { FilePlus, X } from 'lucide-react'; +import { FilePlus, Loader } from 'lucide-react'; import { useSession } from 'next-auth/react'; -import { useForm } from 'react-hook-form'; -import * as z from 'zod'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; -import { base64 } from '@documenso/lib/universal/base64'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; -import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Dialog, DialogClose, @@ -27,24 +22,8 @@ import { DialogTrigger, } from '@documenso/ui/primitives/dialog'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@documenso/ui/primitives/form/form'; -import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; -const ZCreateTemplateFormSchema = z.object({ - name: z.string(), -}); - -type TCreateTemplateFormSchema = z.infer; - type NewTemplateDialogProps = { teamId?: number; templateRootPath: string; @@ -56,50 +35,20 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo const { data: session } = useSession(); const { toast } = useToast(); - const form = useForm({ - defaultValues: { - name: '', - }, - resolver: zodResolver(ZCreateTemplateFormSchema), - }); - const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation(); const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false); - const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>(); + const [isUploadingFile, setIsUploadingFile] = useState(false); const onFileDrop = async (file: File) => { - try { - const arrayBuffer = await file.arrayBuffer(); - const base64String = base64.encode(new Uint8Array(arrayBuffer)); - - setUploadedFile({ - file, - fileBase64: `data:application/pdf;base64,${base64String}`, - }); - - if (!form.getValues('name')) { - form.setValue('name', file.name); - } - } catch { - toast({ - title: 'Something went wrong', - description: 'Please try again later.', - variant: 'destructive', - }); - } - }; - - const onSubmit = async (values: TCreateTemplateFormSchema) => { - if (!uploadedFile) { + if (isUploadingFile) { return; } - const file: File = uploadedFile.file; + setIsUploadingFile(true); try { const { type, data } = await putPdfFile(file); - const { id: templateDocumentDataId } = await createDocumentData({ type, data, @@ -107,7 +56,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo const { id } = await createTemplate({ teamId, - title: values.name ? values.name : file.name, + title: file.name, templateDocumentDataId, }); @@ -127,26 +76,16 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo description: 'Please try again later.', variant: 'destructive', }); + + setIsUploadingFile(false); } }; - const resetForm = () => { - if (form.getValues('name') === uploadedFile?.file.name) { - form.reset(); - } - - setUploadedFile(null); - }; - - useEffect(() => { - if (!showNewTemplateDialog) { - form.reset(); - setUploadedFile(null); - } - }, [form, showNewTemplateDialog]); - return ( - + !isUploadingFile && setShowNewTemplateDialog(value)} + > + {isUploadingFile && ( +
+ +
+ )} +
-
-
-
-
-
- -

- Uploaded Document -

- - - {uploadedFile.file.name} - - - - ) : ( - - )} -
- - - - - - - - - - - + + + + + ); diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts index ca2b6e2f5..577143ead 100644 --- a/packages/api/v1/contract.ts +++ b/packages/api/v1/contract.ts @@ -12,6 +12,8 @@ import { ZDeleteFieldMutationSchema, ZDeleteRecipientMutationSchema, ZDownloadDocumentSuccessfulSchema, + ZGenerateDocumentFromTemplateMutationResponseSchema, + ZGenerateDocumentFromTemplateMutationSchema, ZGetDocumentsQuerySchema, ZSendDocumentForSigningMutationSchema, ZSuccessfulDocumentResponseSchema, @@ -85,6 +87,24 @@ export const ApiContractV1 = c.router( 404: ZUnsuccessfulResponseSchema, }, summary: 'Create a new document from an existing template', + deprecated: true, + description: `This has been deprecated in favour of "/api/v1/templates/:templateId/generate-document". You may face unpredictable behavior using this endpoint as it is no longer maintained.`, + }, + + generateDocumentFromTemplate: { + method: 'POST', + path: '/api/v1/templates/:templateId/generate-document', + body: ZGenerateDocumentFromTemplateMutationSchema, + responses: { + 200: ZGenerateDocumentFromTemplateMutationResponseSchema, + 400: ZUnsuccessfulResponseSchema, + 401: ZUnsuccessfulResponseSchema, + 404: ZUnsuccessfulResponseSchema, + 500: ZUnsuccessfulResponseSchema, + }, + summary: 'Create a new document from an existing template', + description: + 'Create a new document from an existing template. Passing in values for title and meta will override the original values defined in the template. If you do not pass in values for recipients, it will use the values defined in the template.', }, sendDocument: { diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index ee8bf5996..7e729262e 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -1,6 +1,7 @@ import { createNextRoute } from '@ts-rest/next'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; +import { AppError } from '@documenso/lib/errors/app-error'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { createDocument } from '@documenso/lib/server-only/document/create-document'; @@ -19,6 +20,8 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient'; +import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template'; +import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; @@ -351,6 +354,85 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { }; }), + generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => { + const { body, params } = args; + + const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id }); + + if (remaining.documents <= 0) { + return { + status: 400, + body: { + message: 'You have reached the maximum number of documents allowed for this month', + }, + }; + } + + const templateId = Number(params.templateId); + + let document: CreateDocumentFromTemplateResponse | null = null; + + try { + document = await createDocumentFromTemplate({ + templateId, + userId: user.id, + teamId: team?.id, + recipients: body.recipients, + override: { + title: body.title, + ...body.meta, + }, + }); + } catch (err) { + return AppError.toRestAPIError(err); + } + + if (body.formValues) { + const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`; + + const pdf = await getFile(document.documentData); + + const prefilled = await insertFormValuesInPdf({ + pdf: Buffer.from(pdf), + formValues: body.formValues, + }); + + const newDocumentData = await putPdfFile({ + name: fileName, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(prefilled), + }); + + await updateDocument({ + documentId: document.id, + userId: user.id, + teamId: team?.id, + data: { + formValues: body.formValues, + documentData: { + connect: { + id: newDocumentData.id, + }, + }, + }, + }); + } + + return { + status: 200, + body: { + documentId: document.id, + recipients: document.Recipient.map((recipient) => ({ + recipientId: recipient.id, + name: recipient.name, + email: recipient.email, + token: recipient.token, + role: recipient.role, + })), + }, + }; + }), + sendDocument: authenticatedMiddleware(async (args, user, team) => { const { id } = args.params; diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index be0ea1271..f109df348 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; +import { ZUrlSchema } from '@documenso/lib/schemas/common'; import { FieldType, ReadStatus, @@ -141,6 +142,59 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer< typeof ZCreateDocumentFromTemplateMutationResponseSchema >; +export const ZGenerateDocumentFromTemplateMutationSchema = z.object({ + title: z.string().optional(), + recipients: z + .array( + z.object({ + id: z.number(), + name: z.string().optional(), + email: z.string().email().min(1), + }), + ) + .refine( + (schema) => { + const emails = schema.map((signer) => signer.email.toLowerCase()); + const ids = schema.map((signer) => signer.id); + + return new Set(emails).size === emails.length && new Set(ids).size === ids.length; + }, + { message: 'Recipient IDs and emails must be unique' }, + ), + meta: z + .object({ + subject: z.string(), + message: z.string(), + timezone: z.string(), + dateFormat: z.string(), + redirectUrl: ZUrlSchema, + }) + .partial() + .optional(), + formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), +}); + +export type TGenerateDocumentFromTemplateMutationSchema = z.infer< + typeof ZGenerateDocumentFromTemplateMutationSchema +>; + +export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({ + documentId: z.number(), + recipients: z.array( + z.object({ + recipientId: z.number(), + name: z.string(), + email: z.string().email().min(1), + token: z.string(), + role: z.nativeEnum(RecipientRole), + }), + ), +}); + +export type TGenerateDocumentFromTemplateMutationResponseSchema = z.infer< + typeof ZGenerateDocumentFromTemplateMutationResponseSchema +>; + export const ZCreateRecipientMutationSchema = z.object({ name: z.string().min(1), email: z.string().email().min(1), diff --git a/packages/app-tests/e2e/document-flow/settings-step.spec.ts b/packages/app-tests/e2e/document-flow/settings-step.spec.ts index b416baa7c..cef428a24 100644 --- a/packages/app-tests/e2e/document-flow/settings-step.spec.ts +++ b/packages/app-tests/e2e/document-flow/settings-step.spec.ts @@ -41,8 +41,8 @@ test.describe('[EE_ONLY]', () => { // Set EE action auth. await page.getByTestId('documentActionSelectValue').click(); - await page.getByLabel('Require account').getByText('Require account').click(); - await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account'); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); // Save the settings by going to the next step. await page.getByRole('button', { name: 'Continue' }).click(); @@ -52,11 +52,7 @@ test.describe('[EE_ONLY]', () => { await page.getByRole('button', { name: 'Go Back' }).click(); await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); - // Todo: Verify that the values are correct once we fix the issue where going back - // does not show the updated values. - // await expect(page.getByLabel('Title')).toContainText('New Title'); - // await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); - // await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account'); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); await unseedUser(user.id); }); @@ -89,8 +85,8 @@ test.describe('[EE_ONLY]', () => { // Set EE action auth. await page.getByTestId('documentActionSelectValue').click(); - await page.getByLabel('Require account').getByText('Require account').click(); - await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account'); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); // Save the settings by going to the next step. await page.getByRole('button', { name: 'Continue' }).click(); @@ -168,11 +164,8 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => { await page.getByRole('button', { name: 'Go Back' }).click(); await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); - // Todo: Verify that the values are correct once we fix the issue where going back - // does not show the updated values. - // await expect(page.getByLabel('Title')).toContainText('New Title'); - // await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); - // await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account'); + await expect(page.getByLabel('Title')).toHaveValue('New Title'); + await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); await unseedUser(user.id); }); diff --git a/packages/app-tests/e2e/document-flow/signers-step.spec.ts b/packages/app-tests/e2e/document-flow/signers-step.spec.ts index 8676d05ed..a832c69a6 100644 --- a/packages/app-tests/e2e/document-flow/signers-step.spec.ts +++ b/packages/app-tests/e2e/document-flow/signers-step.spec.ts @@ -48,7 +48,7 @@ test.describe('[EE_ONLY]', () => { await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); // Display advanced settings. - await page.getByLabel('Show advanced settings').click(); + await page.getByLabel('Show advanced settings').check(); // Navigate to the next step and back. await page.getByRole('button', { name: 'Continue' }).click(); @@ -62,7 +62,6 @@ test.describe('[EE_ONLY]', () => { }); }); -// Note: Not complete yet due to issue with back button. test('[DOCUMENT_FLOW]: add signers', async ({ page }) => { const user = await seedUser(); const document = await seedBlankDocument(user); @@ -93,26 +92,5 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => { await page.getByRole('button', { name: 'Go Back' }).click(); await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible(); - // Todo: Fix stepper component back issue before finishing test. - - // // Expect that the advanced settings is unchecked, since no advanced settings were applied. - // await expect(page.getByLabel('Show advanced settings')).toBeChecked({ checked: false }); - - // // Add advanced settings for a single recipient. - // await page.getByLabel('Show advanced settings').click(); - // await page.getByRole('combobox').first().click(); - // await page.getByLabel('Require account').click(); - - // // Navigate to the next step and back. - // await page.getByRole('button', { name: 'Continue' }).click(); - // await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible(); - // await page.getByRole('button', { name: 'Go Back' }).click(); - // await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible(); - - // Expect that the advanced settings is visible, and the checkbox is hidden. Since advanced - // settings were applied. - - // Todo: Fix stepper component back issue before finishing test. - await unseedUser(user.id); }); diff --git a/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts b/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts new file mode 100644 index 000000000..517a3f093 --- /dev/null +++ b/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts @@ -0,0 +1,167 @@ +import { expect, test } from '@playwright/test'; + +import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions'; +import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams'; +import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedUser, unseedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +test.describe('[EE_ONLY]', () => { + const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || ''; + + test.beforeEach(() => { + test.skip( + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId, + 'Billing required for this test', + ); + }); + + test('[TEMPLATE_FLOW] add action auth settings', async ({ page }) => { + const user = await seedUser(); + + await seedUserSubscription({ + userId: user.id, + priceId: enterprisePriceId, + }); + + const template = await seedBlankTemplate(user); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/${template.id}`, + }); + + // Set EE action auth. + await page.getByTestId('documentActionSelectValue').click(); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); + + // Save the settings by going to the next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible(); + + // Return to the settings step to check that the results are saved correctly. + await page.getByRole('button', { name: 'Go Back' }).click(); + await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); + + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); + + await unseedUser(user.id); + }); + + test('[TEMPLATE_FLOW] enterprise team member can add action auth settings', async ({ page }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const owner = team.owner; + const teamMemberUser = team.members[1].user; + + // Make the team enterprise by giving the owner the enterprise subscription. + await seedUserSubscription({ + userId: team.ownerUserId, + priceId: enterprisePriceId, + }); + + const template = await seedBlankTemplate(owner, { + createTemplateOptions: { + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: teamMemberUser.email, + redirectPath: `/t/${team.url}/templates/${template.id}`, + }); + + // Set EE action auth. + await page.getByTestId('documentActionSelectValue').click(); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); + + // Save the settings by going to the next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible(); + + // Advanced settings should be visible. + await expect(page.getByLabel('Show advanced settings')).toBeVisible(); + + await unseedTeam(team.url); + }); + + test('[TEMPLATE_FLOW] enterprise team member should not have access to enterprise on personal account', async ({ + page, + }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const teamMemberUser = team.members[1].user; + + // Make the team enterprise by giving the owner the enterprise subscription. + await seedUserSubscription({ + userId: team.ownerUserId, + priceId: enterprisePriceId, + }); + + const template = await seedBlankTemplate(teamMemberUser); + + await apiSignin({ + page, + email: teamMemberUser.email, + redirectPath: `/templates/${template.id}`, + }); + + // Global action auth should not be visible. + await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible(); + + // Next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible(); + + // Advanced settings should not be visible. + await expect(page.getByLabel('Show advanced settings')).not.toBeVisible(); + + await unseedTeam(team.url); + }); +}); + +test('[TEMPLATE_FLOW]: add settings', async ({ page }) => { + const user = await seedUser(); + const template = await seedBlankTemplate(user); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/${template.id}`, + }); + + // Set title. + await page.getByLabel('Title').fill('New Title'); + + // Set access auth. + await page.getByTestId('documentAccessSelectValue').click(); + await page.getByLabel('Require account').getByText('Require account').click(); + await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); + + // Action auth should NOT be visible. + await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible(); + + // Save the settings by going to the next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible(); + + // Return to the settings step to check that the results are saved correctly. + await page.getByRole('button', { name: 'Go Back' }).click(); + await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); + + await expect(page.getByLabel('Title')).toHaveValue('New Title'); + await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); + + await unseedUser(user.id); +}); diff --git a/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts b/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts new file mode 100644 index 000000000..37b58f53b --- /dev/null +++ b/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts @@ -0,0 +1,106 @@ +import { expect, test } from '@playwright/test'; + +import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions'; +import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedUser, unseedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +test.describe('[EE_ONLY]', () => { + const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || ''; + + test.beforeEach(() => { + test.skip( + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId, + 'Billing required for this test', + ); + }); + + test('[TEMPLATE_FLOW] add EE settings', async ({ page }) => { + const user = await seedUser(); + + await seedUserSubscription({ + userId: user.id, + priceId: enterprisePriceId, + }); + + const template = await seedBlankTemplate(user); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/${template.id}`, + }); + + // Save the settings by going to the next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Add 2 signers. + await page.getByPlaceholder('Email').fill('recipient1@documenso.com'); + await page.getByPlaceholder('Name').fill('Recipient 1'); + await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click(); + await page + .getByRole('textbox', { name: 'Email', exact: true }) + .fill('recipient2@documenso.com'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); + + // Display advanced settings. + await page.getByLabel('Show advanced settings').check(); + + // Navigate to the next step and back. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible(); + await page.getByRole('button', { name: 'Go Back' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Expect that the advanced settings is unchecked, since no advanced settings were applied. + await expect(page.getByLabel('Show advanced settings')).toBeChecked({ checked: false }); + + // Add advanced settings for a single recipient. + await page.getByLabel('Show advanced settings').check(); + await page.getByRole('combobox').first().click(); + await page.getByLabel('Require passkey').click(); + + // Navigate to the next step and back. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible(); + await page.getByRole('button', { name: 'Go Back' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Expect that the advanced settings is visible, and the checkbox is hidden. Since advanced + // settings were applied. + await expect(page.getByLabel('Show advanced settings')).toBeHidden(); + + await unseedUser(user.id); + }); +}); + +test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => { + const user = await seedUser(); + const template = await seedBlankTemplate(user); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/${template.id}`, + }); + + // Save the settings by going to the next step. + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Add 2 signers. + await page.getByPlaceholder('Email').fill('recipient1@documenso.com'); + await page.getByPlaceholder('Name').fill('Recipient 1'); + await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click(); + await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); + + // Advanced settings should not be visible for non EE users. + await expect(page.getByLabel('Show advanced settings')).toBeHidden(); + + await unseedUser(user.id); +}); diff --git a/packages/app-tests/e2e/templates/create-document-from-template.spec.ts b/packages/app-tests/e2e/templates/create-document-from-template.spec.ts new file mode 100644 index 000000000..4dfa14eb7 --- /dev/null +++ b/packages/app-tests/e2e/templates/create-document-from-template.spec.ts @@ -0,0 +1,285 @@ +import { expect, test } from '@playwright/test'; + +import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; +import { prisma } from '@documenso/prisma'; +import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions'; +import { seedTeam } from '@documenso/prisma/seed/teams'; +import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || ''; + +/** + * 1. Create a template with all settings filled out + * 2. Create a document from the template + * 3. Ensure all values are correct + * + * Note: There is a direct copy paste of this test below for teams. + * + * If you update this test please update that test as well. + */ +test('[TEMPLATE]: should create a document from a template', async ({ page }) => { + const user = await seedUser(); + const template = await seedBlankTemplate(user); + + const isBillingEnabled = + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId; + + await seedUserSubscription({ + userId: user.id, + priceId: enterprisePriceId, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/${template.id}`, + }); + + // Set template title. + await page.getByLabel('Title').fill('TEMPLATE_TITLE'); + + // Set template document access. + await page.getByTestId('documentAccessSelectValue').click(); + await page.getByLabel('Require account').getByText('Require account').click(); + await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); + + // Set EE action auth. + if (isBillingEnabled) { + await page.getByTestId('documentActionSelectValue').click(); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); + } + + // Set email options. + await page.getByRole('button', { name: 'Email Options' }).click(); + await page.getByLabel('Subject (Optional)').fill('SUBJECT'); + await page.getByLabel('Message (Optional)').fill('MESSAGE'); + + // Set advanced options. + await page.getByRole('button', { name: 'Advanced Options' }).click(); + await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click(); + await page.getByLabel('DD/MM/YYYY').click(); + + await page.locator('.time-zone-field').click(); + await page.getByRole('option', { name: 'Etc/UTC' }).click(); + await page.getByLabel('Redirect URL').fill('https://documenso.com'); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Add 2 signers. + await page.getByPlaceholder('Email').fill('recipient1@documenso.com'); + await page.getByPlaceholder('Name').fill('Recipient 1'); + await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click(); + await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); + + // Apply require passkey for Recipient 1. + if (isBillingEnabled) { + await page.getByLabel('Show advanced settings').check(); + await page.getByRole('combobox').first().click(); + await page.getByLabel('Require passkey').click(); + } + + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible(); + + await page.getByRole('button', { name: 'Save template' }).click(); + + // Use template + await page.waitForURL('/templates'); + await page.getByRole('button', { name: 'Use Template' }).click(); + await page.getByRole('button', { name: 'Create as draft' }).click(); + + // Review that the document was created with the correct values. + await page.waitForURL(/documents/); + + const documentId = Number(page.url().split('/').pop()); + + const document = await prisma.document.findFirstOrThrow({ + where: { + id: documentId, + }, + include: { + Recipient: true, + documentMeta: true, + }, + }); + + const documentAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + }); + + expect(document.title).toEqual('TEMPLATE_TITLE'); + expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT'); + expect(documentAuth.documentAuthOption.globalActionAuth).toEqual( + isBillingEnabled ? 'PASSKEY' : null, + ); + expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a'); + expect(document.documentMeta?.message).toEqual('MESSAGE'); + expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com'); + expect(document.documentMeta?.subject).toEqual('SUBJECT'); + expect(document.documentMeta?.timezone).toEqual('Etc/UTC'); + + const recipientOne = document.Recipient[0]; + const recipientTwo = document.Recipient[1]; + + const recipientOneAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + recipientAuth: recipientOne.authOptions, + }); + + const recipientTwoAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + recipientAuth: recipientTwo.authOptions, + }); + + if (isBillingEnabled) { + expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY'); + } + + expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT'); + expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT'); +}); + +/** + * This is a direct copy paste of the above test but for teams. + */ +test('[TEMPLATE]: should create a team document from a team template', async ({ page }) => { + const { owner, ...team } = await seedTeam({ + createTeamMembers: 2, + }); + + const template = await seedBlankTemplate(owner, { + createTemplateOptions: { + teamId: team.id, + }, + }); + + const isBillingEnabled = + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId; + + await seedUserSubscription({ + userId: owner.id, + priceId: enterprisePriceId, + }); + + await apiSignin({ + page, + email: owner.email, + redirectPath: `/t/${team.url}/templates/${template.id}`, + }); + + // Set template title. + await page.getByLabel('Title').fill('TEMPLATE_TITLE'); + + // Set template document access. + await page.getByTestId('documentAccessSelectValue').click(); + await page.getByLabel('Require account').getByText('Require account').click(); + await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); + + // Set EE action auth. + if (isBillingEnabled) { + await page.getByTestId('documentActionSelectValue').click(); + await page.getByLabel('Require passkey').getByText('Require passkey').click(); + await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey'); + } + + // Set email options. + await page.getByRole('button', { name: 'Email Options' }).click(); + await page.getByLabel('Subject (Optional)').fill('SUBJECT'); + await page.getByLabel('Message (Optional)').fill('MESSAGE'); + + // Set advanced options. + await page.getByRole('button', { name: 'Advanced Options' }).click(); + await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click(); + await page.getByLabel('DD/MM/YYYY').click(); + + await page.locator('.time-zone-field').click(); + await page.getByRole('option', { name: 'Etc/UTC' }).click(); + await page.getByLabel('Redirect URL').fill('https://documenso.com'); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible(); + + // Add 2 signers. + await page.getByPlaceholder('Email').fill('recipient1@documenso.com'); + await page.getByPlaceholder('Name').fill('Recipient 1'); + await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click(); + await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com'); + await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2'); + + // Apply require passkey for Recipient 1. + if (isBillingEnabled) { + await page.getByLabel('Show advanced settings').check(); + await page.getByRole('combobox').first().click(); + await page.getByLabel('Require passkey').click(); + } + + await page.getByRole('button', { name: 'Continue' }).click(); + await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible(); + + await page.getByRole('button', { name: 'Save template' }).click(); + + // Use template + await page.waitForURL(`/t/${team.url}/templates`); + await page.getByRole('button', { name: 'Use Template' }).click(); + await page.getByRole('button', { name: 'Create as draft' }).click(); + + // Review that the document was created with the correct values. + await page.waitForURL(/documents/); + + const documentId = Number(page.url().split('/').pop()); + + const document = await prisma.document.findFirstOrThrow({ + where: { + id: documentId, + }, + include: { + Recipient: true, + documentMeta: true, + }, + }); + + expect(document.teamId).toEqual(team.id); + + const documentAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + }); + + expect(document.title).toEqual('TEMPLATE_TITLE'); + expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT'); + expect(documentAuth.documentAuthOption.globalActionAuth).toEqual( + isBillingEnabled ? 'PASSKEY' : null, + ); + expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a'); + expect(document.documentMeta?.message).toEqual('MESSAGE'); + expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com'); + expect(document.documentMeta?.subject).toEqual('SUBJECT'); + expect(document.documentMeta?.timezone).toEqual('Etc/UTC'); + + const recipientOne = document.Recipient[0]; + const recipientTwo = document.Recipient[1]; + + const recipientOneAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + recipientAuth: recipientOne.authOptions, + }); + + const recipientTwoAuth = extractDocumentAuthMethods({ + documentAuth: document.authOptions, + recipientAuth: recipientTwo.authOptions, + }); + + if (isBillingEnabled) { + expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY'); + } + + expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT'); + expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT'); +}); diff --git a/packages/lib/schemas/common.ts b/packages/lib/schemas/common.ts new file mode 100644 index 000000000..101aeeff5 --- /dev/null +++ b/packages/lib/schemas/common.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +import { URL_REGEX } from '../constants/url-regex'; + +/** + * Note this allows empty strings. + */ +export const ZUrlSchema = z + .string() + .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }); diff --git a/packages/lib/server-only/field/set-fields-for-template.ts b/packages/lib/server-only/field/set-fields-for-template.ts index 2062e06bc..62e8cbcd1 100644 --- a/packages/lib/server-only/field/set-fields-for-template.ts +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -1,22 +1,19 @@ import { prisma } from '@documenso/prisma'; import type { FieldType } from '@documenso/prisma/client'; -export type Field = { - id?: number | null; - type: FieldType; - signerEmail: string; - signerId?: number; - pageNumber: number; - pageX: number; - pageY: number; - pageWidth: number; - pageHeight: number; -}; - export type SetFieldsForTemplateOptions = { userId: number; templateId: number; - fields: Field[]; + fields: { + id?: number | null; + type: FieldType; + signerEmail: string; + pageNumber: number; + pageX: number; + pageY: number; + pageWidth: number; + pageHeight: number; + }[]; }; export const setFieldsForTemplate = async ({ @@ -58,11 +55,7 @@ export const setFieldsForTemplate = async ({ }); const removedFields = existingFields.filter( - (existingField) => - !fields.find( - (field) => - field.id === existingField.id || field.signerEmail === existingField.Recipient?.email, - ), + (existingField) => !fields.find((field) => field.id === existingField.id), ); const linkedFields = fields.map((field) => { @@ -127,5 +120,13 @@ export const setFieldsForTemplate = async ({ }); } - return persistedFields; + // Filter out fields that have been removed or have been updated. + const filteredFields = existingFields.filter((field) => { + const isRemoved = removedFields.find((removedField) => removedField.id === field.id); + const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id); + + return !isRemoved && !isUpdated; + }); + + return [...filteredFields, ...persistedFields]; }; diff --git a/packages/lib/server-only/recipient/set-recipients-for-template.ts b/packages/lib/server-only/recipient/set-recipients-for-template.ts index 5315711a5..73d05ab4e 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-template.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-template.ts @@ -1,21 +1,32 @@ +import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { prisma } from '@documenso/prisma'; -import type { RecipientRole } from '@documenso/prisma/client'; +import type { Recipient } from '@documenso/prisma/client'; +import { RecipientRole } from '@documenso/prisma/client'; +import { AppError, AppErrorCode } from '../../errors/app-error'; +import { + type TRecipientActionAuthTypes, + ZRecipientAuthOptionsSchema, +} from '../../types/document-auth'; import { nanoid } from '../../universal/id'; +import { createRecipientAuthOptions } from '../../utils/document-auth'; export type SetRecipientsForTemplateOptions = { userId: number; + teamId?: number; templateId: number; recipients: { id?: number; email: string; name: string; role: RecipientRole; + actionAuth?: TRecipientActionAuthTypes | null; }[]; }; export const setRecipientsForTemplate = async ({ userId, + teamId, templateId, recipients, }: SetRecipientsForTemplateOptions) => { @@ -43,6 +54,23 @@ export const setRecipientsForTemplate = async ({ throw new Error('Template not found'); } + const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth); + + // Check if user has permission to set the global action auth. + if (recipientsHaveActionAuth) { + const isDocumentEnterprise = await isUserEnterprise({ + userId, + teamId, + }); + + if (!isDocumentEnterprise) { + throw new AppError( + AppErrorCode.UNAUTHORIZED, + 'You do not have permission to set the action auth', + ); + } + } + const normalizedRecipients = recipients.map((recipient) => ({ ...recipient, email: recipient.email.toLowerCase(), @@ -74,31 +102,59 @@ export const setRecipientsForTemplate = async ({ }; }); - const persistedRecipients = await prisma.$transaction( - // Disabling as wrapping promises here causes type issues - // eslint-disable-next-line @typescript-eslint/promise-function-async - linkedRecipients.map((recipient) => - prisma.recipient.upsert({ - where: { - id: recipient._persisted?.id ?? -1, - templateId, - }, - update: { - name: recipient.name, - email: recipient.email, - role: recipient.role, - templateId, - }, - create: { - name: recipient.name, - email: recipient.email, - role: recipient.role, - token: nanoid(), - templateId, - }, + const persistedRecipients = await prisma.$transaction(async (tx) => { + return await Promise.all( + linkedRecipients.map(async (recipient) => { + let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions); + + if (recipient.actionAuth !== undefined) { + authOptions = createRecipientAuthOptions({ + accessAuth: authOptions.accessAuth, + actionAuth: recipient.actionAuth, + }); + } + + const upsertedRecipient = await tx.recipient.upsert({ + where: { + id: recipient._persisted?.id ?? -1, + templateId, + }, + update: { + name: recipient.name, + email: recipient.email, + role: recipient.role, + templateId, + authOptions, + }, + create: { + name: recipient.name, + email: recipient.email, + role: recipient.role, + token: nanoid(), + templateId, + authOptions, + }, + }); + + const recipientId = upsertedRecipient.id; + + // Clear all fields if the recipient role is changed to a type that cannot have fields. + if ( + recipient._persisted && + recipient._persisted.role !== recipient.role && + (recipient.role === RecipientRole.CC || recipient.role === RecipientRole.VIEWER) + ) { + await tx.field.deleteMany({ + where: { + recipientId, + }, + }); + } + + return upsertedRecipient; }), - ), - ); + ); + }); if (removedRecipients.length > 0) { await prisma.recipient.deleteMany({ @@ -110,5 +166,17 @@ export const setRecipientsForTemplate = async ({ }); } - return persistedRecipients; + // Filter out recipients that have been removed or have been updated. + const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => { + const isRemoved = removedRecipients.find( + (removedRecipient) => removedRecipient.id === recipient.id, + ); + const isUpdated = persistedRecipients.find( + (persistedRecipient) => persistedRecipient.id === recipient.id, + ); + + return !isRemoved && !isUpdated; + }); + + return [...filteredRecipients, ...persistedRecipients]; }; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 7cd098d6d..92590cfb2 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -5,15 +5,25 @@ import { type Recipient, WebhookTriggerEvents } from '@documenso/prisma/client'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; +import { ZRecipientAuthOptionsSchema } from '../../types/document-auth'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; +import { + createDocumentAuthOptions, + createRecipientAuthOptions, + extractDocumentAuthMethods, +} from '../../utils/document-auth'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; -type FinalRecipient = Pick & { +type FinalRecipient = Pick & { templateRecipientId: number; fields: Field[]; }; +export type CreateDocumentFromTemplateResponse = Awaited< + ReturnType +>; + export type CreateDocumentFromTemplateOptions = { templateId: number; userId: number; @@ -23,6 +33,19 @@ export type CreateDocumentFromTemplateOptions = { name?: string; email: string; }[]; + + /** + * Values that will override the predefined values in the template. + */ + override?: { + title?: string; + subject?: string; + message?: string; + timezone?: string; + password?: string; + dateFormat?: string; + redirectUrl?: string; + }; requestMetadata?: RequestMetadata; }; @@ -31,6 +54,7 @@ export const createDocumentFromTemplate = async ({ userId, teamId, recipients, + override, requestMetadata, }: CreateDocumentFromTemplateOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -65,6 +89,7 @@ export const createDocumentFromTemplate = async ({ }, }, templateDocumentData: true, + templateMeta: true, }, }); @@ -72,26 +97,34 @@ export const createDocumentFromTemplate = async ({ throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found'); } - if (recipients.length !== template.Recipient.length) { - throw new AppError(AppErrorCode.INVALID_BODY, 'Invalid number of recipients.'); - } - - const finalRecipients: FinalRecipient[] = template.Recipient.map((templateRecipient) => { - const foundRecipient = recipients.find((recipient) => recipient.id === templateRecipient.id); + // Check that all the passed in recipient IDs can be associated with a template recipient. + recipients.forEach((recipient) => { + const foundRecipient = template.Recipient.find( + (templateRecipient) => templateRecipient.id === recipient.id, + ); if (!foundRecipient) { throw new AppError( AppErrorCode.INVALID_BODY, - `Missing template recipient with ID ${templateRecipient.id}`, + `Recipient with ID ${recipient.id} not found in the template.`, ); } + }); + + const { documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({ + documentAuth: template.authOptions, + }); + + const finalRecipients: FinalRecipient[] = template.Recipient.map((templateRecipient) => { + const foundRecipient = recipients.find((recipient) => recipient.id === templateRecipient.id); return { templateRecipientId: templateRecipient.id, fields: templateRecipient.Field, - name: foundRecipient.name ?? '', - email: foundRecipient.email, + name: foundRecipient ? foundRecipient.name ?? '' : templateRecipient.name, + email: foundRecipient ? foundRecipient.email : templateRecipient.email, role: templateRecipient.role, + authOptions: templateRecipient.authOptions, }; }); @@ -108,16 +141,38 @@ export const createDocumentFromTemplate = async ({ data: { userId, teamId: template.teamId, - title: template.title, + title: override?.title || template.title, documentDataId: documentData.id, + authOptions: createDocumentAuthOptions({ + globalAccessAuth: templateAuthOptions.globalAccessAuth, + globalActionAuth: templateAuthOptions.globalActionAuth, + }), + documentMeta: { + create: { + subject: override?.subject || template.templateMeta?.subject, + message: override?.message || template.templateMeta?.message, + timezone: override?.timezone || template.templateMeta?.timezone, + password: override?.password || template.templateMeta?.password, + dateFormat: override?.dateFormat || template.templateMeta?.dateFormat, + redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl, + }, + }, Recipient: { createMany: { - data: finalRecipients.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - token: nanoid(), - })), + data: finalRecipients.map((recipient) => { + const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions); + + return { + email: recipient.email, + name: recipient.name, + role: recipient.role, + authOptions: createRecipientAuthOptions({ + accessAuth: authOptions.accessAuth, + actionAuth: authOptions.actionAuth, + }), + token: nanoid(), + }; + }), }, }, }, diff --git a/packages/lib/server-only/template/get-template-with-details-by-id.ts b/packages/lib/server-only/template/get-template-with-details-by-id.ts new file mode 100644 index 000000000..7d02c87cf --- /dev/null +++ b/packages/lib/server-only/template/get-template-with-details-by-id.ts @@ -0,0 +1,38 @@ +import { prisma } from '@documenso/prisma'; +import type { TemplateWithDetails } from '@documenso/prisma/types/template'; + +export type GetTemplateWithDetailsByIdOptions = { + id: number; + userId: number; +}; + +export const getTemplateWithDetailsById = async ({ + id, + userId, +}: GetTemplateWithDetailsByIdOptions): Promise => { + return await prisma.template.findFirstOrThrow({ + where: { + id, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], + }, + include: { + templateDocumentData: true, + templateMeta: true, + Recipient: true, + Field: true, + }, + }); +}; diff --git a/packages/lib/server-only/template/update-template-settings.ts b/packages/lib/server-only/template/update-template-settings.ts new file mode 100644 index 000000000..ebf15bac0 --- /dev/null +++ b/packages/lib/server-only/template/update-template-settings.ts @@ -0,0 +1,139 @@ +'use server'; + +import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; +import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { prisma } from '@documenso/prisma'; +import type { TemplateMeta } from '@documenso/prisma/client'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; +import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; +import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth'; + +export type UpdateTemplateSettingsOptions = { + userId: number; + teamId?: number; + templateId: number; + data: { + title?: string; + globalAccessAuth?: TDocumentAccessAuthTypes | null; + globalActionAuth?: TDocumentActionAuthTypes | null; + }; + meta?: Partial>; + requestMetadata?: RequestMetadata; +}; + +export const updateTemplateSettings = async ({ + userId, + teamId, + templateId, + meta, + data, +}: UpdateTemplateSettingsOptions) => { + if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) { + throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update'); + } + + const template = await prisma.template.findFirstOrThrow({ + where: { + id: templateId, + ...(teamId + ? { + team: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + } + : { + userId, + teamId: null, + }), + }, + include: { + templateMeta: true, + }, + }); + + const { documentAuthOption } = extractDocumentAuthMethods({ + documentAuth: template.authOptions, + }); + + const { templateMeta } = template; + + const isDateSame = (templateMeta?.dateFormat || null) === (meta?.dateFormat || null); + const isMessageSame = (templateMeta?.message || null) === (meta?.message || null); + const isPasswordSame = (templateMeta?.password || null) === (meta?.password || null); + const isSubjectSame = (templateMeta?.subject || null) === (meta?.subject || null); + const isRedirectUrlSame = (templateMeta?.redirectUrl || null) === (meta?.redirectUrl || null); + const isTimezoneSame = (templateMeta?.timezone || null) === (meta?.timezone || null); + + // Early return to avoid unnecessary updates. + if ( + template.title === data.title && + data.globalAccessAuth === documentAuthOption.globalAccessAuth && + data.globalActionAuth === documentAuthOption.globalActionAuth && + isDateSame && + isMessageSame && + isPasswordSame && + isSubjectSame && + isRedirectUrlSame && + isTimezoneSame + ) { + return template; + } + + const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null; + const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null; + + // If the new global auth values aren't passed in, fallback to the current document values. + const newGlobalAccessAuth = + data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth; + const newGlobalActionAuth = + data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth; + + // Check if user has permission to set the global action auth. + if (newGlobalActionAuth) { + const isDocumentEnterprise = await isUserEnterprise({ + userId, + teamId, + }); + + if (!isDocumentEnterprise) { + throw new AppError( + AppErrorCode.UNAUTHORIZED, + 'You do not have permission to set the action auth', + ); + } + } + + const authOptions = createDocumentAuthOptions({ + globalAccessAuth: newGlobalAccessAuth, + globalActionAuth: newGlobalActionAuth, + }); + + return await prisma.template.update({ + where: { + id: templateId, + }, + data: { + title: data.title, + authOptions, + templateMeta: { + upsert: { + where: { + templateId, + }, + create: { + ...meta, + }, + update: { + ...meta, + }, + }, + }, + }, + }); +}; diff --git a/packages/prisma/migrations/20240508150017_add_template_settings/migration.sql b/packages/prisma/migrations/20240508150017_add_template_settings/migration.sql new file mode 100644 index 000000000..ca2341090 --- /dev/null +++ b/packages/prisma/migrations/20240508150017_add_template_settings/migration.sql @@ -0,0 +1,22 @@ +-- AlterTable +ALTER TABLE "Template" ADD COLUMN "authOptions" JSONB; + +-- CreateTable +CREATE TABLE "TemplateMeta" ( + "id" TEXT NOT NULL, + "subject" TEXT, + "message" TEXT, + "timezone" TEXT DEFAULT 'Etc/UTC', + "password" TEXT, + "dateFormat" TEXT DEFAULT 'yyyy-MM-dd hh:mm a', + "templateId" INTEGER NOT NULL, + "redirectUrl" TEXT, + + CONSTRAINT "TemplateMeta_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TemplateMeta_templateId_key" ON "TemplateMeta"("templateId"); + +-- AddForeignKey +ALTER TABLE "TemplateMeta" ADD CONSTRAINT "TemplateMeta_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8acfbedfa..5c6752092 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -539,15 +539,29 @@ enum TemplateType { PRIVATE } +model TemplateMeta { + id String @id @default(cuid()) + subject String? + message String? + timezone String? @default("Etc/UTC") @db.Text + password String? + dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text + templateId Int @unique + template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) + redirectUrl String? +} + model Template { - id Int @id @default(autoincrement()) - type TemplateType @default(PRIVATE) + id Int @id @default(autoincrement()) + type TemplateType @default(PRIVATE) title String userId Int teamId Int? + authOptions Json? + templateMeta TemplateMeta? templateDocumentDataId String - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts index 3feb82289..f37306c87 100644 --- a/packages/prisma/seed/templates.ts +++ b/packages/prisma/seed/templates.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { prisma } from '..'; +import type { Prisma, User } from '../client'; import { DocumentDataType, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '../client'; const examplePdf = fs @@ -14,6 +15,32 @@ type SeedTemplateOptions = { teamId?: number; }; +type CreateTemplateOptions = { + key?: string | number; + createTemplateOptions?: Partial; +}; + +export const seedBlankTemplate = async (owner: User, options: CreateTemplateOptions = {}) => { + const { key, createTemplateOptions = {} } = options; + + const documentData = await prisma.documentData.create({ + data: { + type: DocumentDataType.BYTES_64, + data: examplePdf, + initialData: examplePdf, + }, + }); + + return await prisma.template.create({ + data: { + title: `[TEST] Template ${key}`, + templateDocumentDataId: documentData.id, + userId: owner.id, + ...createTemplateOptions, + }, + }); +}; + export const seedTemplate = async (options: SeedTemplateOptions) => { const { title = 'Untitled', userId, teamId } = options; diff --git a/packages/prisma/types/template.ts b/packages/prisma/types/template.ts new file mode 100644 index 000000000..c5dc054a7 --- /dev/null +++ b/packages/prisma/types/template.ts @@ -0,0 +1,19 @@ +import type { + DocumentData, + Field, + Recipient, + Template, + TemplateMeta, +} from '@documenso/prisma/client'; + +export type TemplateWithData = Template & { + templateDocumentData?: DocumentData | null; + templateMeta?: TemplateMeta | null; +}; + +export type TemplateWithDetails = Template & { + templateDocumentData: DocumentData; + templateMeta: TemplateMeta | null; + Recipient: Recipient[]; + Field: Field[]; +}; diff --git a/packages/trpc/server/admin-router/router.ts b/packages/trpc/server/admin-router/router.ts index 05ee84736..7ab4c5d2d 100644 --- a/packages/trpc/server/admin-router/router.ts +++ b/packages/trpc/server/admin-router/router.ts @@ -102,7 +102,7 @@ export const adminRouter = router({ try { return await sealDocument({ documentId: id, isResealing: true }); } catch (err) { - console.log('resealDocument error', err); + console.error('resealDocument error', err); throw new TRPCError({ code: 'BAD_REQUEST', @@ -123,7 +123,7 @@ export const adminRouter = router({ return await deleteUser({ id }); } catch (err) { - console.log(err); + console.error(err); throw new TRPCError({ code: 'BAD_REQUEST', @@ -144,7 +144,7 @@ export const adminRouter = router({ requestMetadata: extractNextApiRequestMetadata(ctx.req), }); } catch (err) { - console.log(err); + console.error(err); throw new TRPCError({ code: 'BAD_REQUEST', diff --git a/packages/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index 354e937a5..d097e2400 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -53,7 +53,7 @@ export const fieldRouter = router({ const { templateId, fields } = input; try { - await setFieldsForTemplate({ + return await setFieldsForTemplate({ userId: ctx.user.id, templateId, fields: fields.map((field) => ({ diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index 61740e9a0..584c19ff5 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -46,16 +46,18 @@ export const recipientRouter = router({ .input(ZAddTemplateSignersMutationSchema) .mutation(async ({ input, ctx }) => { try { - const { templateId, signers } = input; + const { templateId, signers, teamId } = input; return await setRecipientsForTemplate({ userId: ctx.user.id, + teamId, templateId, recipients: signers.map((signer) => ({ id: signer.nativeId, email: signer.email, name: signer.name, role: signer.role, + actionAuth: signer.actionAuth, })), }); } catch (err) { diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index 4b5522150..4317285c0 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -34,6 +34,7 @@ export type TAddSignersMutationSchema = z.infer { + try { + return await getTemplateWithDetailsById({ + id: input.id, + userId: ctx.user.id, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to find this template. Please try again later.', + }); + } + }), + + // Todo: Add API + updateTemplateSettings: authenticatedProcedure + .input(ZUpdateTemplateSettingsMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { templateId, teamId, data, meta } = input; + + const userId = ctx.user.id; + + const requestMetadata = extractNextApiRequestMetadata(ctx.req); + + return await updateTemplateSettings({ + userId, + teamId, + templateId, + data, + meta, + requestMetadata, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: + 'We were unable to update the settings for this template. Please try again later.', + }); + } + }), }); diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index ce1489ac3..79d609488 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,5 +1,11 @@ import { z } from 'zod'; +import { URL_REGEX } from '@documenso/lib/constants/url-regex'; +import { + ZDocumentAccessAuthTypesSchema, + ZDocumentActionAuthTypesSchema, +} from '@documenso/lib/types/document-auth'; + export const ZCreateTemplateMutationSchema = z.object({ title: z.string().min(1).trim(), teamId: z.number().optional(), @@ -33,10 +39,38 @@ export const ZDeleteTemplateMutationSchema = z.object({ id: z.number().min(1), }); +export const ZUpdateTemplateSettingsMutationSchema = z.object({ + templateId: z.number(), + teamId: z.number().min(1).optional(), + data: z.object({ + title: z.string().min(1).optional(), + globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(), + globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(), + }), + meta: z.object({ + subject: z.string(), + message: z.string(), + timezone: z.string(), + dateFormat: z.string(), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || value === '' || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }), + }), +}); + +export const ZGetTemplateWithDetailsByIdQuerySchema = z.object({ + id: z.number().min(1), +}); + export type TCreateTemplateMutationSchema = z.infer; export type TCreateDocumentFromTemplateMutationSchema = z.infer< typeof ZCreateDocumentFromTemplateMutationSchema >; - export type TDuplicateTemplateMutationSchema = z.infer; export type TDeleteTemplateMutationSchema = z.infer; +export type TGetTemplateWithDetailsByIdQuerySchema = z.infer< + typeof ZGetTemplateWithDetailsByIdQuerySchema +>; diff --git a/packages/ui/components/document/document-global-auth-access-select.tsx b/packages/ui/components/document/document-global-auth-access-select.tsx new file mode 100644 index 000000000..f660d7c10 --- /dev/null +++ b/packages/ui/components/document/document-global-auth-access-select.tsx @@ -0,0 +1,66 @@ +'use client'; + +import React, { forwardRef } from 'react'; + +import type { SelectProps } from '@radix-ui/react-select'; +import { InfoIcon } from 'lucide-react'; + +import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; +import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; + +export const DocumentGlobalAuthAccessSelect = forwardRef( + (props, ref) => ( + + ), +); + +DocumentGlobalAuthAccessSelect.displayName = 'DocumentGlobalAuthAccessSelect'; + +export const DocumentGlobalAuthAccessTooltip = () => ( + + + + + + +

+ Document access +

+ +

The authentication required for recipients to view the document.

+ +
    +
  • + Require account - The recipient must be signed in to view the document +
  • +
  • + None - The document can be accessed directly by the URL sent to the + recipient +
  • +
+
+
+); diff --git a/packages/ui/components/document/document-global-auth-action-select.tsx b/packages/ui/components/document/document-global-auth-action-select.tsx new file mode 100644 index 000000000..d90b492ac --- /dev/null +++ b/packages/ui/components/document/document-global-auth-action-select.tsx @@ -0,0 +1,80 @@ +'use client'; + +import React, { forwardRef } from 'react'; + +import type { SelectProps } from '@radix-ui/react-select'; +import { InfoIcon } from 'lucide-react'; + +import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; +import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; + +export const DocumentGlobalAuthActionSelect = forwardRef( + (props, ref) => ( + + ), +); + +DocumentGlobalAuthActionSelect.displayName = 'DocumentGlobalAuthActionSelect'; + +export const DocumentGlobalAuthActionTooltip = () => ( + + + + + + +

+ Global recipient action authentication +

+ +

The authentication required for recipients to sign the signature field.

+ +

+ This can be overriden by setting the authentication requirements directly on each recipient + in the next step. +

+ +
    + {/*
  • + Require account - The recipient must be signed in +
  • */} +
  • + Require passkey - The recipient must have an account and passkey + configured via their settings +
  • +
  • + Require 2FA - The recipient must have an account and 2FA enabled via + their settings +
  • +
  • + None - No authentication required +
  • +
+
+
+); diff --git a/packages/ui/components/document/document-send-email-message-helper.tsx b/packages/ui/components/document/document-send-email-message-helper.tsx new file mode 100644 index 000000000..855baefa4 --- /dev/null +++ b/packages/ui/components/document/document-send-email-message-helper.tsx @@ -0,0 +1,34 @@ +'use client'; + +import React from 'react'; + +export const DocumentSendEmailMessageHelper = () => { + return ( +
+

+ You can use the following variables in your message: +

+ +
    +
  • + + {'{signer.name}'} + {' '} + - The signer's name +
  • +
  • + + {'{signer.email}'} + {' '} + - The signer's email +
  • +
  • + + {'{document.name}'} + {' '} + - The document's name +
  • +
+
+ ); +}; diff --git a/packages/ui/components/recipient/recipient-role-select.tsx b/packages/ui/components/recipient/recipient-role-select.tsx index 43d3331ae..eb1735a34 100644 --- a/packages/ui/components/recipient/recipient-role-select.tsx +++ b/packages/ui/components/recipient/recipient-role-select.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { forwardRef } from 'react'; import type { SelectProps } from '@radix-ui/react-select'; import { InfoIcon } from 'lucide-react'; @@ -12,86 +12,86 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive export type RecipientRoleSelectProps = SelectProps; -export const RecipientRoleSelect = (props: RecipientRoleSelectProps) => { - return ( - + + {/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */} + {ROLE_ICONS[props.value as RecipientRole]} + - - -
-
- {ROLE_ICONS[RecipientRole.SIGNER]} - Needs to sign -
- - - - - -

The recipient is required to sign the document for it to be completed.

-
-
+ + +
+
+ {ROLE_ICONS[RecipientRole.SIGNER]} + Needs to sign
- + + + + + +

The recipient is required to sign the document for it to be completed.

+
+
+
+
- -
-
- {ROLE_ICONS[RecipientRole.APPROVER]} - Needs to approve -
- - - - - -

The recipient is required to approve the document for it to be completed.

-
-
+ +
+
+ {ROLE_ICONS[RecipientRole.APPROVER]} + Needs to approve
- + + + + + +

The recipient is required to approve the document for it to be completed.

+
+
+
+
- -
-
- {ROLE_ICONS[RecipientRole.VIEWER]} - Needs to view -
- - - - - -

The recipient is required to view the document for it to be completed.

-
-
+ +
+
+ {ROLE_ICONS[RecipientRole.VIEWER]} + Needs to view
- + + + + + +

The recipient is required to view the document for it to be completed.

+
+
+
+
- -
-
- {ROLE_ICONS[RecipientRole.CC]} - Receives copy -
- - - - - -

- The recipient is not required to take any action and receives a copy of the - document after it is completed. -

-
-
+ +
+
+ {ROLE_ICONS[RecipientRole.CC]} + Receives copy
- - - - ); -}; + + + + + +

+ The recipient is not required to take any action and receives a copy of the document + after it is completed. +

+
+
+
+
+ + +)); + +RecipientRoleSelect.displayName = 'RecipientRoleSelect'; diff --git a/packages/ui/primitives/document-flow/add-settings.tsx b/packages/ui/primitives/document-flow/add-settings.tsx index ce52e03c2..5289ec483 100644 --- a/packages/ui/primitives/document-flow/add-settings.tsx +++ b/packages/ui/primitives/document-flow/add-settings.tsx @@ -7,16 +7,18 @@ import { InfoIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; -import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; -import { - DocumentAccessAuth, - DocumentActionAuth, - DocumentAuth, -} from '@documenso/lib/types/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { + DocumentGlobalAuthAccessSelect, + DocumentGlobalAuthAccessTooltip, +} from '@documenso/ui/components/document/document-global-auth-access-select'; +import { + DocumentGlobalAuthActionSelect, + DocumentGlobalAuthActionTooltip, +} from '@documenso/ui/components/document/document-global-auth-action-select'; import { Accordion, AccordionContent, @@ -144,49 +146,11 @@ export const AddSettingsFormPartial = ({ Document access - - - - - - -

- Document access -

- -

The authentication required for recipients to view the document.

- -
    -
  • - Require account - The recipient must be signed in to - view the document -
  • -
  • - None - The document can be accessed directly by the URL - sent to the recipient -
  • -
-
-
+
- +
)} @@ -200,64 +164,11 @@ export const AddSettingsFormPartial = ({ Recipient action authentication - - - - - - -

- Global recipient action authentication -

- -

- The authentication required for recipients to sign the signature field. -

- -

- This can be overriden by setting the authentication requirements - directly on each recipient in the next step. -

- -
    - {/*
  • - Require account - The recipient must be signed in -
  • */} -
  • - Require passkey - The recipient must have an account - and passkey configured via their settings -
  • -
  • - Require 2FA - The recipient must have an account and - 2FA enabled via their settings -
  • -
  • - None - No authentication required -
  • -
-
-
+
- +
)} diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 1b0608af8..bef5fbf5c 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -6,6 +6,7 @@ import { useForm } from 'react-hook-form'; import type { Field, Recipient } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; import { FormErrorMessage } from '../form/form-error-message'; import { Input } from '../input'; @@ -104,32 +105,7 @@ export const AddSubjectFormPartial = ({ />
-
-

- You can use the following variables in your message: -

- -
    -
  • - - {'{signer.name}'} - {' '} - - The signer's name -
  • -
  • - - {'{signer.email}'} - {' '} - - The signer's email -
  • -
  • - - {'{document.name}'} - {' '} - - The document's name -
  • -
-
+
diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index bbed6a39a..aa6eaec3c 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -26,6 +26,7 @@ import { DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from '../document-flow/document-flow-root'; +import { ShowFieldItem } from '../document-flow/show-field-item'; import type { DocumentFlowStep } from '../document-flow/types'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { useStep } from '../stepper'; @@ -36,15 +37,17 @@ export type AddTemplatePlaceholderRecipientsFormProps = { documentFlow: DocumentFlowStep; recipients: Recipient[]; fields: Field[]; - isTemplateOwnerEnterprise: boolean; + isEnterprise: boolean; + isDocumentPdfLoaded: boolean; onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void; }; export const AddTemplatePlaceholderRecipientsFormPartial = ({ documentFlow, - isTemplateOwnerEnterprise, + isEnterprise, recipients, - fields: _fields, + fields, + isDocumentPdfLoaded, onSubmit, }: AddTemplatePlaceholderRecipientsFormProps) => { const initialId = useId(); @@ -144,6 +147,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ return ( <> + {isDocumentPdfLoaded && + fields.map((field, index) => ( + + ))} +
@@ -209,7 +217,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ )} /> - {showAdvancedSettings && isTemplateOwnerEnterprise && ( + {showAdvancedSettings && isEnterprise && (
- {!alwaysShowAdvancedSettings && isTemplateOwnerEnterprise && ( + {!alwaysShowAdvancedSettings && isEnterprise && (
void; +}; + +export const AddTemplateSettingsFormPartial = ({ + documentFlow, + recipients, + fields, + isEnterprise, + isDocumentPdfLoaded, + template, + onSubmit, +}: AddTemplateSettingsFormProps) => { + const { documentAuthOption } = extractDocumentAuthMethods({ + documentAuth: template.authOptions, + }); + + const form = useForm({ + resolver: zodResolver(ZAddTemplateSettingsFormSchema), + defaultValues: { + title: template.title, + globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined, + globalActionAuth: documentAuthOption?.globalActionAuth || undefined, + meta: { + subject: template.templateMeta?.subject ?? '', + message: template.templateMeta?.message ?? '', + timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + dateFormat: template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + redirectUrl: template.templateMeta?.redirectUrl ?? '', + }, + }, + }); + + const { stepIndex, currentStep, totalSteps, previousStep } = useStep(); + + // We almost always want to set the timezone to the user's local timezone to avoid confusion + // when the document is signed. + useEffect(() => { + if (!form.formState.touchedFields.meta?.timezone) { + form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); + } + }, [form, form.setValue, form.formState.touchedFields.meta?.timezone]); + + return ( + <> + + {isDocumentPdfLoaded && + fields.map((field, index) => ( + + ))} + + +
+ ( + + Template title + + + + + + + )} + /> + + ( + + + Document access + + + + + + + + )} + /> + + {isEnterprise && ( + ( + + + Recipient action authentication + + + + + + + + )} + /> + )} + + + + + Email Options + + + +
+ ( + + + Subject (Optional) + + + + + + + + + )} + /> + + ( + + + Message (Optional) + + + +