From 64e3e2c64b6f897b66676905f9467a216530cf1a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 3 May 2024 22:25:24 +0700 Subject: [PATCH 1/8] fix: disable encrypted pdfs (#1130) ## Description Currently if you complete a pending encrypted document, it will prevent the document from being sealed due to the systems inability to decrypt it. This PR disables uploading any documents that cannot be loaded as a temporary measure. **Note** This is a client side only check ## Changes Made - Disable uploading documents that cannot be parsed - Refactor putFile to putDocumentFile - Add a flag as a backup incase something goes wrong --- .../app/(marketing)/singleplayer/client.tsx | 4 +-- .../(dashboard)/documents/upload-document.tsx | 21 ++++++++---- .../templates/new-template-dialog.tsx | 4 +-- packages/api/v1/implementation.ts | 4 +-- .../webhook/on-early-adopters-checkout.ts | 4 +-- packages/lib/constants/feature-flags.ts | 1 + .../lib/server-only/document/seal-document.ts | 4 +-- .../server-only/document/send-document.tsx | 4 +-- packages/lib/universal/upload/put-file.ts | 33 +++++++++++++++++-- .../trpc/server/singleplayer-router/router.ts | 4 +-- 10 files changed, 60 insertions(+), 23 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index e20b94887..7082c04e1 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { base64 } from '@documenso/lib/universal/base64'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import type { Field, Recipient } from '@documenso/prisma/client'; import { DocumentDataType, Prisma } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; @@ -115,7 +115,7 @@ export const SinglePlayerClient = () => { } try { - const putFileData = await putFile(uploadedFile.file); + const putFileData = await putPdfFile(uploadedFile.file); const documentToken = await createSinglePlayerDocument({ documentData: { diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 26f1e795c..0a0c029ab 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -10,8 +10,9 @@ import { useSession } from 'next-auth/react'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; +import { AppError } from '@documenso/lib/errors/app-error'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; @@ -57,7 +58,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => { try { setIsLoading(true); - const { type, data } = await putFile(file); + const { type, data } = await putPdfFile(file); const { id: documentDataId } = await createDocumentData({ type, @@ -83,13 +84,21 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => { }); router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`); - } catch (error) { - console.error(error); + } catch (err) { + const error = AppError.parseError(err); - if (error instanceof TRPCClientError) { + console.error(err); + + if (error.code === 'INVALID_DOCUMENT_FILE') { + toast({ + title: 'Invalid file', + description: 'You cannot upload encrypted PDFs', + variant: 'destructive', + }); + } else if (err instanceof TRPCClientError) { toast({ title: 'Error', - description: error.message, + description: err.message, variant: 'destructive', }); } else { 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 ab05ac3dc..1a6e34584 100644 --- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -12,7 +12,7 @@ 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 { putFile } from '@documenso/lib/universal/upload/put-file'; +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'; @@ -98,7 +98,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo const file: File = uploadedFile.file; try { - const { type, data } = await putFile(file); + const { type, data } = await putPdfFile(file); const { id: templateDocumentDataId } = await createDocumentData({ type, diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 253803fc8..b1e069e35 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -22,7 +22,7 @@ import { updateRecipient } from '@documenso/lib/server-only/recipient/update-rec import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-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 { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { getPresignGetUrl, getPresignPostUrl, @@ -303,7 +303,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { formValues: body.formValues, }); - const newDocumentData = await putFile({ + const newDocumentData = await putPdfFile({ name: fileName, type: 'application/pdf', arrayBuffer: async () => Promise.resolve(prefilled), diff --git a/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts b/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts index 22f60069e..cda583e81 100644 --- a/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts +++ b/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts @@ -5,7 +5,7 @@ import { sealDocument } from '@documenso/lib/server-only/document/seal-document' import { redis } from '@documenso/lib/server-only/redis'; import { stripe } from '@documenso/lib/server-only/stripe'; import { alphaid, nanoid } from '@documenso/lib/universal/id'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { prisma } from '@documenso/prisma'; import { DocumentStatus, @@ -74,7 +74,7 @@ export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersChecko new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url), ).then(async (res) => res.arrayBuffer()); - const { id: documentDataId } = await putFile({ + const { id: documentDataId } = await putPdfFile({ name: 'Documenso Supporter Pledge.pdf', type: 'application/pdf', arrayBuffer: async () => Promise.resolve(documentBuffer), diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts index 83137217f..9533f4d18 100644 --- a/packages/lib/constants/feature-flags.ts +++ b/packages/lib/constants/feature-flags.ts @@ -21,6 +21,7 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000; * Does not take any person or group properties into account. */ export const LOCAL_FEATURE_FLAGS: Record = { + app_allow_encrypted_documents: false, app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true', app_document_page_view_history_sheet: false, app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag. diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 564dfc049..0546d96e3 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -14,7 +14,7 @@ import { signPdf } from '@documenso/signing'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getFile } from '../../universal/upload/get-file'; -import { putFile } from '../../universal/upload/put-file'; +import { putPdfFile } from '../../universal/upload/put-file'; import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { flattenAnnotations } from '../pdf/flatten-annotations'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; @@ -122,7 +122,7 @@ export const sealDocument = async ({ const { name, ext } = path.parse(document.title); - const { data: newData } = await putFile({ + const { data: newData } = await putPdfFile({ name: `${name}_signed${ext}`, type: 'application/pdf', arrayBuffer: async () => Promise.resolve(pdfBuffer), diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 9f68ed29b..40cdb9ab5 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -8,6 +8,7 @@ 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 { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template'; import { prisma } from '@documenso/prisma'; @@ -20,7 +21,6 @@ import { RECIPIENT_ROLE_TO_EMAIL_TYPE, } from '../../constants/recipient-roles'; import { getFile } from '../../universal/upload/get-file'; -import { putFile } from '../../universal/upload/put-file'; import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -102,7 +102,7 @@ export const sendDocument = async ({ formValues: document.formValues as Record, }); - const newDocumentData = await putFile({ + const newDocumentData = await putPdfFile({ name: document.title, type: 'application/pdf', arrayBuffer: async () => Promise.resolve(prefilled), diff --git a/packages/lib/universal/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts index b6aab5f11..06ecd68ff 100644 --- a/packages/lib/universal/upload/put-file.ts +++ b/packages/lib/universal/upload/put-file.ts @@ -1,9 +1,12 @@ import { base64 } from '@scure/base'; import { env } from 'next-runtime-env'; +import { PDFDocument } from 'pdf-lib'; import { match } from 'ts-pattern'; +import { getFlag } from '@documenso/lib/universal/get-feature-flag'; import { DocumentDataType } from '@documenso/prisma/client'; +import { AppError } from '../../errors/app-error'; import { createDocumentData } from '../../server-only/document-data/create-document-data'; type File = { @@ -12,14 +15,38 @@ type File = { arrayBuffer: () => Promise; }; +/** + * Uploads a document file to the appropriate storage location and creates + * a document data record. + */ +export const putPdfFile = async (file: File) => { + const isEncryptedDocumentsAllowed = await getFlag('app_allow_encrypted_documents').catch( + () => false, + ); + + // This will prevent uploading encrypted PDFs or anything that can't be opened. + if (!isEncryptedDocumentsAllowed) { + await PDFDocument.load(await file.arrayBuffer()).catch((e) => { + console.error(`PDF upload parse error: ${e.message}`); + + throw new AppError('INVALID_DOCUMENT_FILE'); + }); + } + + const { type, data } = await putFile(file); + + return await createDocumentData({ type, data }); +}; + +/** + * Uploads a file to the appropriate storage location. + */ export const putFile = async (file: File) => { const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT'); - const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT) + return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT) .with('s3', async () => putFileInS3(file)) .otherwise(async () => putFileInDatabase(file)); - - return await createDocumentData({ type, data }); }; const putFileInDatabase = async (file: File) => { diff --git a/packages/trpc/server/singleplayer-router/router.ts b/packages/trpc/server/singleplayer-router/router.ts index 33b125110..2634ca895 100644 --- a/packages/trpc/server/singleplayer-router/router.ts +++ b/packages/trpc/server/singleplayer-router/router.ts @@ -10,7 +10,7 @@ import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/cons import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf'; import { alphaid } from '@documenso/lib/universal/id'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { prisma } from '@documenso/prisma'; import { DocumentStatus, @@ -86,7 +86,7 @@ export const singleplayerRouter = router({ }, }); - const { id: documentDataId } = await putFile({ + const { id: documentDataId } = await putPdfFile({ name: `${documentName}.pdf`, type: 'application/pdf', arrayBuffer: async () => Promise.resolve(signedPdfBuffer), From e8d4fe46e5af08ef5a04f8892364941a5d15f452 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 6 May 2024 09:22:50 +0300 Subject: [PATCH 2/8] fix: custom email message for self-signers (#1120) --- packages/lib/server-only/document/resend-document.tsx | 4 ++-- packages/lib/server-only/document/send-document.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index 500c5395a..c8a51cac4 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -110,7 +110,7 @@ export const resendDocument = async ({ assetBaseUrl, signDocumentLink, customBody: renderCustomEmailTemplate( - selfSigner ? selfSignerCustomEmail : customEmail?.message || '', + selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '', customEmailTemplate, ), role: recipient.role, @@ -135,7 +135,7 @@ export const resendDocument = async ({ address: FROM_ADDRESS, }, subject: customEmail?.subject - ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + ? renderCustomEmailTemplate(`Reminder: ${customEmail.subject}`, customEmailTemplate) : 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 40cdb9ab5..64ddb883d 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -151,7 +151,7 @@ export const sendDocument = async ({ assetBaseUrl, signDocumentLink, customBody: renderCustomEmailTemplate( - selfSigner ? selfSignerCustomEmail : customEmail?.message || '', + selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '', customEmailTemplate, ), role: recipient.role, From dc11676d28c4e94119481815ad69cc3317b95831 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 7 May 2024 09:42:16 +0200 Subject: [PATCH 3/8] fix: profile claim name length (#1144) fixes the caim name length on the profile claim popup --- packages/trpc/server/profile-router/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index eb5f54274..bc5fe757b 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -90,7 +90,7 @@ export const profileRouter = router({ try { const { url } = input; - if (IS_BILLING_ENABLED() && url.length <= 6) { + if (IS_BILLING_ENABLED() && url.length < 6) { const subscriptions = await getSubscriptionsByUserId({ userId: ctx.user.id, }).then((subscriptions) => From d7a3c400504ce266e8f1530b2696c0d18c29d70c Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 7 May 2024 15:04:12 +0700 Subject: [PATCH 4/8] feat: add general template enhancements (#1147) ## Description Refactor the "use template" flow ## Changes Made - Add placeholders for recipients - Add audit log when document is created - Trigger DOCUMENT_CREATED webhook when document is created - Remove role field when using template - Remove flaky logic when associating template recipients with form recipients - Refactor to use `Form` ### Using template when document has no recipients image ### Using template with recipients image ### Using template with the send option selected image --- .../templates/[id]/template-page-view.tsx | 2 +- .../templates/use-template-dialog.tsx | 321 ++++++++++-------- packages/api/v1/implementation.ts | 4 +- .../e2e/templates/manage-templates.spec.ts | 18 +- packages/lib/constants/template.ts | 1 + packages/lib/errors/app-error.ts | 21 ++ .../create-document-from-template-legacy.ts | 144 ++++++++ .../template/create-document-from-template.ts | 195 ++++++----- .../trpc/server/template-router/router.ts | 29 +- .../trpc/server/template-router/schema.ts | 12 +- .../add-template-placeholder-recipients.tsx | 1 + 11 files changed, 512 insertions(+), 236 deletions(-) create mode 100644 packages/lib/constants/template.ts create mode 100644 packages/lib/server-only/template/create-document-from-template-legacy.ts diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx index 899e600f1..7bb9640c5 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx @@ -58,7 +58,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps) ]); return ( -
+
Templates diff --git a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx index e4c703a2f..7000e795a 100644 --- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx @@ -1,14 +1,16 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Plus } from 'lucide-react'; -import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { InfoIcon, Plus } from 'lucide-react'; +import { useFieldArray, useForm } from 'react-hook-form'; import * as z from 'zod'; +import { TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX } from '@documenso/lib/constants/template'; +import { AppError } from '@documenso/lib/errors/app-error'; import type { Recipient } from '@documenso/prisma/client'; -import { RecipientRole } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; +import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Dialog, DialogClose, @@ -19,24 +21,59 @@ import { DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; -import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; -import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons'; -import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; +import type { Toast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useOptionalCurrentTeam } from '~/providers/team'; -const ZAddRecipientsForNewDocumentSchema = z.object({ - recipients: z.array( - z.object({ - email: z.string().email(), - name: z.string(), - role: z.nativeEnum(RecipientRole), - }), - ), -}); +const ZAddRecipientsForNewDocumentSchema = z + .object({ + sendDocument: z.boolean(), + recipients: z.array( + z.object({ + id: z.number(), + email: z.string().email(), + name: z.string(), + }), + ), + }) + // Display exactly which rows are duplicates. + .superRefine((items, ctx) => { + const uniqueEmails = new Map(); + + for (const [index, recipients] of items.recipients.entries()) { + const email = recipients.email.toLowerCase(); + + const firstFoundIndex = uniqueEmails.get(email); + + if (firstFoundIndex === undefined) { + uniqueEmails.set(email, index); + continue; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Emails must be unique', + path: ['recipients', index, 'email'], + }); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Emails must be unique', + path: ['recipients', firstFoundIndex, 'email'], + }); + } + }); type TAddRecipientsForNewDocumentSchema = z.infer; @@ -56,33 +93,31 @@ export function UseTemplateDialog({ const team = useOptionalCurrentTeam(); - const { - control, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), defaultValues: { - recipients: - recipients.length > 0 - ? recipients.map((recipient) => ({ - nativeId: recipient.id, - formId: String(recipient.id), - name: recipient.name, - email: recipient.email, - role: recipient.role, - })) - : [ - { - name: '', - email: '', - role: RecipientRole.SIGNER, - }, - ], + sendDocument: false, + recipients: recipients.map((recipient) => { + const isRecipientPlaceholder = recipient.email.match(TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX); + + if (isRecipientPlaceholder) { + return { + id: recipient.id, + name: '', + email: '', + }; + } + + return { + id: recipient.id, + name: recipient.name, + email: recipient.email, + }; + }), }, }); - const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } = + const { mutateAsync: createDocumentFromTemplate } = trpc.template.createDocumentFromTemplate.useMutation(); const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { @@ -91,6 +126,7 @@ export function UseTemplateDialog({ templateId, teamId: team?.id, recipients: data.recipients, + sendDocument: data.sendDocument, }); toast({ @@ -101,18 +137,24 @@ export function UseTemplateDialog({ router.push(`${documentRootPath}/${id}`); } catch (err) { - toast({ + const error = AppError.parseError(err); + + const toastPayload: Toast = { title: 'Error', description: 'An error occurred while creating document from template.', variant: 'destructive', - }); + }; + + if (error.code === 'DOCUMENT_SEND_FAILED') { + toastPayload.description = 'The document was created but could not be sent to recipients.'; + } + + toast(toastPayload); } }; - const onCreateDocumentFromTemplate = handleSubmit(onSubmit); - const { fields: formRecipients } = useFieldArray({ - control, + control: form.control, name: 'recipients', }); @@ -126,121 +168,110 @@ export function UseTemplateDialog({ - Document Recipients - Add the recipients to create the template with. + Create document from template + + {recipients.length === 0 + ? 'A draft document will be created' + : 'Add the recipients to create the document with'} + -
- {formRecipients.map((recipient, index) => ( -
-
- - ( - +
+
+
+ {formRecipients.map((recipient, index) => ( +
+ ( + + {index === 0 && Email} + + + + + + + )} /> - )} - /> -
-
- + ( + + {index === 0 && Name} - ( - + + + + + )} /> - )} - /> +
+ ))}
-
- ( - - )} - /> -
+ + + + -
- - -
-
- ))} -
- - - - - - - - + + + + + ); 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 5/8] 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 6/8] 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 7/8] 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 cc4efddabf8f20dffcc89d3cf8de33a8a1fdd38d Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 8 May 2024 17:03:57 +0530 Subject: [PATCH 8/8] 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"] })