From a2ef9468aeb221e054c5f63823fee4b1a99b38a3 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 7 Sep 2023 19:27:21 +1000 Subject: [PATCH 01/32] feat: separate document data from document --- .../src/pages/api/stripe/webhook/index.ts | 34 +++++++++--- .../documents/[id]/edit-document.tsx | 9 ++-- .../app/(dashboard)/documents/[id]/page.tsx | 6 ++- .../documents/data-table-action-dropdown.tsx | 52 +++++++++++++++---- .../(signing)/sign/[token]/complete/page.tsx | 6 ++- .../src/app/(signing)/sign/[token]/page.tsx | 6 ++- apps/web/src/pages/api/document/create.ts | 12 ++++- .../web/src/pages/api/stripe/webhook/index.ts | 34 +++++++++--- packages/lib/package.json | 2 + .../server-only/document/create-document.ts | 10 ++++ .../document/get-document-by-id.ts | 3 ++ .../document/get-document-by-token.ts | 1 + .../lib/server-only/document/seal-document.ts | 15 +++++- .../migration.sql | 19 +++++++ .../migration.sql | 14 +++++ .../migration.sql | 3 ++ .../migration.sql | 8 +++ packages/prisma/schema.prisma | 38 ++++++++++---- packages/prisma/types/document-with-data.ts | 5 ++ .../trpc/server/document-router/router.ts | 48 ++++++++++++++++- .../trpc/server/document-router/schema.ts | 12 +++++ packages/tsconfig/process-env.d.ts | 7 +++ 22 files changed, 300 insertions(+), 44 deletions(-) create mode 100644 packages/lib/server-only/document/create-document.ts create mode 100644 packages/prisma/migrations/20230907041233_add_document_data_table/migration.sql create mode 100644 packages/prisma/migrations/20230907074451_insert_old_data_into_document_data_table/migration.sql create mode 100644 packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql create mode 100644 packages/prisma/migrations/20230907082622_remove_old_document_data/migration.sql create mode 100644 packages/prisma/types/document-with-data.ts diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index 3f3810fd4..11c9476bd 100644 --- a/apps/marketing/src/pages/api/stripe/webhook/index.ts +++ b/apps/marketing/src/pages/api/stripe/webhook/index.ts @@ -10,6 +10,7 @@ import { redis } from '@documenso/lib/server-only/redis'; import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; import { prisma } from '@documenso/prisma'; import { + DocumentDataType, DocumentStatus, FieldType, ReadStatus, @@ -85,16 +86,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const now = new Date(); + const bytes64 = readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'); + const document = await prisma.document.create({ data: { title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, - document: readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'), created: now, + documentData: { + create: { + type: DocumentDataType.BYTES_64, + data: bytes64, + initialData: bytes64, + }, + }, + }, + include: { + documentData: true, }, }); + const { documentData } = document; + + if (!documentData) { + throw new Error(`Document ${document.id} has no document data`); + } + const recipient = await prisma.recipient.create({ data: { name: user.name ?? '', @@ -122,16 +140,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (signatureDataUrl) { - document.document = await insertImageInPDF( - document.document, + documentData.data = await insertImageInPDF( + documentData.data, signatureDataUrl, Number(field.positionX), Number(field.positionY), field.page, ); } else { - document.document = await insertTextInPDF( - document.document, + documentData.data = await insertTextInPDF( + documentData.data, signatureText ?? '', Number(field.positionX), Number(field.positionY), @@ -153,7 +171,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: document.id, }, data: { - document: document.document, + documentData: { + update: { + data: documentData.data, + }, + }, }, }), ]); 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 7ed28feca..653936b9a 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -4,7 +4,8 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Document, Field, Recipient, User } from '@documenso/prisma/client'; +import { Field, Recipient, User } from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields'; @@ -28,7 +29,7 @@ import { completeDocument } from '~/components/forms/edit-document/add-subject.a export type EditDocumentFormProps = { className?: string; user: User; - document: Document; + document: DocumentWithData; recipients: Recipient[]; fields: Field[]; }; @@ -45,9 +46,11 @@ export const EditDocumentForm = ({ const { toast } = useToast(); const router = useRouter(); + const { documentData } = document; + const [step, setStep] = useState('signers'); - const documentUrl = `data:application/pdf;base64,${document.document}`; + const documentUrl = `data:application/pdf;base64,${documentData?.data}`; const documentFlow: Record = { signers: { diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index 7ab2c331c..f7c8f2525 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -36,10 +36,12 @@ export default async function DocumentPage({ params }: DocumentPageProps) { userId: session.id, }).catch(() => null); - if (!document) { + if (!document || !document.documentData) { redirect('/documents'); } + const { documentData } = document; + const [recipients, fields] = await Promise.all([ await getRecipientsForDocument({ documentId, @@ -91,7 +93,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { {document.status === InternalDocumentStatus.COMPLETED && (
- +
)} 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 8b69f95c4..02e3ad95c 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 @@ -14,8 +14,17 @@ import { XCircle, } from 'lucide-react'; import { useSession } from 'next-auth/react'; +import { match } from 'ts-pattern'; -import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; +import { + Document, + DocumentDataType, + DocumentStatus, + Recipient, + User, +} from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { trpc } from '@documenso/trpc/client'; import { DropdownMenu, DropdownMenuContent, @@ -47,17 +56,42 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = const isComplete = row.status === DocumentStatus.COMPLETED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; - const onDownloadClick = () => { - let decodedDocument = row.document; + const onDownloadClick = async () => { + let document: DocumentWithData | null = null; - try { - decodedDocument = atob(decodedDocument); - } catch (err) { - // We're just going to ignore this error and try to download the document - console.error(err); + if (!recipient) { + document = await trpc.document.getDocumentById.query({ + id: row.id, + }); + } else { + document = await trpc.document.getDocumentByToken.query({ + token: recipient.token, + }); } - const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0))); + const documentData = document?.documentData; + + if (!documentData) { + return; + } + + const documentBytes = await match(documentData.type) + .with(DocumentDataType.BYTES, () => + Uint8Array.from(documentData.data, (c) => c.charCodeAt(0)), + ) + .with(DocumentDataType.BYTES_64, () => + Uint8Array.from( + atob(documentData.data) + .split('') + .map((c) => c.charCodeAt(0)), + ), + ) + .with(DocumentDataType.S3_PATH, async () => + fetch(documentData.data) + .then(async (res) => res.arrayBuffer()) + .then((buffer) => new Uint8Array(buffer)), + ) + .exhaustive(); const blob = new Blob([documentBytes], { type: 'application/pdf', diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index b97b7f8d6..48d2b6435 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -30,10 +30,12 @@ export default async function CompletedSigningPage({ token, }).catch(() => null); - if (!document) { + if (!document || !document.documentData) { return notFound(); } + const { documentData } = document; + const [fields, recipient] = await Promise.all([ getFieldsForToken({ token }), getRecipientByToken({ token }), @@ -91,7 +93,7 @@ export default async function CompletedSigningPage({ diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 35621068a..838e3ee32 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -40,13 +40,15 @@ export default async function SigningPage({ params: { token } }: SigningPageProp viewedDocument({ token }), ]); - if (!document) { + if (!document || !document.documentData) { return notFound(); } + const { documentData } = document; + const user = await getServerComponentSession(); - const documentUrl = `data:application/pdf;base64,${document.document}`; + const documentUrl = `data:application/pdf;base64,${documentData.data}`; return ( diff --git a/apps/web/src/pages/api/document/create.ts b/apps/web/src/pages/api/document/create.ts index b2042315f..897c16f76 100644 --- a/apps/web/src/pages/api/document/create.ts +++ b/apps/web/src/pages/api/document/create.ts @@ -5,7 +5,7 @@ import { readFileSync } from 'fs'; import { getServerSession } from '@documenso/lib/next-auth/get-server-session'; import { prisma } from '@documenso/prisma'; -import { DocumentStatus } from '@documenso/prisma/client'; +import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client'; import { TCreateDocumentRequestSchema, @@ -55,12 +55,20 @@ export default async function handler( const fileBuffer = readFileSync(file.filepath); + const bytes64 = fileBuffer.toString('base64'); + const document = await prisma.document.create({ data: { title: file.originalFilename ?? file.newFilename, status: DocumentStatus.DRAFT, userId: user.id, - document: fileBuffer.toString('base64'), + documentData: { + create: { + type: DocumentDataType.BYTES_64, + data: bytes64, + initialData: bytes64, + }, + }, created: new Date(), }, }); diff --git a/apps/web/src/pages/api/stripe/webhook/index.ts b/apps/web/src/pages/api/stripe/webhook/index.ts index 6c678a33c..818b3759a 100644 --- a/apps/web/src/pages/api/stripe/webhook/index.ts +++ b/apps/web/src/pages/api/stripe/webhook/index.ts @@ -10,6 +10,7 @@ import { redis } from '@documenso/lib/server-only/redis'; import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; import { prisma } from '@documenso/prisma'; import { + DocumentDataType, DocumentStatus, FieldType, ReadStatus, @@ -85,16 +86,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const now = new Date(); + const bytes64 = readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'); + const document = await prisma.document.create({ data: { title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, - document: readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'), created: now, + documentData: { + create: { + type: DocumentDataType.BYTES_64, + data: bytes64, + initialData: bytes64, + }, + }, + }, + include: { + documentData: true, }, }); + const { documentData } = document; + + if (!documentData) { + throw new Error(`Document ${document.id} has no document data`); + } + const recipient = await prisma.recipient.create({ data: { name: user.name ?? '', @@ -122,16 +140,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (signatureDataUrl) { - document.document = await insertImageInPDF( - document.document, + documentData.data = await insertImageInPDF( + documentData.data, signatureDataUrl, field.positionX.toNumber(), field.positionY.toNumber(), field.page, ); } else { - document.document = await insertTextInPDF( - document.document, + documentData.data = await insertTextInPDF( + documentData.data, signatureText ?? '', field.positionX.toNumber(), field.positionY.toNumber(), @@ -153,7 +171,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: document.id, }, data: { - document: document.document, + documentData: { + update: { + data: documentData.data, + }, + }, }, }), ]); diff --git a/packages/lib/package.json b/packages/lib/package.json index 0d04f6c93..e36297834 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -12,6 +12,8 @@ ], "scripts": {}, "dependencies": { + "@aws-sdk/s3-request-presigner": "^3.405.0", + "@aws-sdk/client-s3": "^3.405.0", "@documenso/email": "*", "@documenso/prisma": "*", "@next-auth/prisma-adapter": "1.0.7", diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts new file mode 100644 index 000000000..24a5d6283 --- /dev/null +++ b/packages/lib/server-only/document/create-document.ts @@ -0,0 +1,10 @@ +'use server'; + +export type CreateDocumentOptions = { + userId: number; + fileName: string; +}; + +export const createDocument = () => { + // +}; diff --git a/packages/lib/server-only/document/get-document-by-id.ts b/packages/lib/server-only/document/get-document-by-id.ts index 12b0d03f9..0fce1af4d 100644 --- a/packages/lib/server-only/document/get-document-by-id.ts +++ b/packages/lib/server-only/document/get-document-by-id.ts @@ -11,5 +11,8 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) => id, userId, }, + include: { + documentData: true, + }, }); }; diff --git a/packages/lib/server-only/document/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts index 74bc30c79..62b3ddd48 100644 --- a/packages/lib/server-only/document/get-document-by-token.ts +++ b/packages/lib/server-only/document/get-document-by-token.ts @@ -17,6 +17,7 @@ export const getDocumentAndSenderByToken = async ({ }, include: { User: true, + documentData: true, }, }); diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 1a74cfaac..876da9d0a 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -18,8 +18,15 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { where: { id: documentId, }, + include: { + documentData: true, + }, }); + if (!document.documentData) { + throw new Error(`Document ${document.id} has no document data`); + } + if (document.status !== DocumentStatus.COMPLETED) { throw new Error(`Document ${document.id} has not been completed`); } @@ -48,7 +55,7 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { } // !: Need to write the fields onto the document as a hard copy - const { document: pdfData } = document; + const { data: pdfData } = document.documentData; const doc = await PDFDocument.load(pdfData); @@ -64,7 +71,11 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { status: DocumentStatus.COMPLETED, }, data: { - document: Buffer.from(pdfBytes).toString('base64'), + documentData: { + update: { + data: Buffer.from(pdfBytes).toString('base64'), + }, + }, }, }); }; diff --git a/packages/prisma/migrations/20230907041233_add_document_data_table/migration.sql b/packages/prisma/migrations/20230907041233_add_document_data_table/migration.sql new file mode 100644 index 000000000..f2c69c4ed --- /dev/null +++ b/packages/prisma/migrations/20230907041233_add_document_data_table/migration.sql @@ -0,0 +1,19 @@ +-- CreateEnum +CREATE TYPE "DocumentDataType" AS ENUM ('S3_PATH', 'BYTES', 'BYTES_64'); + +-- CreateTable +CREATE TABLE "DocumentData" ( + "id" TEXT NOT NULL, + "type" "DocumentDataType" NOT NULL, + "data" TEXT NOT NULL, + "initialData" TEXT NOT NULL, + "documentId" INTEGER NOT NULL, + + CONSTRAINT "DocumentData_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DocumentData_documentId_key" ON "DocumentData"("documentId"); + +-- AddForeignKey +ALTER TABLE "DocumentData" ADD CONSTRAINT "DocumentData_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230907074451_insert_old_data_into_document_data_table/migration.sql b/packages/prisma/migrations/20230907074451_insert_old_data_into_document_data_table/migration.sql new file mode 100644 index 000000000..899c6e2d2 --- /dev/null +++ b/packages/prisma/migrations/20230907074451_insert_old_data_into_document_data_table/migration.sql @@ -0,0 +1,14 @@ +INSERT INTO + "DocumentData" ("id", "type", "data", "initialData", "documentId") ( + SELECT + CAST(gen_random_uuid() AS TEXT), + 'BYTES_64', + d."document", + d."document", + d."id" + FROM + "Document" d + WHERE + d."id" IS NOT NULL + AND d."document" IS NOT NULL + ); diff --git a/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql new file mode 100644 index 000000000..333386230 --- /dev/null +++ b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/prisma/migrations/20230907082622_remove_old_document_data/migration.sql b/packages/prisma/migrations/20230907082622_remove_old_document_data/migration.sql new file mode 100644 index 000000000..25c794f65 --- /dev/null +++ b/packages/prisma/migrations/20230907082622_remove_old_document_data/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `document` on the `Document` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "document"; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 2e016f5ec..5171813c3 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -85,15 +85,35 @@ enum DocumentStatus { } model Document { - id Int @id @default(autoincrement()) - created DateTime @default(now()) - userId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - title String - status DocumentStatus @default(DRAFT) - document String - Recipient Recipient[] - Field Field[] + id Int @id @default(autoincrement()) + created DateTime @default(now()) + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String + status DocumentStatus @default(DRAFT) + Recipient Recipient[] + Field Field[] + documentData DocumentData? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) +} + +enum DocumentDataType { + S3_PATH + BYTES + BYTES_64 +} + +model DocumentData { + id String @id @default(cuid()) + type DocumentDataType + data String + initialData String + documentId Int + + Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + + @@unique([documentId]) } enum ReadStatus { diff --git a/packages/prisma/types/document-with-data.ts b/packages/prisma/types/document-with-data.ts new file mode 100644 index 000000000..d52987552 --- /dev/null +++ b/packages/prisma/types/document-with-data.ts @@ -0,0 +1,5 @@ +import { Document, DocumentData } from '@documenso/prisma/client'; + +export type DocumentWithData = Document & { + documentData?: DocumentData | null; +}; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index f20643327..5628bb41d 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,17 +1,63 @@ import { TRPCError } from '@trpc/server'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; -import { authenticatedProcedure, router } from '../trpc'; +import { authenticatedProcedure, procedure, router } from '../trpc'; import { + ZGetDocumentByIdQuerySchema, + ZGetDocumentByTokenQuerySchema, ZSendDocumentMutationSchema, ZSetFieldsForDocumentMutationSchema, ZSetRecipientsForDocumentMutationSchema, } from './schema'; export const documentRouter = router({ + getDocumentById: authenticatedProcedure + .input(ZGetDocumentByIdQuerySchema) + .query(async ({ input, ctx }) => { + try { + const { id } = input; + + console.log({ + id, + userId: ctx.user.id, + }); + + return await getDocumentById({ + id, + userId: ctx.user.id, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to find this document. Please try again later.', + }); + } + }), + + getDocumentByToken: procedure.input(ZGetDocumentByTokenQuerySchema).query(async ({ input }) => { + try { + const { token } = input; + + return await getDocumentAndSenderByToken({ + token, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to find this document. Please try again later.', + }); + } + }), + setRecipientsForDocument: authenticatedProcedure .input(ZSetRecipientsForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 18c3a93ae..9060ef1db 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -2,6 +2,18 @@ import { z } from 'zod'; import { FieldType } from '@documenso/prisma/client'; +export const ZGetDocumentByIdQuerySchema = z.object({ + id: z.number().min(1), +}); + +export type TGetDocumentByIdQuerySchema = z.infer; + +export const ZGetDocumentByTokenQuerySchema = z.object({ + token: z.string().min(1), +}); + +export type TGetDocumentByTokenQuerySchema = z.infer; + export const ZSetRecipientsForDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array( diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 6c031858d..b65a2bb20 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -13,6 +13,13 @@ declare namespace NodeJS { NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; + NEXT_PRIVATE_UPLOAD_TRANSPORT?: 'database' | 's3'; + NEXT_PRIVATE_UPLOAD_ENDPOINT?: string; + NEXT_PRIVATE_UPLOAD_REGION?: string; + NEXT_PRIVATE_UPLOAD_BUCKET?: string; + NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID?: string; + NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY?: string; + NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api'; NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string; From 171a5ba4ee5a167d5fb225b29d12908c808003c9 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 09:16:31 +0300 Subject: [PATCH 02/32] feat: creating the admin ui for metrics --- .../(dashboard)/layout/profile-dropdown.tsx | 13 ++++++++++++- packages/lib/index.ts | 6 +++++- .../20230907075057_user_roles/migration.sql | 5 +++++ packages/prisma/schema.prisma | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 packages/prisma/migrations/20230907075057_user_roles/migration.sql diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 02af86d70..19a15564b 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -11,10 +11,12 @@ import { Monitor, Moon, Sun, + UserCog, } from 'lucide-react'; import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { isAdmin } from '@documenso/lib/'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -35,8 +37,8 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); - const { getFlag } = useFeatureFlags(); + const userIsAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -67,6 +69,15 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { + {userIsAdmin && ( + + + + Admin + + + )} + diff --git a/packages/lib/index.ts b/packages/lib/index.ts index cb0ff5c3b..2801305dd 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1 +1,5 @@ -export {}; +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; diff --git a/packages/prisma/migrations/20230907075057_user_roles/migration.sql b/packages/prisma/migrations/20230907075057_user_roles/migration.sql new file mode 100644 index 000000000..f47e48361 --- /dev/null +++ b/packages/prisma/migrations/20230907075057_user_roles/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "roles" "Role"[] DEFAULT ARRAY['USER']::"Role"[]; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 2e016f5ec..22955310b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -13,6 +13,11 @@ enum IdentityProvider { GOOGLE } +enum Role { + ADMIN + USER +} + model User { id Int @id @default(autoincrement()) name String? @@ -21,6 +26,7 @@ model User { password String? source String? signature String? + roles Role[] @default([USER]) identityProvider IdentityProvider @default(DOCUMENSO) accounts Account[] sessions Session[] From 67571158e88912620e2aa33c537a91ea7da6443f Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 11:28:50 +0300 Subject: [PATCH 03/32] feat: add the admin page --- apps/web/src/app/(dashboard)/admin/layout.tsx | 23 ++++ apps/web/src/app/(dashboard)/admin/page.tsx | 107 ++++++++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 4 +- .../lib/server-only/admin/get-documents.ts | 5 + .../lib/server-only/admin/get-recipients.ts | 20 ++++ packages/lib/server-only/admin/get-users.ts | 18 +++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/layout.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/page.tsx create mode 100644 packages/lib/server-only/admin/get-documents.ts create mode 100644 packages/lib/server-only/admin/get-recipients.ts create mode 100644 packages/lib/server-only/admin/get-users.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx new file mode 100644 index 000000000..340605bc7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; + +import { isAdmin } from '@documenso/lib/'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; + +export type AdminLayoutProps = { + children: React.ReactNode; +}; + +export default async function AdminLayout({ children }: AdminLayoutProps) { + const user = await getRequiredServerComponentSession(); + const isUserAdmin = isAdmin(user); + + if (!user) { + redirect('/signin'); + } + + if (!isUserAdmin) { + redirect('/dashboard'); + } + + return
{children}
; +} diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx new file mode 100644 index 000000000..e4a62f725 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -0,0 +1,107 @@ +import { + Archive, + File, + FileX2, + LucideIcon, + User as LucideUser, + Mail, + MailOpen, + PenTool, + Send, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users'; +import { + ReadStatus as InternalReadStatus, + SendStatus as InternalSendStatus, + SigningStatus as InternalSigningStatus, +} from '@documenso/prisma/client'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +type TCardData = { + icon: LucideIcon; + title: string; + status: + | 'TOTAL_RECIPIENTS' + | 'OPENED' + | 'NOT_OPENED' + | 'SIGNED' + | 'NOT_SIGNED' + | 'SENT' + | 'NOT_SENT'; +}[]; + +const CARD_DATA: TCardData = [ + { + icon: UserSquare2, + title: 'Total recipients in the database', + status: 'TOTAL_RECIPIENTS', + }, + { + icon: MailOpen, + title: 'Total recipients with opened count', + status: InternalReadStatus.OPENED, + }, + { + icon: Mail, + title: 'Total recipients with unopened count', + status: InternalReadStatus.NOT_OPENED, + }, + { + icon: Send, + title: 'Total recipients with sent count', + status: InternalSendStatus.SENT, + }, + { + icon: Archive, + title: 'Total recipients with unsent count', + status: InternalSendStatus.NOT_SENT, + }, + { + icon: PenTool, + title: 'Total recipients with signed count', + status: InternalSigningStatus.SIGNED, + }, + { + icon: FileX2, + title: 'Total recipients with unsigned count', + status: InternalSigningStatus.NOT_SIGNED, + }, +]; + +export default async function Admin() { + const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocsCount(), + getRecipientsStats(), + ]); + + return ( +
+

Documenso instance metrics

+
+ + + + {CARD_DATA.map((card) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 19a15564b..0bea64565 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -38,7 +38,7 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); const { getFlag } = useFeatureFlags(); - const userIsAdmin = isAdmin(user); + const isUserAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -69,7 +69,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
- {userIsAdmin && ( + {isUserAdmin && ( diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents.ts new file mode 100644 index 000000000..9100a886c --- /dev/null +++ b/packages/lib/server-only/admin/get-documents.ts @@ -0,0 +1,5 @@ +import { prisma } from '@documenso/prisma'; + +export const getDocsCount = async () => { + return await prisma.document.count(); +}; diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts new file mode 100644 index 000000000..0be612e55 --- /dev/null +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -0,0 +1,20 @@ +import { prisma } from '@documenso/prisma'; +import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client'; + +export const getRecipientsStats = async () => { + const results = await prisma.recipient.groupBy({ + by: ['readStatus', 'signingStatus', 'sendStatus'], + _count: true, + }); + + return { + TOTAL_RECIPIENTS: results.length, + [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, + [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, + [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, + [SigningStatus.NOT_SIGNED]: + results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, + [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, + [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + }; +}; diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users.ts new file mode 100644 index 000000000..09892171a --- /dev/null +++ b/packages/lib/server-only/admin/get-users.ts @@ -0,0 +1,18 @@ +import { prisma } from '@documenso/prisma'; +import { SubscriptionStatus } from '@documenso/prisma/client'; + +export const getUsersCount = async () => { + return await prisma.user.count(); +}; + +export const getUsersWithSubscriptionsCount = async () => { + return await prisma.user.count({ + where: { + Subscription: { + some: { + status: SubscriptionStatus.ACTIVE, + }, + }, + }, + }); +}; From 6cdba45396299cc1e06a7d185280acaddda0fb59 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:39:13 +0300 Subject: [PATCH 04/32] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 4 +-- .../lib/server-only/admin/get-recipients.ts | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e4a62f725..e72d35dc3 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -37,9 +37,9 @@ type TCardData = { | 'NOT_SIGNED' | 'SENT' | 'NOT_SENT'; -}[]; +}; -const CARD_DATA: TCardData = [ +const CARD_DATA: TCardData[] = [ { icon: UserSquare2, title: 'Total recipients in the database', diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts index 0be612e55..92c0c3527 100644 --- a/packages/lib/server-only/admin/get-recipients.ts +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -7,14 +7,21 @@ export const getRecipientsStats = async () => { _count: true, }); - return { - TOTAL_RECIPIENTS: results.length, - [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, - [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, - [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, - [SigningStatus.NOT_SIGNED]: - results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, - [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, - [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + const stats = { + TOTAL_RECIPIENTS: 0, + [ReadStatus.OPENED]: 0, + [ReadStatus.NOT_OPENED]: 0, + [SigningStatus.SIGNED]: 0, + [SigningStatus.NOT_SIGNED]: 0, + [SendStatus.SENT]: 0, + [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { + const { readStatus, signingStatus, sendStatus, _count } = result; + stats[readStatus] += _count; + stats[signingStatus] += _count; + stats[sendStatus] += _count; + stats.TOTAL_RECIPIENTS += _count; + }); + return stats; }; From 77058220a8975f01d4b9fda48c29bc6089a3bef0 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:42:14 +0300 Subject: [PATCH 05/32] chore: rename files --- apps/web/src/app/(dashboard)/admin/page.tsx | 6 +++--- .../admin/{get-documents.ts => get-documents-stats.ts} | 0 .../admin/{get-recipients.ts => get-recipients-stats.ts} | 0 .../server-only/admin/{get-users.ts => get-users-stats.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/lib/server-only/admin/{get-documents.ts => get-documents-stats.ts} (100%) rename packages/lib/server-only/admin/{get-recipients.ts => get-recipients-stats.ts} (100%) rename packages/lib/server-only/admin/{get-users.ts => get-users-stats.ts} (100%) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e72d35dc3..aabdbfa35 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -12,12 +12,12 @@ import { UserSquare2, } from 'lucide-react'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { getUsersCount, getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users'; +} from '@documenso/lib/server-only/admin/get-users-stats'; import { ReadStatus as InternalReadStatus, SendStatus as InternalSendStatus, diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-documents.ts rename to packages/lib/server-only/admin/get-documents-stats.ts diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-recipients.ts rename to packages/lib/server-only/admin/get-recipients-stats.ts diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-users.ts rename to packages/lib/server-only/admin/get-users-stats.ts From 660f5894a6f66baa5fd9394efec21306e640dc7b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:56:44 +0300 Subject: [PATCH 06/32] chore: feedback improvements --- apps/web/src/app/(dashboard)/admin/layout.tsx | 2 +- .../src/components/(dashboard)/layout/profile-dropdown.tsx | 2 +- packages/lib/index.ts | 6 +----- packages/lib/next-auth/guards/is-admin.ts | 5 +++++ 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 packages/lib/next-auth/guards/is-admin.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 340605bc7..a221d92ba 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; -import { isAdmin } from '@documenso/lib/'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; export type AdminLayoutProps = { children: React.ReactNode; diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 0bea64565..e3fd4c6d6 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -16,7 +16,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; -import { isAdmin } from '@documenso/lib/'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 2801305dd..cb0ff5c3b 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1,5 +1 @@ -import { Role, User } from '@documenso/prisma/client'; - -const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); - -export { isAdmin }; +export {}; diff --git a/packages/lib/next-auth/guards/is-admin.ts b/packages/lib/next-auth/guards/is-admin.ts new file mode 100644 index 000000000..2801305dd --- /dev/null +++ b/packages/lib/next-auth/guards/is-admin.ts @@ -0,0 +1,5 @@ +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; From 5969f148c861bbc1d3e05a68cf359542bdff481c Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 14:51:55 +0300 Subject: [PATCH 07/32] chore: changed the cards titles --- apps/web/src/app/(dashboard)/admin/page.tsx | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index aabdbfa35..fdb54dc07 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -42,37 +42,37 @@ type TCardData = { const CARD_DATA: TCardData[] = [ { icon: UserSquare2, - title: 'Total recipients in the database', + title: 'Recipients in the database', status: 'TOTAL_RECIPIENTS', }, { icon: MailOpen, - title: 'Total recipients with opened count', + title: 'Opened documents', status: InternalReadStatus.OPENED, }, { icon: Mail, - title: 'Total recipients with unopened count', + title: 'Unopened documents', status: InternalReadStatus.NOT_OPENED, }, { icon: Send, - title: 'Total recipients with sent count', + title: 'Sent documents', status: InternalSendStatus.SENT, }, { icon: Archive, - title: 'Total recipients with unsent count', + title: 'Unsent documents', status: InternalSendStatus.NOT_SENT, }, { icon: PenTool, - title: 'Total recipients with signed count', + title: 'Signed documents', status: InternalSigningStatus.SIGNED, }, { icon: FileX2, - title: 'Total recipients with unsigned count', + title: 'Unsigned documents', status: InternalSigningStatus.NOT_SIGNED, }, ]; @@ -87,15 +87,22 @@ export default async function Admin() { return (
-

Documenso instance metrics

-
+

Instance metrics

+
+
+

Document metrics

+
+
+ +

Recipients metrics

+
{CARD_DATA.map((card) => (
From fbf32404a6b859355e5a189d98756fc9da2a466f Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 11 Sep 2023 16:58:41 +1000 Subject: [PATCH 08/32] feat: add avatar email fallback --- .../src/components/(dashboard)/avatar/stack-avatar.tsx | 4 ++-- .../(dashboard)/avatar/stack-avatars-with-tooltip.tsx | 10 +++++----- .../components/(dashboard)/avatar/stack-avatars.tsx | 4 ++-- .../components/(dashboard)/layout/profile-dropdown.tsx | 10 +++------- packages/lib/client-only/recipient-avatar-fallback.ts | 7 +++++++ packages/lib/client-only/recipient-initials.ts | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 packages/lib/client-only/recipient-avatar-fallback.ts diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx index 78814cd45..a2a81bb2a 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx @@ -15,7 +15,7 @@ export type StackAvatarProps = { type: 'unsigned' | 'waiting' | 'opened' | 'completed'; }; -export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarProps) => { +export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAvatarProps) => { let classes = ''; let zIndexClass = ''; const firstClass = first ? '' : '-ml-3'; @@ -48,7 +48,7 @@ export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarPr ${firstClass} dark:border-border h-10 w-10 border-2 border-solid border-white`} > - {fallbackText ?? 'UK'} + {fallbackText} ); }; 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 2a053a35a..3f6407029 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,4 +1,4 @@ -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { Recipient } from '@documenso/prisma/client'; import { @@ -56,7 +56,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -73,7 +73,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -90,7 +90,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -107,7 +107,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx index 97af9dc9e..678836ffd 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { Recipient } from '@documenso/prisma/client'; @@ -26,7 +26,7 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) { first={first} zIndex={String(zIndex - index * 10)} type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)} - fallbackText={lastItemText ? lastItemText : initials(recipient.name)} + fallbackText={lastItemText ? lastItemText : recipientAvatarFallback(recipient)} /> ); }); diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 02af86d70..e52d9b42f 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -15,6 +15,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { initials } from '@documenso/lib/client-only/recipient-initials'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -40,19 +41,14 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const isBillingEnabled = getFlag('app_billing'); - const initials = - user.name - ?.split(' ') - .map((name: string) => name.slice(0, 1).toUpperCase()) - .slice(0, 2) - .join('') ?? 'UK'; + const avatarFallback = user.name ? initials(user.name) : user.email.slice(0, 1).toUpperCase(); return ( diff --git a/packages/lib/client-only/recipient-avatar-fallback.ts b/packages/lib/client-only/recipient-avatar-fallback.ts new file mode 100644 index 000000000..7a296a5fa --- /dev/null +++ b/packages/lib/client-only/recipient-avatar-fallback.ts @@ -0,0 +1,7 @@ +import { Recipient } from '@documenso/prisma/client'; + +import { initials } from './recipient-initials'; + +export const recipientAvatarFallback = (recipient: Recipient) => { + return initials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); +}; diff --git a/packages/lib/client-only/recipient-initials.ts b/packages/lib/client-only/recipient-initials.ts index 0712ccd7d..403ed26e4 100644 --- a/packages/lib/client-only/recipient-initials.ts +++ b/packages/lib/client-only/recipient-initials.ts @@ -3,4 +3,4 @@ export const initials = (text: string) => ?.split(' ') .map((name: string) => name.slice(0, 1).toUpperCase()) .slice(0, 2) - .join('') ?? 'UK'; + .join(''); From 326743d8a1f3a2f363f5df4ebe30947cea2a476b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 10:59:50 +0300 Subject: [PATCH 09/32] chore: added app version --- apps/web/next.config.js | 4 +++- apps/web/src/app/(dashboard)/admin/page.tsx | 2 +- turbo.json | 16 +++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 09760f806..1e98b98fc 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -18,7 +18,9 @@ const config = { '@documenso/ui', '@documenso/email', ], - env, + env: { + APP_VERSION: process.env.npm_package_version, + }, modularizeImports: { 'lucide-react': { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index fdb54dc07..78358c95a 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -87,7 +87,7 @@ export default async function Admin() { return (
-

Instance metrics

+

Instance version: {process.env.APP_VERSION}

Date: Mon, 11 Sep 2023 11:34:10 +0300 Subject: [PATCH 10/32] chore: fix version in nextjs config --- apps/web/next.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 1e98b98fc..fa6c0d1ac 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); +const { version } = require('./package.json'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), @@ -19,7 +20,8 @@ const config = { '@documenso/email', ], env: { - APP_VERSION: process.env.npm_package_version, + ...env, + APP_VERSION: version, }, modularizeImports: { 'lucide-react': { From 00574325b90e11d64f552cbd6b809acc98897ea2 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 13:43:17 +0300 Subject: [PATCH 11/32] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 8 ++++---- turbo.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 78358c95a..073056478 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -3,11 +3,11 @@ import { File, FileX2, LucideIcon, - User as LucideUser, Mail, MailOpen, PenTool, Send, + User as UserIcon, UserPlus2, UserSquare2, } from 'lucide-react'; @@ -26,7 +26,7 @@ import { import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; -type TCardData = { +type CardData = { icon: LucideIcon; title: string; status: @@ -39,7 +39,7 @@ type TCardData = { | 'NOT_SENT'; }; -const CARD_DATA: TCardData[] = [ +const CARD_DATA: CardData[] = [ { icon: UserSquare2, title: 'Recipients in the database', @@ -89,7 +89,7 @@ export default async function Admin() {

Instance version: {process.env.APP_VERSION}

- + Date: Mon, 11 Sep 2023 15:22:09 +0200 Subject: [PATCH 12/32] fix: update building documenso article description --- .../content/blog/building-documenso-pt1.mdx | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/apps/marketing/content/blog/building-documenso-pt1.mdx b/apps/marketing/content/blog/building-documenso-pt1.mdx index 92c6f61ed..36e28c7dc 100644 --- a/apps/marketing/content/blog/building-documenso-pt1.mdx +++ b/apps/marketing/content/blog/building-documenso-pt1.mdx @@ -1,98 +1,98 @@ ---- -title: 'Building Documenso — Part 1: Certificates' -description: In today's fast-paced world, productivity and efficiency are crucial for success, both in personal and professional endeavors. We all strive to make the most of our time and energy to achieve our goals effectively. However, it's not always easy to stay on track and maintain peak performance. In this blog post, we'll explore 10 valuable tips to help you boost productivity and efficiency in your daily life. -authorName: 'Timur Ercan' -authorImage: '/blog/blog-author-timur.jpeg' -authorRole: 'Co-Founder' -date: 2023-06-23 -tags: - - Open Source - - Document Signature - - Certificates - - Signing ---- - -
- - -
- What actually is a signature? -
-
- -> Disclaimer: I’m not a lawyer and this isn’t legal advice. We plan to publish a much more specific framework on the topic of signature validity. - -This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. - -As you may have heard, we launched the community-reviewed version 0.9 of Documenso on GitHub recently and it’s now available through the early adopter’s plan. One of the most fundamental choices we had to make on this first release, was the choice of certificate. While it’s interesting to know what we opted for, this shall also serve as a guide for everyone facing the same choice for self-hosting Documenso. - -> Question: Why do I need a document signing certificate to self-host? -> -> Short Answer: Inserting the images of a signature into the document is only part of the signing process. - -To have an actual digitally signed document you need a document signing certificate that is used to create the digital signature that is inserted into the document, alongside the visible one¹. - -When hosting a signature service yourself, as we do, there are four main choices for handling the certificate: Not using a certificate, creating your own, buying a trusted certificate, and becoming and trusted service provider to issue your own trusted certificate. - -## 1\. No Certificate - -A lot of signing services actually don’t employ actual digital signatures besides the inserted image. The only insert and image of the signatures into the document you sign. This can be done and is legally acceptable in many cases. This option isn’t directly supported by Documenso without changing the code. - -## 2\. Create your own - -Since the cryptography behind certificates is freely available as open source you could generate your own using OpenSSL for example. Since it’s hardly more work than option 1 (using Documenso at least), this would be my minimum effort recommendation. Having a self-created (“self-signed”) certificate doesn’t add much in terms of regulation but it guarantees the document’s integrity, meaning no changes have been made after signing². What this doesn’t give you, is the famous green checkmark in Adobe Acrobat. Why? Because you aren’t on the list of providers Adobe “trusts”.³ - -## 3\. Buy a “trusted” certificate. - -There are Certificate Authorities (CAs) that can sell you a certificate⁴. The service they provide is, that they validate your name (personal certificates) or your organization’s name (corporate certificate) before creating your certificate for you, just like you did in option 2. The difference is, that they are listed on the previously mentioned trust lists (e.g. Adobe’s) and thus the resulting signatures get a nice, green checkmark in Adobe Reader⁵ - -## 4\. Becoming a Trusted Certificate Authority (CA) yourself and create your own certificate - -This option is an incredibly complex endeavour, requiring a lot of effort and skill. It can be done, as there are multiple CAs around the world. Is it worth the effort? That depends a lot on what you’re trying to accomplish. - -
.  .  .
- -## What we did - -Having briefly introduced the options, here is what we did: Since we aim to raise the bar on digital signature proliferation and trust, we opted to buy an “Advanced Personal Certificates for Companies/Organisations” from WiseKey. Thus, documents signed with Documenso’s hosted version look like this: - -
- - -
The famous green checkmark: Signed by hosted Documenso
-
- -There weren’t any deeper reasons we choose WiseKey, other than they offered what we needed and there wasn’t any reason to look much further. While I didn’t map the entire certificate market offering (yet), I’m pretty sure something similar could be found elsewhere. While we opted for option 3, choosing option 2 might be perfectly reasonable considering your use case.⁶ - -> While this is our setup, for now, we have a bigger plan for this topic. While globally trusted SSL Certificates have been available for free, courtesy of Let’s Encrypt, for a while now, there is no such thing as document signing. And there should be. Not having free and trusted infrastructure for signing is blocking a completely new generation of signing products from being created. This is why we’ll start working on option 4 when the time is right. - -Do you have questions or thoughts about this? As always, let me know in the comments, on twitter.com/eltimuro -or directly: documen.so/timur - -Join the self-hoster community here: https://documenso.slack.com/ - -Best from Hamburg - -Timur - -\[1\] There are different approaches to signing a document. For the sake of simplicity, here we talk about a document with X inserted signature images, that is afterward signed once the by signing service, i.e. Documenso. If each visual signature should have its own digital one (e.g. QES — eIDAS Level 3), the case is a bit more complex. - -\[2\] Of course, the signing service provider technically can change and resign the document, especially in the case mentioned in \[1\]. This can be countered by requiring actual digital signatures from each signer, that are bound to their identity/ account. Creating a completely trustless system in the context however is extremely hard to do and not the most pressing business need for the industry at this point, in my opinion. Though, this would be nice. - -\[3\] Adobe, like the EU, has a list of organizations they trust. The Adobe green checkmark is powered by the Adobe trust list, if you want to be trusted by EU standards here: https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation, you need to be on the EU trust list. Getting on each list is possible, though the latter is much more work. - -\[4\] Technically, they sign your certificate creation request (created by you), containing your info with their certificate (which is trusted), making your certificate trusted. This way, everything you sign with your certificate is seen as trusted. They created their certificate just like you, the difference is they are on the lists, mentioned in \[3\] - -\[5\] Why does Adobe get to say, what is trusted? They simply happen to have the most used pdf viewer. And since everyone checks there, whom they consider trusted carries weight. If it should be like this, is a different matter. - -\[6\] Self-Signed signatures, even purely visual signatures, are fully legally binding. Why you use changes mainly your confidence in the signature and the burden of proof. Also, some industries require a certain level of signatures e.g. retail loans (QES/ eIDAS Level 3 in the EU). +--- +title: 'Building Documenso — Part 1: Certificates' +description: This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2023-06-23 +tags: + - Open Source + - Document Signature + - Certificates + - Signing +--- + +
+ + +
+ What actually is a signature? +
+
+ +> Disclaimer: I’m not a lawyer and this isn’t legal advice. We plan to publish a much more specific framework on the topic of signature validity. + +This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. + +As you may have heard, we launched the community-reviewed version 0.9 of Documenso on GitHub recently and it’s now available through the early adopter’s plan. One of the most fundamental choices we had to make on this first release, was the choice of certificate. While it’s interesting to know what we opted for, this shall also serve as a guide for everyone facing the same choice for self-hosting Documenso. + +> Question: Why do I need a document signing certificate to self-host? +> +> Short Answer: Inserting the images of a signature into the document is only part of the signing process. + +To have an actual digitally signed document you need a document signing certificate that is used to create the digital signature that is inserted into the document, alongside the visible one¹. + +When hosting a signature service yourself, as we do, there are four main choices for handling the certificate: Not using a certificate, creating your own, buying a trusted certificate, and becoming and trusted service provider to issue your own trusted certificate. + +## 1\. No Certificate + +A lot of signing services actually don’t employ actual digital signatures besides the inserted image. The only insert and image of the signatures into the document you sign. This can be done and is legally acceptable in many cases. This option isn’t directly supported by Documenso without changing the code. + +## 2\. Create your own + +Since the cryptography behind certificates is freely available as open source you could generate your own using OpenSSL for example. Since it’s hardly more work than option 1 (using Documenso at least), this would be my minimum effort recommendation. Having a self-created (“self-signed”) certificate doesn’t add much in terms of regulation but it guarantees the document’s integrity, meaning no changes have been made after signing². What this doesn’t give you, is the famous green checkmark in Adobe Acrobat. Why? Because you aren’t on the list of providers Adobe “trusts”.³ + +## 3\. Buy a “trusted” certificate. + +There are Certificate Authorities (CAs) that can sell you a certificate⁴. The service they provide is, that they validate your name (personal certificates) or your organization’s name (corporate certificate) before creating your certificate for you, just like you did in option 2. The difference is, that they are listed on the previously mentioned trust lists (e.g. Adobe’s) and thus the resulting signatures get a nice, green checkmark in Adobe Reader⁵ + +## 4\. Becoming a Trusted Certificate Authority (CA) yourself and create your own certificate + +This option is an incredibly complex endeavour, requiring a lot of effort and skill. It can be done, as there are multiple CAs around the world. Is it worth the effort? That depends a lot on what you’re trying to accomplish. + +
.  .  .
+ +## What we did + +Having briefly introduced the options, here is what we did: Since we aim to raise the bar on digital signature proliferation and trust, we opted to buy an “Advanced Personal Certificates for Companies/Organisations” from WiseKey. Thus, documents signed with Documenso’s hosted version look like this: + +
+ + +
The famous green checkmark: Signed by hosted Documenso
+
+ +There weren’t any deeper reasons we choose WiseKey, other than they offered what we needed and there wasn’t any reason to look much further. While I didn’t map the entire certificate market offering (yet), I’m pretty sure something similar could be found elsewhere. While we opted for option 3, choosing option 2 might be perfectly reasonable considering your use case.⁶ + +> While this is our setup, for now, we have a bigger plan for this topic. While globally trusted SSL Certificates have been available for free, courtesy of Let’s Encrypt, for a while now, there is no such thing as document signing. And there should be. Not having free and trusted infrastructure for signing is blocking a completely new generation of signing products from being created. This is why we’ll start working on option 4 when the time is right. + +Do you have questions or thoughts about this? As always, let me know in the comments, on twitter.com/eltimuro +or directly: documen.so/timur + +Join the self-hoster community here: https://documenso.slack.com/ + +Best from Hamburg + +Timur + +\[1\] There are different approaches to signing a document. For the sake of simplicity, here we talk about a document with X inserted signature images, that is afterward signed once the by signing service, i.e. Documenso. If each visual signature should have its own digital one (e.g. QES — eIDAS Level 3), the case is a bit more complex. + +\[2\] Of course, the signing service provider technically can change and resign the document, especially in the case mentioned in \[1\]. This can be countered by requiring actual digital signatures from each signer, that are bound to their identity/ account. Creating a completely trustless system in the context however is extremely hard to do and not the most pressing business need for the industry at this point, in my opinion. Though, this would be nice. + +\[3\] Adobe, like the EU, has a list of organizations they trust. The Adobe green checkmark is powered by the Adobe trust list, if you want to be trusted by EU standards here: https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation, you need to be on the EU trust list. Getting on each list is possible, though the latter is much more work. + +\[4\] Technically, they sign your certificate creation request (created by you), containing your info with their certificate (which is trusted), making your certificate trusted. This way, everything you sign with your certificate is seen as trusted. They created their certificate just like you, the difference is they are on the lists, mentioned in \[3\] + +\[5\] Why does Adobe get to say, what is trusted? They simply happen to have the most used pdf viewer. And since everyone checks there, whom they consider trusted carries weight. If it should be like this, is a different matter. + +\[6\] Self-Signed signatures, even purely visual signatures, are fully legally binding. Why you use changes mainly your confidence in the signature and the burden of proof. Also, some industries require a certain level of signatures e.g. retail loans (QES/ eIDAS Level 3 in the EU). From e8796a7d86cdf9be56da6fc8b88cb12360b506ab Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 12 Sep 2023 12:33:04 +1000 Subject: [PATCH 13/32] refactor: organise recipient utils --- .../avatar/stack-avatars-with-tooltip.tsx | 10 +++++----- .../components/(dashboard)/avatar/stack-avatars.tsx | 4 ++-- .../(dashboard)/layout/profile-dropdown.tsx | 6 ++++-- .../lib/client-only/recipient-avatar-fallback.ts | 7 ------- packages/lib/client-only/recipient-initials.ts | 6 ------ packages/lib/utils/recipient-formatter.ts | 12 ++++++++++++ 6 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 packages/lib/client-only/recipient-avatar-fallback.ts delete mode 100644 packages/lib/client-only/recipient-initials.ts create mode 100644 packages/lib/utils/recipient-formatter.ts 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 3f6407029..e36415813 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,5 +1,5 @@ -import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { Recipient } from '@documenso/prisma/client'; import { Tooltip, @@ -56,7 +56,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -73,7 +73,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -90,7 +90,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -107,7 +107,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx index 678836ffd..91f470f74 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { Recipient } from '@documenso/prisma/client'; import { StackAvatar } from './stack-avatar'; @@ -26,7 +26,7 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) { first={first} zIndex={String(zIndex - index * 10)} type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)} - fallbackText={lastItemText ? lastItemText : recipientAvatarFallback(recipient)} + fallbackText={lastItemText ? lastItemText : recipientAbbreviation(recipient)} /> ); }); diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e52d9b42f..3b361e885 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -15,7 +15,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientInitials } from '@documenso/lib/utils/recipient-formatter'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -41,7 +41,9 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const isBillingEnabled = getFlag('app_billing'); - const avatarFallback = user.name ? initials(user.name) : user.email.slice(0, 1).toUpperCase(); + const avatarFallback = user.name + ? recipientInitials(user.name) + : user.email.slice(0, 1).toUpperCase(); return ( diff --git a/packages/lib/client-only/recipient-avatar-fallback.ts b/packages/lib/client-only/recipient-avatar-fallback.ts deleted file mode 100644 index 7a296a5fa..000000000 --- a/packages/lib/client-only/recipient-avatar-fallback.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Recipient } from '@documenso/prisma/client'; - -import { initials } from './recipient-initials'; - -export const recipientAvatarFallback = (recipient: Recipient) => { - return initials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); -}; diff --git a/packages/lib/client-only/recipient-initials.ts b/packages/lib/client-only/recipient-initials.ts deleted file mode 100644 index 403ed26e4..000000000 --- a/packages/lib/client-only/recipient-initials.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const initials = (text: string) => - text - ?.split(' ') - .map((name: string) => name.slice(0, 1).toUpperCase()) - .slice(0, 2) - .join(''); diff --git a/packages/lib/utils/recipient-formatter.ts b/packages/lib/utils/recipient-formatter.ts new file mode 100644 index 000000000..da404830b --- /dev/null +++ b/packages/lib/utils/recipient-formatter.ts @@ -0,0 +1,12 @@ +import { Recipient } from '@documenso/prisma/client'; + +export const recipientInitials = (text: string) => + text + .split(' ') + .map((name: string) => name.slice(0, 1).toUpperCase()) + .slice(0, 2) + .join(''); + +export const recipientAbbreviation = (recipient: Recipient) => { + return recipientInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); +}; From 581f08c59bf1d39bffb499f746b7942b7a9674c3 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Tue, 12 Sep 2023 07:25:44 +0000 Subject: [PATCH 14/32] fix: update layout and wording --- apps/web/src/app/(dashboard)/admin/layout.tsx | 28 ++--- apps/web/src/app/(dashboard)/admin/nav.tsx | 47 +++++++ apps/web/src/app/(dashboard)/admin/page.tsx | 115 +----------------- .../src/app/(dashboard)/admin/stats/page.tsx | 75 ++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 22 ++-- .../(dashboard)/metric-card/metric-card.tsx | 6 +- .../server-only/admin/get-documents-stats.ts | 25 +++- 7 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/nav.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/stats/page.tsx diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a221d92ba..a04c7b693 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,23 +1,19 @@ -import { redirect } from 'next/navigation'; +import React from 'react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; +import { AdminNav } from './nav'; -export type AdminLayoutProps = { +export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default async function AdminLayout({ children }: AdminLayoutProps) { - const user = await getRequiredServerComponentSession(); - const isUserAdmin = isAdmin(user); +export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + return ( +
+
+ - if (!user) { - redirect('/signin'); - } - - if (!isUserAdmin) { - redirect('/dashboard'); - } - - return
{children}
; +
{children}
+
+
+ ); } diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx new file mode 100644 index 000000000..3b87a9b13 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { HTMLAttributes } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { BarChart3, User2 } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +export type AdminNavProps = HTMLAttributes; + +export const AdminNav = ({ className, ...props }: AdminNavProps) => { + const pathname = usePathname(); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 073056478..5fe030685 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -1,114 +1,5 @@ -import { - Archive, - File, - FileX2, - LucideIcon, - Mail, - MailOpen, - PenTool, - Send, - User as UserIcon, - UserPlus2, - UserSquare2, -} from 'lucide-react'; +import { redirect } from 'next/navigation'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; -import { - getUsersCount, - getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users-stats'; -import { - ReadStatus as InternalReadStatus, - SendStatus as InternalSendStatus, - SigningStatus as InternalSigningStatus, -} from '@documenso/prisma/client'; - -import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; - -type CardData = { - icon: LucideIcon; - title: string; - status: - | 'TOTAL_RECIPIENTS' - | 'OPENED' - | 'NOT_OPENED' - | 'SIGNED' - | 'NOT_SIGNED' - | 'SENT' - | 'NOT_SENT'; -}; - -const CARD_DATA: CardData[] = [ - { - icon: UserSquare2, - title: 'Recipients in the database', - status: 'TOTAL_RECIPIENTS', - }, - { - icon: MailOpen, - title: 'Opened documents', - status: InternalReadStatus.OPENED, - }, - { - icon: Mail, - title: 'Unopened documents', - status: InternalReadStatus.NOT_OPENED, - }, - { - icon: Send, - title: 'Sent documents', - status: InternalSendStatus.SENT, - }, - { - icon: Archive, - title: 'Unsent documents', - status: InternalSendStatus.NOT_SENT, - }, - { - icon: PenTool, - title: 'Signed documents', - status: InternalSigningStatus.SIGNED, - }, - { - icon: FileX2, - title: 'Unsigned documents', - status: InternalSigningStatus.NOT_SIGNED, - }, -]; - -export default async function Admin() { - const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ - getUsersCount(), - getUsersWithSubscriptionsCount(), - getDocsCount(), - getRecipientsStats(), - ]); - - return ( -
-

Instance version: {process.env.APP_VERSION}

-
- - -
-

Document metrics

-
- -
- -

Recipients metrics

-
- {CARD_DATA.map((card) => ( -
- -
- ))} -
-
- ); +export default function Admin() { + redirect('/admin/stats'); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx new file mode 100644 index 000000000..b93af5a03 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -0,0 +1,75 @@ +import { + File, + FileCheck, + FileClock, + FileEdit, + Mail, + MailOpen, + PenTool, + User as UserIcon, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users-stats'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +export default async function AdminStatsPage() { + const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocumentStats(), + getRecipientsStats(), + ]); + + return ( +
+

Instance Stats

+ +
+ + + + +
+ +
+
+

Document metrics

+ +
+ + + + +
+
+ +
+

Recipients metrics

+ +
+ + + + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e3fd4c6d6..3f7a02e60 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -62,6 +62,19 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { Account + {isUserAdmin && ( + <> + + + + Admin + + + + + + )} + @@ -69,15 +82,6 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - {isUserAdmin && ( - - - - Admin - - - )} - diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx index f59d42096..a2248ccdc 100644 --- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx +++ b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx @@ -18,10 +18,10 @@ export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricPr )} >
-
- {Icon && } +
+ {Icon && } -

{title}

+

{title}

diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index 9100a886c..e0d53373f 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -1,5 +1,26 @@ import { prisma } from '@documenso/prisma'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -export const getDocsCount = async () => { - return await prisma.document.count(); +export const getDocumentStats = async () => { + const counts = await prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + }); + + const stats: Record, number> = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }; + + counts.forEach((stat) => { + stats[stat.status] = stat._count._all; + + stats.ALL += stat._count._all; + }); + + return stats; }; From 599e857a1e282141fba6add03b5cf7c6e3c110d0 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 12 Sep 2023 17:53:38 +1000 Subject: [PATCH 15/32] fix: add removed layout guard --- apps/web/src/app/(dashboard)/admin/layout.tsx | 13 ++++++++++++- .../lib/server-only/admin/get-recipients-stats.ts | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a04c7b693..3aa47d1a9 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,12 +1,23 @@ import React from 'react'; +import { redirect } from 'next/navigation'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; + import { AdminNav } from './nav'; export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { +export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + const user = await getRequiredServerComponentSession(); + + if (!isAdmin(user)) { + redirect('/documents'); + } + return (

diff --git a/packages/lib/server-only/admin/get-recipients-stats.ts b/packages/lib/server-only/admin/get-recipients-stats.ts index 92c0c3527..f24d0b5a2 100644 --- a/packages/lib/server-only/admin/get-recipients-stats.ts +++ b/packages/lib/server-only/admin/get-recipients-stats.ts @@ -16,6 +16,7 @@ export const getRecipientsStats = async () => { [SendStatus.SENT]: 0, [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { const { readStatus, signingStatus, sendStatus, _count } = result; stats[readStatus] += _count; @@ -23,5 +24,6 @@ export const getRecipientsStats = async () => { stats[sendStatus] += _count; stats.TOTAL_RECIPIENTS += _count; }); + return stats; }; From 46dfaa70a3de4eec85a8df094e5973aab88bc298 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 13 Sep 2023 14:39:01 +0200 Subject: [PATCH 16/32] Update apps/marketing/content/blog/building-documenso-pt1.mdx Co-authored-by: Adithya Krishna --- apps/marketing/content/blog/building-documenso-pt1.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marketing/content/blog/building-documenso-pt1.mdx b/apps/marketing/content/blog/building-documenso-pt1.mdx index 36e28c7dc..592f4e1f7 100644 --- a/apps/marketing/content/blog/building-documenso-pt1.mdx +++ b/apps/marketing/content/blog/building-documenso-pt1.mdx @@ -79,7 +79,7 @@ There weren’t any deeper reasons we choose WiseKey, other than they offered wh Do you have questions or thoughts about this? As always, let me know in the comments, on twitter.com/eltimuro or directly: documen.so/timur -Join the self-hoster community here: https://documenso.slack.com/ +Join the self-hoster community here: https://documen.so/discord Best from Hamburg From 3c36eedfba35605af1ef3326404a9530484deef2 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 13 Sep 2023 14:42:27 +0200 Subject: [PATCH 17/32] chore: phrasing --- apps/marketing/content/blog/building-documenso-pt1.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marketing/content/blog/building-documenso-pt1.mdx b/apps/marketing/content/blog/building-documenso-pt1.mdx index 592f4e1f7..b8507be03 100644 --- a/apps/marketing/content/blog/building-documenso-pt1.mdx +++ b/apps/marketing/content/blog/building-documenso-pt1.mdx @@ -1,6 +1,6 @@ --- title: 'Building Documenso — Part 1: Certificates' -description: This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. +description: This is the first part of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. authorName: 'Timur Ercan' authorImage: '/blog/blog-author-timur.jpeg' authorRole: 'Co-Founder' From 974dc740732fb3fc3f1f13fdb46540db9e74c4d6 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 13 Sep 2023 14:53:27 +0200 Subject: [PATCH 18/32] chore: moved rewrite article from next repo --- .../content/blog/why-were-doing-a-rewrite.mdx | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 apps/marketing/content/blog/why-were-doing-a-rewrite.mdx diff --git a/apps/marketing/content/blog/why-were-doing-a-rewrite.mdx b/apps/marketing/content/blog/why-were-doing-a-rewrite.mdx new file mode 100644 index 000000000..f2195a019 --- /dev/null +++ b/apps/marketing/content/blog/why-were-doing-a-rewrite.mdx @@ -0,0 +1,113 @@ +--- +title: Why we're doing a rewrite +description: As we move beyond MVP and onto creating the open signing infrastructure we all deserve we need to take a quick pit-stop. +authorName: 'Lucas Smith' +authorImage: '/blog/blog-author-lucas.png' +authorRole: 'Co-Founder' +date: 2023-08-05 +tags: + - Community + - Development +--- + +
+ + +
+ The next generation of Documenso and signing infrastructure. +
+
+ +> TLDR; We're rewriting Documenso to move on from our MVP foundations and create an even better base for the project. This rewrite will provide us the opportunity to fix a few things within the project while enabling a faster development process moving forward. + +# Introduction + +At Documenso, we're building the next generation of signing infrastructure with a focus on making it inclusive and accessible for all. To do this we need to ensure that the software we write is also inclusive and accessible and for this reason we’ve decided to take a step back and perform a _quick_ rewrite. + +Although we've achieved validated MVP status and gained paying customers, we're still quite far from our goal of creating a trusted, open signing experience. To move closer to that future, we need to step back and focus on the project's foundations to ensure we can solve all the items we set out to on our current homepage. + +Fortunately, this wasn't a case of someone joining the team and proposing a rewrite due to a lack of understanding of the codebase and context surrounding it. Prior to joining Documenso as a co-founder, I had spent an extensive amount of time within the Documenso codebase and had a fairly intimate understanding of what was happening for the most part. This knowledge allowed me to make the fair and simultaneously hard call to take a quick pause so we can rebuild our current foundations to enable accessibility and a faster delivery time in the future. + +# The Reasoning: TypeScript + +Our primary reason for the rewrite is to better leverage the tools and technologies we've already chosen, namely TypeScript. While Documenso currently uses TypeScript, it's not fully taking advantage of its safety features, such as generics and type guards. + +The codebase currently has several instances of `any` types, which is expected when working in an unknown domain where object models aren't fully understood before exploration and experimentation. These `any`s initially sped up development, but have since become a hindrance due to the lack of type information, combined with prop drilling. As a result, it's necessary to go through a lot of context to understand the root of any given issue. + +The rewrite is using TypeScript to its full potential, ensuring that every interaction is strongly typed, both through general TypeScript tooling and the introduction of [Zod](https://github.com/colinhacks/zod), a validation library with excellent TypeScript support. With these choices, we can ensure that the codebase is robust to various inputs and states, as most issues will be caught during compile time and flagged within a developer's IDE. + +# The Reasoning: Stronger API contracts + +In line with our pattern of creating strongly typed contracts, we've decided to use [tRPC](https://github.com/trpc/trpc) for our internal API. This enables us to share types between our frontend and backend and establish a solid contract for interactions between the two. This is in contrast to the currently untyped API endpoints in Documenso, which are accessed using the `fetch` API that is itself untyped. + +Using tRPC drastically reduces the chance of failures resulting from mundane things like argument or response shape changes during updates and upgrades. We made this decision easily because tRPC is a mature technology with no signs of losing momentum any time soon. + +Additionally, many of our open-source friends have made the same choice for similar reasons. + +# The Reasoning: Choosing exciting technologies + +Although we already work with what I consider to be a fun stack that includes Next.js, Prisma, Tailwind, and more, it's no secret that contributors enjoy working with new technologies that benefit them in their own careers and projects. + +To take advantage of this, we have decided to use Next.js 13 and React's new server component and actions architecture. Server components are currently popular among developers, with many loving and hating them at the same time. + +I have personally worked with server components and actions since they were first released in October 2022 and have dealt with most of the hiccups and limitations along the way. Now, in July 2023, I believe they are in a much more stable place and are ready to be adopted, with their benefits being recognised by many. + +By choosing to use server components and actions, we hope to encourage the community to participate more than they otherwise might. However, we are only choosing this because it has become more mature and stable. We will not choose things that are less likely to become the de-facto solution in the future, as we do not wish to inherit a pile of tech debt later on. + +# The Reasoning: Allowing concurrent work + +Another compelling reason for the rewrite was to effectively modularise code so we can work on features concurrently and without issue. This means extracting as much as possible out of components, API handlers and more and into a set of methods and functions that attempt to focus on just one thing. + +In performing this work we should be able to easily make refactors and other changes to various parts of the code without stepping on each others feet, this also grants us the ability to upgrade or deprecate items as required by sticking to the contract of the previous method. + +Additionally, this makes testing a much easier task as we can focus more on units of work rather than extensive end to end testing although we aim to have both, just not straight away. + +# The Reasoning: Licensing of work + +Another major reasoning for the rewrite is to ensure that all work performed on the project by both our internal team and external contributors is licensed in a way that benefits the project long-term. Prior to the rewrite contributors would create pull requests that would be merged in without any further process outside of the common code-review and testing cycles. + +This was fine for the most part since we were simply working on the MVP but now as we move towards an infrastructure focus we intend on taking on enterprise clients who will have a need for a non-GPLv3 license since interpretations of it can be quite harmful to private hosting, to facilitate this we will require contributors to sign a contributor license agreement (CLA) prior to their changes being merged which will assign a perpetual license for us to use their code and relicense it as required such as for the use-case above. + +While some might cringe at the idea of signing a CLA, we want to offer a compelling enterprise offering through means of dual-licensing. Great enterprise adoption is one of the cornerstones of our strategy and will be key to funding community and product development long-term. + +_Do note that the above does not mean that we will ever go closed-source, it’s a point in our investor agreements that [https://github.com/documenso/documenso](https://github.com/documenso/documenso) will always remain available and open-source._ + +# Goals and Non-Goals + +Rewriting an application is a monumental task that I have taken on and rejected many times in my career. As I get older, I become more hesitant to perform these rewrites because I understand that systems carry a lot of context and history. This makes them better suited for piecemeal refactoring instead, which avoids learning the lessons of the past all over again during the launch of the rewrite. + +To ensure that we aren't just jumping off the deep end, I have set out a list of goals and non-goals to keep this rewrite lean and affordable. + +### Goals + +- Provide a clean design and interface for the newly rewritten application that creates a sense of trust and security at first glance. +- Create a stable foundation and architecture that will allow for growth into our future roadmap items (teams, automation, workflows, etc.). +- Create a robust system that requires minimal context through strong contracts and typing. + +### Non-Goals + +- Change the database schema (we don't want to make migration harder than it needs to be, thus all changes must be additive). +- Add too many features that weren't in the system prior to the rewrite. +- Remove any features that were in the older version of Documenso, such as free signatures (signatures that have no corresponding field). + +# Rollout Plan + +Thanks to the constraints listed above our rollout will hopefully be fairly painless, still to be safe we plan on doing the following. + +1. In the current [testing environment](https://test.documenso.com), create and sign a number of documents leaving many in varying states of completion. +2. Deploy the rewrite to the testing environment and verify that all existing documents and information is retrievable and modifiable without any issue. +3. Create another set of documents using the new rewrite and verify that all interactions between authoring and signing work as expected. +4. Repeat this until we reach a general confidence level (expectation of two weeks). + +Once we’ve reached the desired confidence level with our testing environment we will look to deploy the rewrite to the production environment ensuring that we’ve performed all the required backups in the event of a catastrophic failure. + +# Want to help out? + +We’re currently working on the **[feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh)** branch on GitHub, we aim to have a CLA available to sign in the coming days so we can start accepting external contributions asap. While we’re nearing the end-stage of the rewrite we will be throwing up a couple of bounties shortly for things like [Husky](https://github.com/typicode/husky) and [Changesets](https://github.com/changesets/changesets). + +Keep an eye on our [GitHub issues](https://github.com/documenso/documenso/issues) to stay up to date! From 71818c0f1f36f1d5256691ec427e26d70e00e439 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 13 Sep 2023 14:57:22 +0200 Subject: [PATCH 19/32] chore: update readme to main version --- README.md | 118 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ebab1c3f5..29ffb0d65 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@

- + Documenso Logo -

Open Source Signing Infrastructure

-

- The DocuSign Open Source Alternative. -
+ The Open Source DocuSign Alternative. +
Learn more »

@@ -22,12 +20,16 @@

- Join Documenso on Discord + Join Documenso on Discord Github Stars License Commits-per-month

+> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.** +> +> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)** + # Documenso 0.9 - Developer Preview
@@ -63,18 +65,28 @@ Signing documents digitally is fast, easy and should be best practice for every ## Community and Next Steps 🎯 -The current project goal is to [release a production ready version](https://github.com/documenso/documenso/milestone/1) for self-hosting as soon as possible. If you want to help making that happen you can: +We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. - Check out the first source code release in this repository and test it - Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions) -- Join the [Slack Channel](https://documen.so/slack) for any questions and getting to know to other community members +- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members - ⭐ the repository to help us raise awareness - Spread the word on Twitter, that Documenso is working towards a more open signing tool - Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release ## Contributing -- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md). +- To contribute, please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md). + +## Contact us + +Contact us if you are interested in our Enterprise plan for large organizations that need extra flexibility and control. + +Book us with Cal.com + +## Activity + +![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg) # Tech @@ -89,10 +101,6 @@ Documenso is built using awesome open source tech including: - [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf) - [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf) - [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib) -- [Zod for schema declaration and validation](https://zod.dev/) -- [Lucide React for icons in React app](https://lucide.dev/) -- [Framer Motion for motion library](https://www.framer.com/motion/) -- [Radix UI for component library](https://www.radix-ui.com/) - Check out `/package.json` and `/apps/web/package.json` for more - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. @@ -135,37 +143,47 @@ Your database will also be available on port `54320`. You can connect to it usin ## Developer Setup +### Manual Setup + Follow these steps to setup documenso on you local machine: - [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. ```sh git clone https://github.com/documenso/documenso ``` -- Run npm i in root directory -- Rename .env.example to .env +- Run `npm i` in root directory +- Rename `.env.example` to `.env` - Set DATABASE_URL value in .env file - You can use the provided test database url (may be wiped at any point) - Or setup a local postgres sql instance (recommended) -- Create the database scheme by running db-migrate:dev +- Create the database scheme by running `db-migrate:dev` - Setup your mail provider - - Set SENDGRID_API_KEY value in .env file + - Set `SENDGRID_API_KEY` value in .env file - You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/). - - Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the SMTP\_\* variables in your .env -- Run npm run dev root directory to start + - Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP + \_ + * variables` in your .env +- Run `npm run dev` root directory to start - Register a new user at http://localhost:3000/signup --- -- Optional: Seed the database using npm run db-seed to create a test user and document -- Optional: Upload and sign apps/web/resources/example.pdf manually to test your setup +- Optional: Seed the database using `npm run db-seed` to create a test user and document +- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup - Optional: Create your own signing certificate - A demo certificate is provided in `/app/web/resources/certificate.p12` - To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**. +### Run in Gitpod + +- Click below to launch a ready-to-use Gitpod workspace in your browser. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso) + ## Updating -- If you pull the newest version from main, using git pull, it may be necessary to regenerate your database client +- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client - You can do this by running the generate command in `/packages/prisma`: ```sh npx prisma generate @@ -176,16 +194,22 @@ Follow these steps to setup documenso on you local machine: For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one: -1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\ - openssl genrsa -out private.key 2048 +1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key: + + `openssl genrsa -out private.key 2048` + +2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate: + + `openssl req -new -x509 -key private.key -out certificate.crt -days 365` -2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:\ - openssl req -new -x509 -key private.key -out certificate.crt -days 365 \ This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid. -3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \ - openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt + +3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: + + `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` # Docker @@ -193,16 +217,42 @@ For the digital signature of your documents you need a signing certificate in .p Want to create a production ready docker image? Follow these steps: -- Run `./docker/build.sh` in the root directory. -- Publish the image to your docker registry of choice. +- cd into `docker` directory +- Make `build.sh` executable by running `chmod +x build.sh` +- Run `./build.sh` to start building the docker image. +- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command -# Deploying - Coming Soon™ +``` +docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest +``` -- Docker support -- One-Click-Deploy on Render.com Deploy +Command Breakdown: +- `-d` - Let's you run the container in background +- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port. +- `-v` - Volume let's you persist the data +- `--name` - Name of the container +- `documenso:latest` - Image you have built + +# Deployment + +We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates! + +## Railway + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX) + +## Render + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) # Troubleshooting +## I'm not receiving any emails when using the developer quickstart + +When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view. + +The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500. + ## Support IPv6 In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command From 9014f012766cef710e4febe88896f381839b7722 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 12:46:36 +1000 Subject: [PATCH 20/32] feat: universal upload Implementation of a universal upload allowing for multiple storage backends starting with `database` and `s3`. Allows clients to put and retrieve files from either client or server using a blend of client and server actions. --- .env.example | 14 + apps/marketing/next.config.js | 9 +- .../src/pages/api/stripe/webhook/index.ts | 46 +- apps/web/next.config.js | 2 +- apps/web/package.json | 1 - apps/web/src/api/document/create/fetcher.ts | 34 - apps/web/src/api/document/create/types.ts | 19 - .../documents/[id]/edit-document.tsx | 10 +- .../documents/[id]/loadable-pdf-card.tsx | 20 - .../app/(dashboard)/documents/[id]/page.tsx | 10 +- .../(dashboard)/documents/upload-document.tsx | 28 +- .../sign/[token]/complete/download-button.tsx | 61 +- .../(signing)/sign/[token]/complete/page.tsx | 8 +- .../src/app/(signing)/sign/[token]/page.tsx | 13 +- apps/web/src/pages/api/document/create.ts | 96 - apps/web/src/pages/api/feature-flag/get.ts | 2 +- .../web/src/pages/api/stripe/webhook/index.ts | 16 +- package-lock.json | 1808 ++++++++++++++++- packages/email/package.json | 5 +- packages/email/tailwind.config.js | 5 +- packages/lib/constants/time.ts | 5 + packages/lib/package.json | 7 +- .../document-data/create-document-data.ts | 19 + .../server-only/document/create-document.ts | 15 +- .../lib/server-only/document/seal-document.ts | 28 +- .../recipient/set-recipients-for-document.ts | 4 +- packages/lib/tsconfig.json | 3 + packages/lib/universal/id.ts | 5 + packages/lib/universal/upload/delete-file.ts | 22 + packages/lib/universal/upload/get-file.ts | 45 + packages/lib/universal/upload/put-file.ts | 53 + .../lib/universal/upload/server-actions.ts | 104 + packages/lib/universal/upload/update-file.ts | 54 + .../migration.sql | 23 + packages/prisma/schema.prisma | 31 +- .../trpc/server/document-router/router.ts | 28 +- .../trpc/server/document-router/schema.ts | 7 + packages/tsconfig/process-env.d.ts | 2 +- packages/ui/package.json | 5 +- .../primitives/document-flow/add-fields.tsx | 2 +- .../primitives/document-flow/add-signers.tsx | 2 +- turbo.json | 6 + 42 files changed, 2372 insertions(+), 305 deletions(-) delete mode 100644 apps/web/src/api/document/create/fetcher.ts delete mode 100644 apps/web/src/api/document/create/types.ts delete mode 100644 apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx delete mode 100644 apps/web/src/pages/api/document/create.ts create mode 100644 packages/lib/constants/time.ts create mode 100644 packages/lib/server-only/document-data/create-document-data.ts create mode 100644 packages/lib/universal/id.ts create mode 100644 packages/lib/universal/upload/delete-file.ts create mode 100644 packages/lib/universal/upload/get-file.ts create mode 100644 packages/lib/universal/upload/put-file.ts create mode 100644 packages/lib/universal/upload/server-actions.ts create mode 100644 packages/lib/universal/upload/update-file.ts create mode 100644 packages/prisma/migrations/20230912011344_reverse_document_data_relation/migration.sql diff --git a/.env.example b/.env.example index cfa96f59b..6f32b5a63 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,20 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen # Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool. NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" +# [[STORAGE]] +# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3 +NEXT_PUBLIC_UPLOAD_TRANSPORT="database" +# OPTIONAL: Defines the endpoint to use for the S3 storage transport. Relevant when using third-party S3-compatible providers. +NEXT_PRIVATE_UPLOAD_ENDPOINT= +# OPTIONAL: Defines the region to use for the S3 storage transport. Defaults to us-east-1. +NEXT_PRIVATE_UPLOAD_REGION= +# REQUIRED: Defines the bucket to use for the S3 storage transport. +NEXT_PRIVATE_UPLOAD_BUCKET= +# OPTIONAL: Defines the access key ID to use for the S3 storage transport. +NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID= +# OPTIONAL: Defines the secret access key to use for the S3 storage transport. +NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY= + # [[SMTP]] # OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index ee7d10899..97f904cf0 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -8,9 +8,16 @@ const { parsed: env } = require('dotenv').config({ /** @type {import('next').NextConfig} */ const config = { + experimental: { + serverActions: true, + }, reactStrictMode: true, transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'], - env, + modularizeImports: { + 'lucide-react': { + transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', + }, + }, }; module.exports = withContentlayer(config); diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index 11c9476bd..b5feeb870 100644 --- a/apps/marketing/src/pages/api/stripe/webhook/index.ts +++ b/apps/marketing/src/pages/api/stripe/webhook/index.ts @@ -8,6 +8,8 @@ import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf'; import { redis } from '@documenso/lib/server-only/redis'; import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { updateFile } from '@documenso/lib/universal/upload/update-file'; import { prisma } from '@documenso/prisma'; import { DocumentDataType, @@ -88,19 +90,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const bytes64 = readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'); + const { id: documentDataId } = await prisma.documentData.create({ + data: { + type: DocumentDataType.BYTES_64, + data: bytes64, + initialData: bytes64, + }, + }); + const document = await prisma.document.create({ data: { title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, created: now, - documentData: { - create: { - type: DocumentDataType.BYTES_64, - data: bytes64, - initialData: bytes64, - }, - }, + documentDataId, }, include: { documentData: true, @@ -139,17 +143,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); + let pdfData = await getFile(documentData).then((data) => + Buffer.from(data).toString('base64'), + ); + if (signatureDataUrl) { - documentData.data = await insertImageInPDF( - documentData.data, + pdfData = await insertImageInPDF( + pdfData, signatureDataUrl, Number(field.positionX), Number(field.positionY), field.page, ); } else { - documentData.data = await insertTextInPDF( - documentData.data, + pdfData = await insertTextInPDF( + pdfData, signatureText ?? '', Number(field.positionX), Number(field.positionY), @@ -157,6 +165,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ); } + const { data: newData } = await updateFile({ + type: documentData.type, + oldData: documentData.initialData, + newData: Buffer.from(pdfData, 'base64').toString('binary'), + }); + await Promise.all([ prisma.signature.create({ data: { @@ -166,16 +180,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) typedSignature: signatureDataUrl ? '' : signatureText, }, }), - prisma.document.update({ + prisma.documentData.update({ where: { - id: document.id, + id: documentData.id, }, data: { - documentData: { - update: { - data: documentData.data, - }, - }, + data: newData, }, }), ]); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 09760f806..7ec6c49ee 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -9,6 +9,7 @@ const { parsed: env } = require('dotenv').config({ const config = { experimental: { serverActions: true, + serverActionsBodySizeLimit: '50mb', }, reactStrictMode: true, transpilePackages: [ @@ -18,7 +19,6 @@ const config = { '@documenso/ui', '@documenso/email', ], - env, modularizeImports: { 'lucide-react': { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', diff --git a/apps/web/package.json b/apps/web/package.json index 8e7dd2be7..d3ab34f96 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,7 +24,6 @@ "lucide-react": "^0.214.0", "luxon": "^3.4.0", "micro": "^10.0.1", - "nanoid": "^4.0.2", "next": "13.4.12", "next-auth": "4.22.3", "next-plausible": "^3.10.1", diff --git a/apps/web/src/api/document/create/fetcher.ts b/apps/web/src/api/document/create/fetcher.ts deleted file mode 100644 index fdc23456c..000000000 --- a/apps/web/src/api/document/create/fetcher.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { TCreateDocumentRequestSchema, ZCreateDocumentResponseSchema } from './types'; - -export const useCreateDocument = () => { - return useMutation(async ({ file }: TCreateDocumentRequestSchema) => { - const formData = new FormData(); - - formData.set('file', file); - - const response = await fetch('/api/document/create', { - method: 'POST', - body: formData, - }); - - const body = await response.json(); - - if (response.status !== 200) { - throw new Error('Failed to create document'); - } - - const safeBody = ZCreateDocumentResponseSchema.safeParse(body); - - if (!safeBody.success) { - throw new Error('Failed to create document'); - } - - if ('error' in safeBody.data) { - throw new Error(safeBody.data.error); - } - - return safeBody.data; - }); -}; diff --git a/apps/web/src/api/document/create/types.ts b/apps/web/src/api/document/create/types.ts deleted file mode 100644 index 07541a5dd..000000000 --- a/apps/web/src/api/document/create/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from 'zod'; - -export const ZCreateDocumentRequestSchema = z.object({ - file: z.instanceof(File), -}); - -export type TCreateDocumentRequestSchema = z.infer; - -export const ZCreateDocumentResponseSchema = z - .object({ - id: z.number(), - }) - .or( - z.object({ - error: z.string(), - }), - ); - -export type TCreateDocumentResponseSchema = z.infer; 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 0ea19cfc4..b4837ab23 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -32,6 +32,7 @@ export type EditDocumentFormProps = { document: DocumentWithData; recipients: Recipient[]; fields: Field[]; + dataUrl: string; }; type EditDocumentStep = 'signers' | 'fields' | 'subject'; @@ -42,16 +43,13 @@ export const EditDocumentForm = ({ recipients, fields, user: _user, + dataUrl, }: EditDocumentFormProps) => { const { toast } = useToast(); const router = useRouter(); - const { documentData } = document; - const [step, setStep] = useState('signers'); - const documentUrl = `data:application/pdf;base64,${documentData?.data}`; - const documentFlow: Record = { signers: { title: 'Add Signers', @@ -154,11 +152,11 @@ export const EditDocumentForm = ({ return (
- + diff --git a/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx b/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx deleted file mode 100644 index 5f01ec107..000000000 --- a/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import { Card, CardContent } from '@documenso/ui/primitives/card'; -import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; -import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer'; - -export type LoadablePDFCard = PDFViewerProps & { - className?: string; - pdfClassName?: string; -}; - -export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => { - return ( - - - - - - ); -}; diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index f7c8f2525..915547607 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -7,6 +7,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; @@ -42,6 +43,12 @@ export default async function DocumentPage({ params }: DocumentPageProps) { const { documentData } = document; + const documentDataUrl = await getFile(documentData) + .then((buffer) => Buffer.from(buffer).toString('base64')) + .then((data) => `data:application/pdf;base64,${data}`); + + console.log({ documentDataUrl: documentDataUrl.slice(0, 40) }); + const [recipients, fields] = await Promise.all([ await getRecipientsForDocument({ documentId, @@ -88,12 +95,13 @@ export default async function DocumentPage({ params }: DocumentPageProps) { user={session} recipients={recipients} fields={fields} + dataUrl={documentDataUrl} /> )} {document.status === InternalDocumentStatus.COMPLETED && (
- +
)}
diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index ee8a69ae6..b472c606b 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -1,29 +1,45 @@ 'use client'; +import { useState } from 'react'; + import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; +import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; +import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { useCreateDocument } from '~/api/document/create/fetcher'; - export type UploadDocumentProps = { className?: string; }; export const UploadDocument = ({ className }: UploadDocumentProps) => { - const { toast } = useToast(); const router = useRouter(); - const { isLoading, mutateAsync: createDocument } = useCreateDocument(); + const { toast } = useToast(); + + const [isLoading, setIsLoading] = useState(false); + + const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation(); const onFileDrop = async (file: File) => { try { + setIsLoading(true); + + const { type, data } = await putFile(file); + + const { id: documentDataId } = await createDocumentData({ + type, + data, + }); + const { id } = await createDocument({ - file: file, + title: file.name, + documentDataId, }); toast({ @@ -41,6 +57,8 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { description: 'An error occurred while uploading your document.', variant: 'destructive', }); + } finally { + setIsLoading(false); } }; diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx index 2195e2e70..088afad33 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx @@ -1,55 +1,55 @@ 'use client'; -import { HTMLAttributes } from 'react'; +import { HTMLAttributes, useState } from 'react'; import { Download } from 'lucide-react'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { DocumentData } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; export type DownloadButtonProps = HTMLAttributes & { disabled?: boolean; fileName?: string; - document?: string; + documentData?: DocumentData; }; export const DownloadButton = ({ className, fileName, - document, + documentData, disabled, ...props }: DownloadButtonProps) => { - /** - * Convert the document from base64 to a blob and download it. - */ - const onDownloadClick = () => { - if (!document) { - return; - } - - let decodedDocument = document; + const [isLoading, setIsLoading] = useState(false); + const onDownloadClick = async () => { try { - decodedDocument = atob(document); + setIsLoading(true); + + if (!documentData) { + return; + } + + const bytes = await getFile(documentData); + + const blob = new Blob([bytes], { + type: 'application/pdf', + }); + + const link = window.document.createElement('a'); + + link.href = window.URL.createObjectURL(blob); + link.download = fileName || 'document.pdf'; + + link.click(); + + window.URL.revokeObjectURL(link.href); } catch (err) { - // We're just going to ignore this error and try to download the document console.error(err); + } finally { + setIsLoading(false); } - - const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0))); - - const blob = new Blob([documentBytes], { - type: 'application/pdf', - }); - - const link = window.document.createElement('a'); - - link.href = window.URL.createObjectURL(blob); - link.download = fileName || 'document.pdf'; - - link.click(); - - window.URL.revokeObjectURL(link.href); }; return ( @@ -57,8 +57,9 @@ export const DownloadButton = ({ type="button" variant="outline" className={className} - disabled={disabled || !document} + disabled={disabled || !documentData} onClick={onDownloadClick} + loading={isLoading} {...props} > diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index 48d2b6435..71a368da5 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -38,9 +38,13 @@ export default async function CompletedSigningPage({ const [fields, recipient] = await Promise.all([ getFieldsForToken({ token }), - getRecipientByToken({ token }), + getRecipientByToken({ token }).catch(() => null), ]); + if (!recipient) { + return notFound(); + } + const recipientName = recipient.name || fields.find((field) => field.type === FieldType.NAME)?.customText || @@ -93,7 +97,7 @@ export default async function CompletedSigningPage({
diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 838e3ee32..d2c14a524 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -8,6 +8,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; 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 { getFile } from '@documenso/lib/universal/upload/get-file'; import { FieldType } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; @@ -36,19 +37,21 @@ export default async function SigningPage({ params: { token } }: SigningPageProp token, }).catch(() => null), getFieldsForToken({ token }), - getRecipientByToken({ token }), + getRecipientByToken({ token }).catch(() => null), viewedDocument({ token }), ]); - if (!document || !document.documentData) { + if (!document || !document.documentData || !recipient) { return notFound(); } const { documentData } = document; - const user = await getServerComponentSession(); + const documentDataUrl = await getFile(documentData) + .then((buffer) => Buffer.from(buffer).toString('base64')) + .then((data) => `data:application/pdf;base64,${data}`); - const documentUrl = `data:application/pdf;base64,${documentData.data}`; + const user = await getServerComponentSession(); return ( @@ -69,7 +72,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp gradient > - + diff --git a/apps/web/src/pages/api/document/create.ts b/apps/web/src/pages/api/document/create.ts deleted file mode 100644 index 897c16f76..000000000 --- a/apps/web/src/pages/api/document/create.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -import formidable, { type File } from 'formidable'; -import { readFileSync } from 'fs'; - -import { getServerSession } from '@documenso/lib/next-auth/get-server-session'; -import { prisma } from '@documenso/prisma'; -import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client'; - -import { - TCreateDocumentRequestSchema, - TCreateDocumentResponseSchema, -} from '~/api/document/create/types'; - -export const config = { - api: { - bodyParser: false, - }, -}; - -export type TFormidableCreateDocumentRequestSchema = { - file: File; -}; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const user = await getServerSession({ req, res }); - - if (!user) { - return res.status(401).json({ - error: 'Unauthorized', - }); - } - - try { - const form = formidable(); - - const { file } = await new Promise( - (resolve, reject) => { - form.parse(req, (err, fields, files) => { - if (err) { - reject(err); - } - - // We had intended to do this with Zod but we can only validate it - // as a persistent file which does not include the properties that we - // need. - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - resolve({ ...fields, ...files } as any); - }); - }, - ); - - const fileBuffer = readFileSync(file.filepath); - - const bytes64 = fileBuffer.toString('base64'); - - const document = await prisma.document.create({ - data: { - title: file.originalFilename ?? file.newFilename, - status: DocumentStatus.DRAFT, - userId: user.id, - documentData: { - create: { - type: DocumentDataType.BYTES_64, - data: bytes64, - initialData: bytes64, - }, - }, - created: new Date(), - }, - }); - - return res.status(200).json({ - id: document.id, - }); - } catch (err) { - console.error(err); - - return res.status(500).json({ - error: 'Internal server error', - }); - } -} - -/** - * This is a hack to ensure that the types are correct. - */ -type FormidableSatisfiesCreateDocument = - keyof TCreateDocumentRequestSchema extends keyof TFormidableCreateDocumentRequestSchema - ? true - : never; - -true satisfies FormidableSatisfiesCreateDocument; diff --git a/apps/web/src/pages/api/feature-flag/get.ts b/apps/web/src/pages/api/feature-flag/get.ts index 6d5204596..6e45b5a18 100644 --- a/apps/web/src/pages/api/feature-flag/get.ts +++ b/apps/web/src/pages/api/feature-flag/get.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; -import { nanoid } from 'nanoid'; import { JWT, getToken } from 'next-auth/jwt'; import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags'; +import { nanoid } from '@documenso/lib/universal/id'; import PostHogServerClient from '~/helpers/get-post-hog-server-client'; diff --git a/apps/web/src/pages/api/stripe/webhook/index.ts b/apps/web/src/pages/api/stripe/webhook/index.ts index 818b3759a..fb7877259 100644 --- a/apps/web/src/pages/api/stripe/webhook/index.ts +++ b/apps/web/src/pages/api/stripe/webhook/index.ts @@ -88,19 +88,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const bytes64 = readFileSync('./public/documenso-supporter-pledge.pdf').toString('base64'); + const { id: documentDataId } = await prisma.documentData.create({ + data: { + type: DocumentDataType.BYTES_64, + data: bytes64, + initialData: bytes64, + }, + }); + const document = await prisma.document.create({ data: { title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, created: now, - documentData: { - create: { - type: DocumentDataType.BYTES_64, - data: bytes64, - initialData: bytes64, - }, - }, + documentDataId, }, include: { documentData: true, diff --git a/package-lock.json b/package-lock.json index 1fa10b764..348a046de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,6 @@ "lucide-react": "^0.214.0", "luxon": "^3.4.0", "micro": "^10.0.1", - "nanoid": "^4.0.2", "next": "13.4.12", "next-auth": "4.22.3", "next-plausible": "^3.10.1", @@ -132,6 +131,776 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32c/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.410.0.tgz", + "integrity": "sha512-9pInvFl3xgk+CnbHFZVk0wAicZUiokIGQ05e/ZDBHjiWK5ph/XeQ4CCTuh7JxT0yABNhua8/6txsyq/uNXOzoA==", + "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.410.0", + "@aws-sdk/credential-provider-node": "3.410.0", + "@aws-sdk/middleware-bucket-endpoint": "3.410.0", + "@aws-sdk/middleware-expect-continue": "3.410.0", + "@aws-sdk/middleware-flexible-checksums": "3.410.0", + "@aws-sdk/middleware-host-header": "3.410.0", + "@aws-sdk/middleware-location-constraint": "3.410.0", + "@aws-sdk/middleware-logger": "3.410.0", + "@aws-sdk/middleware-recursion-detection": "3.410.0", + "@aws-sdk/middleware-sdk-s3": "3.410.0", + "@aws-sdk/middleware-signing": "3.410.0", + "@aws-sdk/middleware-ssec": "3.410.0", + "@aws-sdk/middleware-user-agent": "3.410.0", + "@aws-sdk/signature-v4-multi-region": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-endpoints": "3.410.0", + "@aws-sdk/util-user-agent-browser": "3.410.0", + "@aws-sdk/util-user-agent-node": "3.410.0", + "@aws-sdk/xml-builder": "3.310.0", + "@smithy/config-resolver": "^2.0.7", + "@smithy/eventstream-serde-browser": "^2.0.6", + "@smithy/eventstream-serde-config-resolver": "^2.0.6", + "@smithy/eventstream-serde-node": "^2.0.6", + "@smithy/fetch-http-handler": "^2.1.2", + "@smithy/hash-blob-browser": "^2.0.6", + "@smithy/hash-node": "^2.0.6", + "@smithy/hash-stream-node": "^2.0.6", + "@smithy/invalid-dependency": "^2.0.6", + "@smithy/md5-js": "^2.0.6", + "@smithy/middleware-content-length": "^2.0.8", + "@smithy/middleware-endpoint": "^2.0.6", + "@smithy/middleware-retry": "^2.0.9", + "@smithy/middleware-serde": "^2.0.6", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/node-http-handler": "^2.1.2", + "@smithy/protocol-http": "^3.0.2", + "@smithy/smithy-client": "^2.1.3", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.7", + "@smithy/util-defaults-mode-node": "^2.0.9", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-stream": "^2.0.9", + "@smithy/util-utf8": "^2.0.0", + "@smithy/util-waiter": "^2.0.6", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.410.0.tgz", + "integrity": "sha512-MC9GrgwtlOuSL2WS3DRM3dQ/5y+49KSMMJRH6JiEcU5vE0dX/OtEcX+VfEwpi73x5pSfIjm7xnzjzOFx+sQBIg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.410.0", + "@aws-sdk/middleware-logger": "3.410.0", + "@aws-sdk/middleware-recursion-detection": "3.410.0", + "@aws-sdk/middleware-user-agent": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-endpoints": "3.410.0", + "@aws-sdk/util-user-agent-browser": "3.410.0", + "@aws-sdk/util-user-agent-node": "3.410.0", + "@smithy/config-resolver": "^2.0.7", + "@smithy/fetch-http-handler": "^2.1.2", + "@smithy/hash-node": "^2.0.6", + "@smithy/invalid-dependency": "^2.0.6", + "@smithy/middleware-content-length": "^2.0.8", + "@smithy/middleware-endpoint": "^2.0.6", + "@smithy/middleware-retry": "^2.0.9", + "@smithy/middleware-serde": "^2.0.6", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/node-http-handler": "^2.1.2", + "@smithy/protocol-http": "^3.0.2", + "@smithy/smithy-client": "^2.1.3", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.7", + "@smithy/util-defaults-mode-node": "^2.0.9", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.410.0.tgz", + "integrity": "sha512-e6VMrBJtnTxxUXwDmkADGIvyppmDMFf4+cGGA68tVCUm1cFNlCI6M/67bVSIPN/WVKAAfhEL5O2vVXCM7aatYg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/credential-provider-node": "3.410.0", + "@aws-sdk/middleware-host-header": "3.410.0", + "@aws-sdk/middleware-logger": "3.410.0", + "@aws-sdk/middleware-recursion-detection": "3.410.0", + "@aws-sdk/middleware-sdk-sts": "3.410.0", + "@aws-sdk/middleware-signing": "3.410.0", + "@aws-sdk/middleware-user-agent": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-endpoints": "3.410.0", + "@aws-sdk/util-user-agent-browser": "3.410.0", + "@aws-sdk/util-user-agent-node": "3.410.0", + "@smithy/config-resolver": "^2.0.7", + "@smithy/fetch-http-handler": "^2.1.2", + "@smithy/hash-node": "^2.0.6", + "@smithy/invalid-dependency": "^2.0.6", + "@smithy/middleware-content-length": "^2.0.8", + "@smithy/middleware-endpoint": "^2.0.6", + "@smithy/middleware-retry": "^2.0.9", + "@smithy/middleware-serde": "^2.0.6", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/node-http-handler": "^2.1.2", + "@smithy/protocol-http": "^3.0.2", + "@smithy/smithy-client": "^2.1.3", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.7", + "@smithy/util-defaults-mode-node": "^2.0.9", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.410.0.tgz", + "integrity": "sha512-c7TB9LbN0PkFOsXI0lcRJnqPNOmc4VBvrHf8jP/BkTDg4YUoKQKOFd4d0SqzODmlZiAyoMQVZTR4ISZo95Zj4Q==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.410.0.tgz", + "integrity": "sha512-D8rcr5bRCFD0f42MPQ7K6TWZq5d3pfqrKINL1/bpfkK5BJbvq1BGYmR88UC6CLpTRtZ1LHY2HgYG0fp/2zjjww==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.410.0", + "@aws-sdk/credential-provider-process": "3.410.0", + "@aws-sdk/credential-provider-sso": "3.410.0", + "@aws-sdk/credential-provider-web-identity": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.410.0.tgz", + "integrity": "sha512-0wmVm33T/j1FS7MZ/j+WsPlgSc0YnCXnpbWSov1Mn6R86SHI2b2JhdIPRRE4XbGfyW2QGNUl2CwoZVaqhXeF5g==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.410.0", + "@aws-sdk/credential-provider-ini": "3.410.0", + "@aws-sdk/credential-provider-process": "3.410.0", + "@aws-sdk/credential-provider-sso": "3.410.0", + "@aws-sdk/credential-provider-web-identity": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.410.0.tgz", + "integrity": "sha512-BMju1hlDCDNkkSZpKF5SQ8G0WCLRj6/Jvw9QmudLHJuVwYJXEW1r2AsVMg98OZ3hB9G+MAvHruHZIbMiNmUMXQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.410.0.tgz", + "integrity": "sha512-zEaoY/sY+KYTlQUkp9dvveAHf175b8RIt0DsQkDrRPtrg/RBHR00r5rFvz9+nrwsR8546RaBU7h/zzTaQGhmcA==", + "dependencies": { + "@aws-sdk/client-sso": "3.410.0", + "@aws-sdk/token-providers": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.410.0.tgz", + "integrity": "sha512-cE0l8LmEHdWbDkdPNgrfdYSgp4/cIVXrjUKI1QCATA729CrHZ/OQjB/maOBOrMHO9YTiggko887NkslVvwVB7w==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.410.0.tgz", + "integrity": "sha512-pUGrpFgCKf9fDHu01JJhhw+MUImheS0HFlZwNG37OMubkxUAbCdmYGewGxfTCUvWyZJtx9bVjrSu6gG7w+RARg==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "@smithy/util-config-provider": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.410.0.tgz", + "integrity": "sha512-e5YqGCNmW99GZjEPPujJ02RlEZql19U40oORysBhVF7mKz8BBvF3s8l37tvu37oxebDEkh1u/2cm2+ggOXxLjQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.410.0.tgz", + "integrity": "sha512-IK7KlvEKtrQVBfmAp/MmGd0wbWLuN2GZwwfAmsU0qFb0f5vOVUbKDsu6tudtDKCBG9uXyTEsx3/QGvoK2zDy+g==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/types": "3.410.0", + "@smithy/is-array-buffer": "^2.0.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.410.0.tgz", + "integrity": "sha512-ED/OVcyITln5rrxnajZP+V0PN1nug+gSDHJDqdDo/oLy7eiDr/ZWn3nlWW7WcMplQ1/Jnb+hK0UetBp/25XooA==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.410.0.tgz", + "integrity": "sha512-jAftSpOpw/5AdpOJ/cGiXCb+Vv22KXR5QZmxmllUDsnlm18672tpRaI2plmu/1d98CVvqhY61eSklFMrIf2c4w==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.410.0.tgz", + "integrity": "sha512-YtmKYCVtBfScq3/UFJk+aSZOktKJBNZL9DaSc2aPcy/goCVsYDOkGwtHk0jIkC1JRSNCkVTqL7ya60sSr8zaQQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.410.0.tgz", + "integrity": "sha512-KWaes5FLzRqj28vaIEE4Bimpga2E596WdPF2HaH6zsVMJddoRDsc3ZX9ZhLOGrXzIO1RqBd0QxbLrM0S/B2aOQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.410.0.tgz", + "integrity": "sha512-K2sG2V1ZkezYMCIy3uMt0MwtflcfIwLptwm0iFLaYitiINZQ1tcslk9ggAjyTHg0rslDSI4/zjkhy8VHFOV7HA==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.410.0.tgz", + "integrity": "sha512-YfBpctDocRR4CcROoDueJA7D+aMLBV8nTFfmVNdLLLgyuLZ/AUR11VQSu1lf9gQZKl8IpKE/BLf2fRE/qV1ZuA==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.410.0.tgz", + "integrity": "sha512-KBAZ/eoAJUSJv5us2HsKwK2OszG2s9FEyKpEhgnHLcbbKzW873zHBH5GcOGEQu4AWArTy2ndzJu3FF+9/J9hJQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.3.0", + "@smithy/util-middleware": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.410.0.tgz", + "integrity": "sha512-DNsjVTXoxIh+PuW9o45CFaMiconbuZRm19MC3NA1yNCaCj3ZxD5OdXAutq6UjQdrx8UG4EjUlCJEEvBKmboITw==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.410.0.tgz", + "integrity": "sha512-ZayDtLfvCZUohSxQc/49BfoU/y6bDHLfLdyyUJbJ54Sv8zQcrmdyKvCBFUZwE6tHQgAmv9/ZT18xECMl+xiONA==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-endpoints": "3.410.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.410.0.tgz", + "integrity": "sha512-In2/XPdPA874XH0MdhLJ7tG74Yay/ATCMpMQcy+summlPhmO1G3BiKMoaDPRks+zJNhgiy6++PlcP93fwDSxcA==", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-format-url": "3.410.0", + "@smithy/middleware-endpoint": "^2.0.6", + "@smithy/protocol-http": "^3.0.2", + "@smithy/smithy-client": "^2.1.3", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-crt": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-crt/-/signature-v4-crt-3.410.0.tgz", + "integrity": "sha512-8lt0YG/LzdCNCXM+GNhsYjHdCkH83oM/Di1HOOgZy/u50u0KOb5REiEYBq2TXMzED4BPVgblDhiJviCGqwcWiQ==", + "dependencies": { + "@smithy/querystring-parser": "^2.0.0", + "@smithy/signature-v4": "^2.0.0", + "@smithy/util-middleware": "^2.0.0", + "aws-crt": "^1.15.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.410.0.tgz", + "integrity": "sha512-abgcl9/i9frxGUVAfHHWj49UMCFEmzkYwKmV/4kw9MYn6BZ3HKb5M00tBLn9/PcAKfANS7O+qJRiEQT66rmfhg==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/signature-v4-crt": "^3.118.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/signature-v4-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.410.0.tgz", + "integrity": "sha512-d5Nc0xydkH/X0LA1HDyhGY5sEv4LuADFk+QpDtT8ogLilcre+b1jpdY8Sih/gd1KoGS1H+d1tz2hSGwUHAbUbw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.410.0", + "@aws-sdk/middleware-logger": "3.410.0", + "@aws-sdk/middleware-recursion-detection": "3.410.0", + "@aws-sdk/middleware-user-agent": "3.410.0", + "@aws-sdk/types": "3.410.0", + "@aws-sdk/util-endpoints": "3.410.0", + "@aws-sdk/util-user-agent-browser": "3.410.0", + "@aws-sdk/util-user-agent-node": "3.410.0", + "@smithy/config-resolver": "^2.0.7", + "@smithy/fetch-http-handler": "^2.1.2", + "@smithy/hash-node": "^2.0.6", + "@smithy/invalid-dependency": "^2.0.6", + "@smithy/middleware-content-length": "^2.0.8", + "@smithy/middleware-endpoint": "^2.0.6", + "@smithy/middleware-retry": "^2.0.9", + "@smithy/middleware-serde": "^2.0.6", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/node-http-handler": "^2.1.2", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.2", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/smithy-client": "^2.1.3", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.7", + "@smithy/util-defaults-mode-node": "^2.0.9", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.410.0.tgz", + "integrity": "sha512-D7iaUCszv/v04NDaZUmCmekamy6VD/lKozm/3gS9+dkfU6cC2CsNoUfPV8BlV6dPdw0oWgF91am3I1stdvfVrQ==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz", + "integrity": "sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.410.0.tgz", + "integrity": "sha512-iNiqJyC7N3+8zFwnXUqcWSxrZecVZLToo1iTQQdeYL2af1IcOtRgb7n8jpAI/hmXhBSx2+3RI+Y7pxyFo1vu+w==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.410.0.tgz", + "integrity": "sha512-ftxPYq7RBxJMQrOCJARx8+sQccmG+6y7mm9JzfXOHOfS1aWnYQizTitJ7PMA8p90xrUAFQ2CmjT0jaEGWg5VGQ==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/querystring-builder": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz", + "integrity": "sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.410.0.tgz", + "integrity": "sha512-i1G/XGpXGMRT2zEiAhi1xucJsfCWk8nNYjk/LbC0sA+7B9Huri96YAzVib12wkHPsJQvZxZC6CpQDIHWm4lXMA==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/types": "^2.3.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.410.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.410.0.tgz", + "integrity": "sha512-bK70t1jHRl8HrJXd4hEIwc5PBZ7U0w+81AKFnanIVKZwZedd6nLibUXDTK14z/Jp2GFcBqd4zkt2YLGkRt/U4A==", + "dependencies": { + "@aws-sdk/types": "3.410.0", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz", + "integrity": "sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -1683,6 +2452,53 @@ "react-hook-form": "^7.0.0" } }, + "node_modules/@httptoolkit/websocket-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@httptoolkit/websocket-stream/-/websocket-stream-6.0.1.tgz", + "integrity": "sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==", + "dependencies": { + "@types/ws": "*", + "duplexify": "^3.5.1", + "inherits": "^2.0.1", + "isomorphic-ws": "^4.0.1", + "readable-stream": "^2.3.3", + "safe-buffer": "^5.1.2", + "ws": "*", + "xtend": "^4.0.0" + } + }, + "node_modules/@httptoolkit/websocket-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/@httptoolkit/websocket-stream/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==", + "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/@httptoolkit/websocket-stream/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==" + }, + "node_modules/@httptoolkit/websocket-stream/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==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -4183,6 +4999,14 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", "integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==" }, + "node_modules/@scure/base": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", + "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.10.0.tgz", @@ -4195,6 +5019,650 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.6.tgz", + "integrity": "sha512-4I7g0lyGUlW2onf8mD76IzU37oRWSHsQ5zlW5MjDzgg4I4J9bOK4500Gx6qOuoN7+GulAnGLe1YwyrIluzhakg==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.0.0.tgz", + "integrity": "sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.0.0.tgz", + "integrity": "sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==", + "dependencies": { + "@smithy/util-base64": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.7.tgz", + "integrity": "sha512-J4J1AWiqaApC+3I9U++SuxAQ3BOoM5VoYnpFzCZcb63aLF80Zpc/nq2pFR1OsEIYyg2UYNdcBKKfHABmwo4WgQ==", + "dependencies": { + "@smithy/node-config-provider": "^2.0.9", + "@smithy/types": "^2.3.0", + "@smithy/util-config-provider": "^2.0.0", + "@smithy/util-middleware": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.9.tgz", + "integrity": "sha512-K7WZRkHS5HZofRgK+O8W4YXXyaVexU1K6hp9vlUL/8CsnrFbZS9quyH/6hTROrYh2PuJr24yii1kc83NJdxMGQ==", + "dependencies": { + "@smithy/node-config-provider": "^2.0.9", + "@smithy/property-provider": "^2.0.7", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.6.tgz", + "integrity": "sha512-J9xL82mlYRUMXFnB9VaThXkD7z2JLr52FIVZMoQQ1dxZG5ub+NOGmzaTTZC/cMmKXI/nwCoFuwDWCTjwQhYhQA==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.3.0", + "@smithy/util-hex-encoding": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.6.tgz", + "integrity": "sha512-cNJqAkmArHytV0CjBka3CKnU/J6zNlOZynvo2Txj98a0cxKeug8gL6SQTpoTyGk+M4LicjcrzQtDs06mU8U0Ag==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.6.tgz", + "integrity": "sha512-jODu0MWaP06kzBMUtSd4Ga3S2DnTp3tfjPgdjaw9K/Z4yI7J9rUB73aNGo6ZxxH/vl/k66b5NZJ/3O1AzZ4ggw==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.6.tgz", + "integrity": "sha512-ua7ok1g16p7OGAVZntn1l3wegN8RtsyPBl9ebqEDeSxdm+iuEfkAS1E/JFs6S6UBfr8Z0tbql5jTT9iVwIFGGA==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.6.tgz", + "integrity": "sha512-bH1TElelS8tlqll6cJAWKM11Es+pE9htRzjiiFG1+xcyKaM90UFNRX5oKZIrJugZlmP37pvfRwSJ/3ZaaqSBIA==", + "dependencies": { + "@smithy/eventstream-codec": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.1.2.tgz", + "integrity": "sha512-3Gm3pQm4viUPU+e7KkRScS9t5phBxSNRS8rQSZ+HeCwK/busrX0/2HJZiwLvGblqPqi1laJB0lD18AdiOioJww==", + "dependencies": { + "@smithy/protocol-http": "^3.0.2", + "@smithy/querystring-builder": "^2.0.6", + "@smithy/types": "^2.3.0", + "@smithy/util-base64": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-2.0.6.tgz", + "integrity": "sha512-zmJCRb80WDthCZqQ9LiKeFUEmyPM9WUcd0jYa7tlU3p0LsDnaFKuUS+MT0uJehPGyUEicbi1KBdUmtoqEAQr1A==", + "dependencies": { + "@smithy/chunked-blob-reader": "^2.0.0", + "@smithy/chunked-blob-reader-native": "^2.0.0", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.6.tgz", + "integrity": "sha512-xz7fzFxSzxohKGGyKPbLReRrY01JOZgRDHIXSks3PxQxG9c8PJMa5nUw0stH8UOySUgkofmMy0n7vTUsF5Mdqg==", + "dependencies": { + "@smithy/types": "^2.3.0", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-2.0.6.tgz", + "integrity": "sha512-BWtWJ8Ppc8z+Rz9XBu4Hcl+pC+9BKV5GvbQpXZf4IsQX6oTwqo0qJK7Lwe5mYM0hRnqgwjn2mhQ303fIRN7AMw==", + "dependencies": { + "@smithy/types": "^2.3.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.6.tgz", + "integrity": "sha512-L5MUyl9mzawIvBxr0Hg3J/Q5qZFXKcBgMk0PacfK3Mthp4WAR6h7iMxdSQ23Q7X/kxOrpZuoYEdh1BWLKbDc8Q==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", + "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-2.0.6.tgz", + "integrity": "sha512-Ek2qSFFICJa2E0RRVsIkQ6c1jeJTESwF24SMh3liKFNbr2Ax4uJiWsLhDBDQFOhJwjp1mbC4lN85isfGS+KhQg==", + "dependencies": { + "@smithy/types": "^2.3.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.8.tgz", + "integrity": "sha512-fHJFsscHXrYhUSWMFJNXfsZW8KsyhWQfBgU3b0nvDfpm+NAeQLqKYNhywGrDwZQc1k+lt7Fw9faAquhNPxTZRA==", + "dependencies": { + "@smithy/protocol-http": "^3.0.2", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.0.6.tgz", + "integrity": "sha512-MuSPPtEHFal/M77tR3ffLsdOfX29IZpA990nGuoPj5zQnAYrA4PYBGoqqrASQKm8Xb3C0NwuYzOATT7WX4f5Pg==", + "dependencies": { + "@smithy/middleware-serde": "^2.0.6", + "@smithy/types": "^2.3.0", + "@smithy/url-parser": "^2.0.6", + "@smithy/util-middleware": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.9.tgz", + "integrity": "sha512-gneEqWj4l/ZjHdZPk0BFMXoTalRArdQ8i579/KqJgBAc6Ux5vnR/SSppkMCkj2kOQYwdypvzSPeqEW3ZrvIg6g==", + "dependencies": { + "@smithy/node-config-provider": "^2.0.9", + "@smithy/protocol-http": "^3.0.2", + "@smithy/service-error-classification": "^2.0.0", + "@smithy/types": "^2.3.0", + "@smithy/util-middleware": "^2.0.0", + "@smithy/util-retry": "^2.0.0", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.6.tgz", + "integrity": "sha512-8/GODBngYbrS28CMZtaHIL4R9rLNSQ/zgb+N1OAZ02NwBUawlnLDcatve9YRzhJC/IWz0/pt+WimJZaO1sGcig==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.0.tgz", + "integrity": "sha512-31XC1xNF65nlbc16yuh3wwTudmqs6qy4EseQUGF8A/p2m/5wdd/cnXJqpniy/XvXVwkHPz/GwV36HqzHtIKATQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.0.9.tgz", + "integrity": "sha512-TlSPbCwtT/jgNnmPQqKuCR5CFN8UIrCCHRrgUfs3NqRMuaLLeP8TPe1fSKq2J8h1M/jd4BF853gneles0gWevg==", + "dependencies": { + "@smithy/property-provider": "^2.0.7", + "@smithy/shared-ini-file-loader": "^2.0.8", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.1.2.tgz", + "integrity": "sha512-PdEEDCShuM8zxGoaRxmGB/1ikB8oeqz+ZAF9VIA8FCP3E59j8zDTF+wCELoWd1Y6gtxr+RcTAg5sA8nvn5qH/w==", + "dependencies": { + "@smithy/abort-controller": "^2.0.6", + "@smithy/protocol-http": "^3.0.2", + "@smithy/querystring-builder": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.7.tgz", + "integrity": "sha512-XT8Tl7YNxM8tCtGqy7v7DSf6PxyXaPE9cdA/Yj4dEw2b05V3RrPqsP+t5XJiZu0yIsQ7pdeYZWv2sSEWVjNeAg==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.2.tgz", + "integrity": "sha512-LUOWCPRihvJBkdSs+ivK9m1f/rMfF3n9Zpzg8qdry2eIG4HQqqLBMWQyF9bgk7JhsrrOa3//jJKhXzvL7wL5Xw==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.6.tgz", + "integrity": "sha512-HnU00shCGoV8vKJZTiNBkNvR9NogU3NIUaVMAGJPSqNGJj3psWo+TUrC0BVCDcwiCljXwXCFGJqIcsWtClrktQ==", + "dependencies": { + "@smithy/types": "^2.3.0", + "@smithy/util-uri-escape": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.6.tgz", + "integrity": "sha512-i4LKoXHP7pTFAPjLIJyQXYOhWokbcFha3WWsX74sAKmuluv0XM2cxONZoFxwEzmWhsNyM6buSwJSZXyPiec0AQ==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.0.tgz", + "integrity": "sha512-2z5Nafy1O0cTf69wKyNjGW/sNVMiqDnb4jgwfMG8ye8KnFJ5qmJpDccwIbJNhXIfbsxTg9SEec2oe1cexhMJvw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.0.8.tgz", + "integrity": "sha512-4u+V+Dv7JGpJ0tppB5rxCem7WhdFux950z4cGPhV0kHTPkKe8DDgINzOlVa2RBu5dI33D02OBJcxFjhW4FPORg==", + "dependencies": { + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.6.tgz", + "integrity": "sha512-4zNTi8w4sky07YKq7oYucZt4ogY00IEaS1NFDXxmCN5V/ywE0WiK+WMim+8wtYQmB0qy3oExZR4LoCAml6j/rA==", + "dependencies": { + "@smithy/eventstream-codec": "^2.0.6", + "@smithy/is-array-buffer": "^2.0.0", + "@smithy/types": "^2.3.0", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-middleware": "^2.0.0", + "@smithy/util-uri-escape": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.1.3.tgz", + "integrity": "sha512-nSMMp2AKqcG/ruzCY01ogrMdbq/WS1cvGStTsw7yd6bTpp/bGtlOgXvy3h7e0zP7w2DH1AtvIwzYBD6ejZePsQ==", + "dependencies": { + "@smithy/middleware-stack": "^2.0.0", + "@smithy/types": "^2.3.0", + "@smithy/util-stream": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.3.0.tgz", + "integrity": "sha512-pJce3rd39MElkV57UTPAoSYAApjQLELUxjU5adHNLYk9gnPvyIGbJNJTZVVFu00BrgZH3W/cQe8QuFcknDyodQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.6.tgz", + "integrity": "sha512-9i6j5QW6bapHZ4rtkXOAm0hOUG1+5IVdVJXNSUTcNskwJchZH5IQuDNPCbgUi/u2P8EZazKt4wXT51QxOXCz1A==", + "dependencies": { + "@smithy/querystring-parser": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.0.tgz", + "integrity": "sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==", + "dependencies": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz", + "integrity": "sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", + "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", + "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", + "dependencies": { + "@smithy/is-array-buffer": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz", + "integrity": "sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.7.tgz", + "integrity": "sha512-s1caKxC7Y87Q72Goll//clZs2WNBfG9WtFDWVRS+Qgk147YPCOUYtkpuD0XZAh/vbayObFz5tQ1fiX4G19HSCA==", + "dependencies": { + "@smithy/property-provider": "^2.0.7", + "@smithy/types": "^2.3.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.9.tgz", + "integrity": "sha512-HlV4iNL3/PgPpmDGs0+XrAKtwFQ8rOs5P2y5Dye8dUYaJauadlzHRrNKk7wH2aBYswvT2HM+PIgXamvrE7xbcw==", + "dependencies": { + "@smithy/config-resolver": "^2.0.7", + "@smithy/credential-provider-imds": "^2.0.9", + "@smithy/node-config-provider": "^2.0.9", + "@smithy/property-provider": "^2.0.7", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", + "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.0.tgz", + "integrity": "sha512-eCWX4ECuDHn1wuyyDdGdUWnT4OGyIzV0LN1xRttBFMPI9Ff/4heSHVxneyiMtOB//zpXWCha1/SWHJOZstG7kA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.0.tgz", + "integrity": "sha512-/dvJ8afrElasuiiIttRJeoS2sy8YXpksQwiM/TcepqdRVp7u4ejd9C4IQURHNjlfPUT7Y6lCDSa2zQJbdHhVTg==", + "dependencies": { + "@smithy/service-error-classification": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.9.tgz", + "integrity": "sha512-Fn2/3IMwqu0l2hOC7K3bbtSqFEJ6nOzMLoPVIhuH84yw/95itNkFBwVbIIiAfDaout0ZfZ26+5ch86E2q3avww==", + "dependencies": { + "@smithy/fetch-http-handler": "^2.1.2", + "@smithy/node-http-handler": "^2.1.2", + "@smithy/types": "^2.3.0", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", + "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.0.tgz", + "integrity": "sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==", + "dependencies": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.0.6.tgz", + "integrity": "sha512-wjxvKB4XSfgpOg3lr4RulnVhd21fMMC4CPARBwrSN7+3U28fwOifv8f7T+Ibay9DAQTj9qXxmd8ag6WXBRgNhg==", + "dependencies": { + "@smithy/abort-controller": "^2.0.6", + "@smithy/types": "^2.3.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", @@ -4607,6 +6075,14 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -5204,6 +6680,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-crt": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/aws-crt/-/aws-crt-1.18.0.tgz", + "integrity": "sha512-H5Vrb/GMzq72+Of2zrW69i/BTQ4gQd3MQvdZ3X3okfppzHdEjSPkdJN6ia8V2/1J1FmFvEtoxaY4nwraHUGQvg==", + "hasInstallScript": true, + "dependencies": { + "@aws-sdk/util-utf8-browser": "^3.109.0", + "@httptoolkit/websocket-stream": "^6.0.0", + "axios": "^0.24.0", + "buffer": "^6.0.3", + "crypto-js": "^4.0.0", + "mqtt": "^4.3.7", + "process": "^0.11.10" + } + }, + "node_modules/aws-crt/node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "node_modules/aws-crt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/axe-core": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", @@ -5306,6 +6828,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -6166,6 +7693,15 @@ "node": ">= 6" } }, + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -6181,6 +7717,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/condense-newlines": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", @@ -6391,6 +7941,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/css-unit-converter": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", @@ -6548,6 +8103,7 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -6967,6 +8523,49 @@ "node": ">=12" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/duplexify/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==", + "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/duplexify/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==" + }, + "node_modules/duplexify/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==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8193,6 +9792,27 @@ "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" }, + "node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -9000,6 +10620,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -9728,6 +11357,14 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -9828,6 +11465,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9976,6 +11622,14 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11551,6 +13205,79 @@ "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" }, + "node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/mqtt/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -11960,6 +13687,15 @@ "set-blocking": "^2.0.0" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -12925,6 +14661,19 @@ "node": ">=16.13" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "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/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13637,6 +15386,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, "node_modules/remark-frontmatter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz", @@ -13870,8 +15624,7 @@ "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, "node_modules/rimraf": { "version": "3.0.2", @@ -14388,7 +16141,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, "dependencies": { "readable-stream": "^3.0.0" } @@ -14424,6 +16176,11 @@ "node": ">= 0.6" } }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -14622,6 +16379,11 @@ "node": ">=12.*" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/style-to-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz", @@ -15724,6 +17486,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -16227,6 +17994,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16332,13 +18119,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@documenso/tailwind-config": "*", - "@documenso/tsconfig": "*", - "@documenso/ui": "*", "@react-email/components": "^0.0.7", "nodemailer": "^6.9.3" }, "devDependencies": { + "@documenso/tailwind-config": "*", + "@documenso/tsconfig": "*", "@types/nodemailer": "^6.4.8", "tsup": "^7.1.0" } @@ -16365,10 +18151,15 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@aws-sdk/client-s3": "^3.410.0", + "@aws-sdk/s3-request-presigner": "^3.410.0", + "@aws-sdk/signature-v4-crt": "^3.410.0", "@documenso/email": "*", "@documenso/prisma": "*", "@next-auth/prisma-adapter": "1.0.7", "@pdf-lib/fontkit": "^1.1.1", + "@scure/base": "^1.1.3", + "@sindresorhus/slugify": "^2.2.1", "@upstash/redis": "^1.20.6", "bcrypt": "^5.1.0", "luxon": "^3.4.0", @@ -16451,6 +18242,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@documenso/lib": "*", "@radix-ui/react-accordion": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.0.3", "@radix-ui/react-aspect-ratio": "^1.0.2", @@ -16480,7 +18272,6 @@ "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", "cmdk": "^0.2.0", - "date-fns": "^2.30.0", "framer-motion": "^10.12.8", "lucide-react": "^0.214.0", "next": "13.4.12", @@ -16491,6 +18282,7 @@ "tailwindcss-animate": "^1.0.5" }, "devDependencies": { + "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", diff --git a/packages/email/package.json b/packages/email/package.json index 39e2ca67a..c1751dce6 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -16,13 +16,12 @@ "worker:test": "tsup worker/index.ts --format esm" }, "dependencies": { - "@documenso/tsconfig": "*", - "@documenso/tailwind-config": "*", - "@documenso/ui": "*", "@react-email/components": "^0.0.7", "nodemailer": "^6.9.3" }, "devDependencies": { + "@documenso/tsconfig": "*", + "@documenso/tailwind-config": "*", "@types/nodemailer": "^6.4.8", "tsup": "^7.1.0" } diff --git a/packages/email/tailwind.config.js b/packages/email/tailwind.config.js index 81816bdfb..2e138b912 100644 --- a/packages/email/tailwind.config.js +++ b/packages/email/tailwind.config.js @@ -4,8 +4,5 @@ const path = require('path'); module.exports = { ...baseConfig, - content: [ - `templates/**/*.{ts,tsx}`, - `${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`, - ], + content: [`templates/**/*.{ts,tsx}`], }; diff --git a/packages/lib/constants/time.ts b/packages/lib/constants/time.ts new file mode 100644 index 000000000..e2581e14c --- /dev/null +++ b/packages/lib/constants/time.ts @@ -0,0 +1,5 @@ +export const ONE_SECOND = 1000; +export const ONE_MINUTE = ONE_SECOND * 60; +export const ONE_HOUR = ONE_MINUTE * 60; +export const ONE_DAY = ONE_HOUR * 24; +export const ONE_WEEK = ONE_DAY * 7; diff --git a/packages/lib/package.json b/packages/lib/package.json index e36297834..757ab7932 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -12,12 +12,15 @@ ], "scripts": {}, "dependencies": { - "@aws-sdk/s3-request-presigner": "^3.405.0", - "@aws-sdk/client-s3": "^3.405.0", + "@aws-sdk/client-s3": "^3.410.0", + "@aws-sdk/s3-request-presigner": "^3.410.0", + "@aws-sdk/signature-v4-crt": "^3.410.0", "@documenso/email": "*", "@documenso/prisma": "*", "@next-auth/prisma-adapter": "1.0.7", "@pdf-lib/fontkit": "^1.1.1", + "@scure/base": "^1.1.3", + "@sindresorhus/slugify": "^2.2.1", "@upstash/redis": "^1.20.6", "bcrypt": "^5.1.0", "luxon": "^3.4.0", diff --git a/packages/lib/server-only/document-data/create-document-data.ts b/packages/lib/server-only/document-data/create-document-data.ts new file mode 100644 index 000000000..e41f00fe7 --- /dev/null +++ b/packages/lib/server-only/document-data/create-document-data.ts @@ -0,0 +1,19 @@ +'use server'; + +import { prisma } from '@documenso/prisma'; +import { DocumentDataType } from '@documenso/prisma/client'; + +export type CreateDocumentDataOptions = { + type: DocumentDataType; + data: string; +}; + +export const createDocumentData = async ({ type, data }: CreateDocumentDataOptions) => { + return await prisma.documentData.create({ + data: { + type, + data, + initialData: data, + }, + }); +}; diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index 24a5d6283..b84f8e46e 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -1,10 +1,19 @@ 'use server'; +import { prisma } from '@documenso/prisma'; + export type CreateDocumentOptions = { + title: string; userId: number; - fileName: string; + documentDataId: string; }; -export const createDocument = () => { - // +export const createDocument = async ({ userId, title, documentDataId }: CreateDocumentOptions) => { + return await prisma.document.create({ + data: { + title, + documentDataId, + userId, + }, + }); }; diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 876da9d0a..883d13e6f 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -1,10 +1,13 @@ 'use server'; +import path from 'node:path'; import { PDFDocument } from 'pdf-lib'; import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; +import { getFile } from '../../universal/upload/get-file'; +import { putFile } from '../../universal/upload/put-file'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; export type SealDocumentOptions = { @@ -23,7 +26,9 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { }, }); - if (!document.documentData) { + const { documentData } = document; + + if (!documentData) { throw new Error(`Document ${document.id} has no document data`); } @@ -55,7 +60,7 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { } // !: Need to write the fields onto the document as a hard copy - const { data: pdfData } = document.documentData; + const pdfData = await getFile(documentData); const doc = await PDFDocument.load(pdfData); @@ -65,17 +70,20 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { const pdfBytes = await doc.save(); - await prisma.document.update({ + const { name, ext } = path.parse(document.title); + + const { data: newData } = await putFile({ + name: `${name}_signed${ext}`, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(Buffer.from(pdfBytes)), + }); + + await prisma.documentData.update({ where: { - id: document.id, - status: DocumentStatus.COMPLETED, + id: documentData.id, }, data: { - documentData: { - update: { - data: Buffer.from(pdfBytes).toString('base64'), - }, - }, + data: newData, }, }); }; diff --git a/packages/lib/server-only/recipient/set-recipients-for-document.ts b/packages/lib/server-only/recipient/set-recipients-for-document.ts index 15db42084..c34885143 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-document.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-document.ts @@ -1,8 +1,8 @@ -import { nanoid } from 'nanoid'; - import { prisma } from '@documenso/prisma'; import { SendStatus, SigningStatus } from '@documenso/prisma/client'; +import { nanoid } from '../../universal/id'; + export interface SetRecipientsForDocumentOptions { userId: number; documentId: number; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 0f63d1612..fdefbd544 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@documenso/tsconfig/react-library.json", + "compilerOptions": { + "types": ["@documenso/tsconfig/process-env.d.ts"] + }, "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"], "exclude": ["dist", "build", "node_modules"] } diff --git a/packages/lib/universal/id.ts b/packages/lib/universal/id.ts new file mode 100644 index 000000000..13738233e --- /dev/null +++ b/packages/lib/universal/id.ts @@ -0,0 +1,5 @@ +import { customAlphabet } from 'nanoid'; + +export const alphaid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 10); + +export { nanoid } from 'nanoid'; diff --git a/packages/lib/universal/upload/delete-file.ts b/packages/lib/universal/upload/delete-file.ts new file mode 100644 index 000000000..69f93d7a0 --- /dev/null +++ b/packages/lib/universal/upload/delete-file.ts @@ -0,0 +1,22 @@ +import { match } from 'ts-pattern'; + +import { DocumentDataType } from '@documenso/prisma/client'; + +import { deleteS3File } from './server-actions'; + +export type DeleteFileOptions = { + type: DocumentDataType; + data: string; +}; + +export const deleteFile = async ({ type, data }: DeleteFileOptions) => { + return await match(type) + .with(DocumentDataType.S3_PATH, async () => deleteFileFromS3(data)) + .otherwise(() => { + return; + }); +}; + +const deleteFileFromS3 = async (key: string) => { + await deleteS3File(key); +}; diff --git a/packages/lib/universal/upload/get-file.ts b/packages/lib/universal/upload/get-file.ts new file mode 100644 index 000000000..2c71c3774 --- /dev/null +++ b/packages/lib/universal/upload/get-file.ts @@ -0,0 +1,45 @@ +import { base64 } from '@scure/base'; +import { match } from 'ts-pattern'; + +import { DocumentDataType } from '@documenso/prisma/client'; + +import { getPresignGetUrl } from './server-actions'; + +export type GetFileOptions = { + type: DocumentDataType; + data: string; +}; + +export const getFile = async ({ type, data }: GetFileOptions) => { + return await match(type) + .with(DocumentDataType.BYTES, () => getFileFromBytes(data)) + .with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data)) + .with(DocumentDataType.S3_PATH, async () => getFileFromS3(data)) + .exhaustive(); +}; + +const getFileFromBytes = (data: string) => { + const encoder = new TextEncoder(); + + const binaryData = encoder.encode(data); + + return binaryData; +}; + +const getFileFromBytes64 = (data: string) => { + const binaryData = base64.decode(data); + + return binaryData; +}; + +const getFileFromS3 = async (key: string) => { + const { url } = await getPresignGetUrl(key); + + const buffer = await fetch(url, { + method: 'GET', + }).then(async (res) => res.arrayBuffer()); + + const binaryData = new Uint8Array(buffer); + + return binaryData; +}; diff --git a/packages/lib/universal/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts new file mode 100644 index 000000000..ccfa96e7b --- /dev/null +++ b/packages/lib/universal/upload/put-file.ts @@ -0,0 +1,53 @@ +import { base64 } from '@scure/base'; +import { match } from 'ts-pattern'; + +import { DocumentDataType } from '@documenso/prisma/client'; + +import { createDocumentData } from '../../server-only/document-data/create-document-data'; +import { getPresignPostUrl } from './server-actions'; + +type File = { + name: string; + type: string; + arrayBuffer: () => Promise; +}; + +export const putFile = async (file: File) => { + const { type, data } = await match(process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT) + .with('s3', async () => putFileInS3(file)) + .otherwise(async () => putFileInDatabase(file)); + + return await createDocumentData({ type, data }); +}; + +const putFileInDatabase = async (file: File) => { + const contents = await file.arrayBuffer(); + + const binaryData = new Uint8Array(contents); + + const asciiData = base64.encode(binaryData); + + return { + type: DocumentDataType.BYTES_64, + data: asciiData, + }; +}; + +const putFileInS3 = async (file: File) => { + const { url, key } = await getPresignPostUrl(file.name, file.type); + + const body = await file.arrayBuffer(); + + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/octet-stream', + }, + body, + }); + + return { + type: DocumentDataType.S3_PATH, + data: key, + }; +}; diff --git a/packages/lib/universal/upload/server-actions.ts b/packages/lib/universal/upload/server-actions.ts new file mode 100644 index 000000000..629d62a2a --- /dev/null +++ b/packages/lib/universal/upload/server-actions.ts @@ -0,0 +1,104 @@ +'use server'; + +import { + DeleteObjectCommand, + GetObjectCommand, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import slugify from '@sindresorhus/slugify'; +import path from 'node:path'; + +import { ONE_HOUR, ONE_SECOND } from '../../constants/time'; +import { getServerComponentSession } from '../../next-auth/get-server-session'; +import { alphaid } from '../id'; + +export const getPresignPostUrl = async (fileName: string, contentType: string) => { + const client = getS3Client(); + + const user = await getServerComponentSession(); + + // Get the basename and extension for the file + const { name, ext } = path.parse(fileName); + + let key = `${alphaid(12)}/${slugify(name)}${ext}`; + + if (user) { + key = `${user.id}/${key}`; + } + + const putObjectCommand = new PutObjectCommand({ + Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET, + Key: key, + ContentType: contentType, + }); + + const url = await getSignedUrl(client, putObjectCommand, { + expiresIn: ONE_HOUR / ONE_SECOND, + }); + + return { key, url }; +}; + +export const getAbsolutePresignPostUrl = async (key: string) => { + const client = getS3Client(); + + const putObjectCommand = new PutObjectCommand({ + Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET, + Key: key, + }); + + const url = await getSignedUrl(client, putObjectCommand, { + expiresIn: ONE_HOUR / ONE_SECOND, + }); + + return { key, url }; +}; + +export const getPresignGetUrl = async (key: string) => { + const client = getS3Client(); + + const getObjectCommand = new GetObjectCommand({ + Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET, + Key: key, + }); + + const url = await getSignedUrl(client, getObjectCommand, { + expiresIn: ONE_HOUR / ONE_SECOND, + }); + + return { key, url }; +}; + +export const deleteS3File = async (key: string) => { + const client = getS3Client(); + + await client.send( + new DeleteObjectCommand({ + Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET, + Key: key, + }), + ); +}; + +const getS3Client = () => { + if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') { + throw new Error('Invalid upload transport'); + } + + const hasCredentials = + process.env.NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID && + process.env.NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY; + + return new S3Client({ + endpoint: process.env.NEXT_PRIVATE_UPLOAD_ENDPOINT || undefined, + region: process.env.NEXT_PRIVATE_UPLOAD_REGION || 'us-east-1', + credentials: hasCredentials + ? { + accessKeyId: String(process.env.NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID), + secretAccessKey: String(process.env.NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY), + } + : undefined, + }); +}; diff --git a/packages/lib/universal/upload/update-file.ts b/packages/lib/universal/upload/update-file.ts new file mode 100644 index 000000000..a7a227bab --- /dev/null +++ b/packages/lib/universal/upload/update-file.ts @@ -0,0 +1,54 @@ +import { base64 } from '@scure/base'; +import { match } from 'ts-pattern'; + +import { DocumentDataType } from '@documenso/prisma/client'; + +import { getAbsolutePresignPostUrl } from './server-actions'; + +export type UpdateFileOptions = { + type: DocumentDataType; + oldData: string; + newData: string; +}; + +export const updateFile = async ({ type, oldData, newData }: UpdateFileOptions) => { + return await match(type) + .with(DocumentDataType.BYTES, () => updateFileWithBytes(newData)) + .with(DocumentDataType.BYTES_64, () => updateFileWithBytes64(newData)) + .with(DocumentDataType.S3_PATH, async () => updateFileWithS3(oldData, newData)) + .exhaustive(); +}; + +const updateFileWithBytes = (data: string) => { + return { + type: DocumentDataType.BYTES, + data, + }; +}; + +const updateFileWithBytes64 = (data: string) => { + const encoder = new TextEncoder(); + + const binaryData = encoder.encode(data); + + const asciiData = base64.encode(binaryData); + + return { + type: DocumentDataType.BYTES_64, + data: asciiData, + }; +}; + +const updateFileWithS3 = async (key: string, data: string) => { + const { url } = await getAbsolutePresignPostUrl(key); + + await fetch(url, { + method: 'PUT', + body: data, + }); + + return { + type: DocumentDataType.S3_PATH, + data: key, + }; +}; diff --git a/packages/prisma/migrations/20230912011344_reverse_document_data_relation/migration.sql b/packages/prisma/migrations/20230912011344_reverse_document_data_relation/migration.sql new file mode 100644 index 000000000..7a02d4cfe --- /dev/null +++ b/packages/prisma/migrations/20230912011344_reverse_document_data_relation/migration.sql @@ -0,0 +1,23 @@ +-- DropForeignKey +ALTER TABLE "DocumentData" DROP CONSTRAINT "DocumentData_documentId_fkey"; + +-- DropIndex +DROP INDEX "DocumentData_documentId_key"; + +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "documentDataId" TEXT; + +-- Reverse relation foreign key ids +UPDATE "Document" SET "documentDataId" = "DocumentData"."id" FROM "DocumentData" WHERE "Document"."id" = "DocumentData"."documentId"; + +-- AlterColumn +ALTER TABLE "Document" ALTER COLUMN "documentDataId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "DocumentData" DROP COLUMN "documentId"; + +-- CreateIndex +CREATE UNIQUE INDEX "Document_documentDataId_key" ON "Document"("documentDataId"); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_documentDataId_fkey" FOREIGN KEY ("documentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 5171813c3..41f1b32fb 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -85,17 +85,20 @@ enum DocumentStatus { } model Document { - id Int @id @default(autoincrement()) - created DateTime @default(now()) - userId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - title String - status DocumentStatus @default(DRAFT) - Recipient Recipient[] - Field Field[] - documentData DocumentData? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt @default(now()) + id Int @id @default(autoincrement()) + created DateTime @default(now()) + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String + status DocumentStatus @default(DRAFT) + Recipient Recipient[] + Field Field[] + documentDataId String + documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@unique([documentDataId]) } enum DocumentDataType { @@ -109,11 +112,7 @@ model DocumentData { type DocumentDataType data String initialData String - documentId Int - - Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) - - @@unique([documentId]) + Document Document? } enum ReadStatus { diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 5628bb41d..e436bb391 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,5 +1,6 @@ import { TRPCError } from '@trpc/server'; +import { createDocument } from '@documenso/lib/server-only/document/create-document'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; @@ -8,6 +9,7 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s import { authenticatedProcedure, procedure, router } from '../trpc'; import { + ZCreateDocumentMutationSchema, ZGetDocumentByIdQuerySchema, ZGetDocumentByTokenQuerySchema, ZSendDocumentMutationSchema, @@ -22,11 +24,6 @@ export const documentRouter = router({ try { const { id } = input; - console.log({ - id, - userId: ctx.user.id, - }); - return await getDocumentById({ id, userId: ctx.user.id, @@ -58,6 +55,27 @@ export const documentRouter = router({ } }), + createDocument: authenticatedProcedure + .input(ZCreateDocumentMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { title, documentDataId } = input; + + return await createDocument({ + userId: ctx.user.id, + title, + documentDataId, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to create this document. Please try again later.', + }); + } + }), + setRecipientsForDocument: authenticatedProcedure .input(ZSetRecipientsForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 9060ef1db..c95417306 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -14,6 +14,13 @@ export const ZGetDocumentByTokenQuerySchema = z.object({ export type TGetDocumentByTokenQuerySchema = z.infer; +export const ZCreateDocumentMutationSchema = z.object({ + title: z.string().min(1), + documentDataId: z.string().min(1), +}); + +export type TCreateDocumentMutationSchema = z.infer; + export const ZSetRecipientsForDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array( diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index b65a2bb20..b0852b4f4 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -13,7 +13,7 @@ declare namespace NodeJS { NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; - NEXT_PRIVATE_UPLOAD_TRANSPORT?: 'database' | 's3'; + NEXT_PUBLIC_UPLOAD_TRANSPORT?: 'database' | 's3'; NEXT_PRIVATE_UPLOAD_ENDPOINT?: string; NEXT_PRIVATE_UPLOAD_REGION?: string; NEXT_PRIVATE_UPLOAD_BUCKET?: string; diff --git a/packages/ui/package.json b/packages/ui/package.json index 039687491..5a60f6c07 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,6 +15,7 @@ "lint": "eslint \"**/*.ts*\"" }, "devDependencies": { + "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", @@ -22,6 +23,7 @@ "typescript": "^5.1.6" }, "dependencies": { + "@documenso/lib": "*", "@radix-ui/react-accordion": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.0.3", "@radix-ui/react-aspect-ratio": "^1.0.2", @@ -51,7 +53,6 @@ "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", "cmdk": "^0.2.0", - "date-fns": "^2.30.0", "framer-motion": "^10.12.8", "lucide-react": "^0.214.0", "next": "13.4.12", @@ -61,4 +62,4 @@ "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5" } -} \ No newline at end of file +} diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 5de43c411..ac938b41a 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -5,12 +5,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Caveat } from 'next/font/google'; import { Check, ChevronsUpDown, Info } from 'lucide-react'; -import { nanoid } from 'nanoid'; import { useFieldArray, useForm } from 'react-hook-form'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { nanoid } from '@documenso/lib/universal/id'; import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index fe52627fd..412fbfc19 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -5,9 +5,9 @@ import React, { useId } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; import { Plus, Trash } from 'lucide-react'; -import { nanoid } from 'nanoid'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { nanoid } from '@documenso/lib/universal/id'; import { Field, Recipient, SendStatus } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; diff --git a/turbo.json b/turbo.json index f7d3d342c..10a39ddf1 100644 --- a/turbo.json +++ b/turbo.json @@ -33,6 +33,12 @@ "NEXT_PRIVATE_NEXT_AUTH_SECRET", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", "NEXT_PRIVATE_GOOGLE_CLIENT_SECRET", + "NEXT_PUBLIC_UPLOAD_TRANSPORT", + "NEXT_PRIVATE_UPLOAD_ENDPOINT", + "NEXT_PRIVATE_UPLOAD_REGION", + "NEXT_PRIVATE_UPLOAD_BUCKET", + "NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID", + "NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY", "NEXT_PRIVATE_SMTP_TRANSPORT", "NEXT_PRIVATE_MAILCHANNELS_API_KEY", "NEXT_PRIVATE_MAILCHANNELS_ENDPOINT", From f8534b2c3d04b19bdff698b8a2d8b1fcda5bf4bf Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 12:51:59 +1000 Subject: [PATCH 21/32] fix: add dashboard header border on scroll --- .../app/(dashboard)/settings/billing/page.tsx | 14 ++++++++------ .../components/(dashboard)/layout/header.tsx | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index e9966b9ac..555c645ce 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -21,16 +21,18 @@ export default async function BillingSettingsPage() { redirect('/settings/profile'); } - let subscription = await getSubscriptionByUserId({ userId: user.id }); + const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => { + if (sub) { + return sub; + } - // If we don't have a customer record, create one as well as an empty subscription. - if (!subscription?.customerId) { - subscription = await createCustomer({ user }); - } + // If we don't have a customer record, create one as well as an empty subscription. + return createCustomer({ user }); + }); let billingPortalUrl = ''; - if (subscription?.customerId) { + if (subscription.customerId) { billingPortalUrl = await getPortalSession({ customerId: subscription.customerId, returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`, diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx index 88dc5d7a4..bea7f4aee 100644 --- a/apps/web/src/components/(dashboard)/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/layout/header.tsx @@ -1,6 +1,6 @@ 'use client'; -import { HTMLAttributes } from 'react'; +import { HTMLAttributes, useEffect, useState } from 'react'; import Link from 'next/link'; @@ -17,10 +17,23 @@ export type HeaderProps = HTMLAttributes & { }; export const Header = ({ className, user, ...props }: HeaderProps) => { + const [scrollY, setScrollY] = useState(0); + + useEffect(() => { + const onScroll = () => { + setScrollY(window.scrollY); + }; + + window.addEventListener('scroll', onScroll); + + return () => window.removeEventListener('scroll', onScroll); + }, []); + return (
5 && 'border-b-border', className, )} {...props} From 6c12ed4afc34ccb61756a75756c9f3315617626a Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 13:07:55 +1000 Subject: [PATCH 22/32] fix: update migration for timestamp columns --- .../migration.sql | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql index 333386230..371181e80 100644 --- a/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql +++ b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql @@ -1,3 +1,13 @@ -- AlterTable -ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3); +ALTER TABLE "Document" ADD COLUMN "updatedAt" TIMESTAMP(3); + +-- DefaultValues +UPDATE "Document" +SET + "createdAt" = COALESCE("created"::TIMESTAMP, NOW()), + "updatedAt" = COALESCE("created"::TIMESTAMP, NOW()); + +-- AlterColumn +ALTER TABLE "Document" ALTER COLUMN "createdAt" SET NOT NULL DEFAULT NOW(); +ALTER TABLE "Document" ALTER COLUMN "updatedAt" SET NOT NULL DEFAULT NOW(); From 2356f58e7b5c471432eb6e4d221d0f0f0de67b9a Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 13:21:03 +1000 Subject: [PATCH 23/32] fix: implement feedback --- .../app/(dashboard)/documents/[id]/page.tsx | 2 -- .../documents/data-table-action-dropdown.tsx | 28 ++----------------- .../sign/[token]/complete/download-button.tsx | 9 ++++++ packages/lib/universal/upload/get-file.ts | 10 +++++-- packages/lib/universal/upload/put-file.ts | 8 +++++- packages/lib/universal/upload/update-file.ts | 6 +++- .../migration.sql | 10 +++++-- .../migration.sql | 8 ++++++ packages/prisma/schema.prisma | 1 - 9 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 packages/prisma/migrations/20230914031347_remove_redundant_created_column/migration.sql diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index 915547607..3d6dbb954 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -47,8 +47,6 @@ export default async function DocumentPage({ params }: DocumentPageProps) { .then((buffer) => Buffer.from(buffer).toString('base64')) .then((data) => `data:application/pdf;base64,${data}`); - console.log({ documentDataUrl: documentDataUrl.slice(0, 40) }); - const [recipients, fields] = await Promise.all([ await getRecipientsForDocument({ documentId, 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 02e3ad95c..dd028e0b2 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 @@ -14,15 +14,9 @@ import { XCircle, } from 'lucide-react'; import { useSession } from 'next-auth/react'; -import { match } from 'ts-pattern'; -import { - Document, - DocumentDataType, - DocumentStatus, - Recipient, - User, -} from '@documenso/prisma/client'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { trpc } from '@documenso/trpc/client'; import { @@ -75,23 +69,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = return; } - const documentBytes = await match(documentData.type) - .with(DocumentDataType.BYTES, () => - Uint8Array.from(documentData.data, (c) => c.charCodeAt(0)), - ) - .with(DocumentDataType.BYTES_64, () => - Uint8Array.from( - atob(documentData.data) - .split('') - .map((c) => c.charCodeAt(0)), - ), - ) - .with(DocumentDataType.S3_PATH, async () => - fetch(documentData.data) - .then(async (res) => res.arrayBuffer()) - .then((buffer) => new Uint8Array(buffer)), - ) - .exhaustive(); + const documentBytes = await getFile(documentData); const blob = new Blob([documentBytes], { type: 'application/pdf', diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx index 088afad33..49b7a8f15 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/download-button.tsx @@ -7,6 +7,7 @@ import { Download } from 'lucide-react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { DocumentData } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; +import { useToast } from '@documenso/ui/primitives/use-toast'; export type DownloadButtonProps = HTMLAttributes & { disabled?: boolean; @@ -21,6 +22,8 @@ export const DownloadButton = ({ disabled, ...props }: DownloadButtonProps) => { + const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); const onDownloadClick = async () => { @@ -47,6 +50,12 @@ export const DownloadButton = ({ window.URL.revokeObjectURL(link.href); } catch (err) { console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while downloading your document.', + variant: 'destructive', + }); } finally { setIsLoading(false); } diff --git a/packages/lib/universal/upload/get-file.ts b/packages/lib/universal/upload/get-file.ts index 2c71c3774..10e624aaf 100644 --- a/packages/lib/universal/upload/get-file.ts +++ b/packages/lib/universal/upload/get-file.ts @@ -35,9 +35,15 @@ const getFileFromBytes64 = (data: string) => { const getFileFromS3 = async (key: string) => { const { url } = await getPresignGetUrl(key); - const buffer = await fetch(url, { + const response = await fetch(url, { method: 'GET', - }).then(async (res) => res.arrayBuffer()); + }); + + if (!response.ok) { + throw new Error(`Failed to get file "${key}", failed with status code ${response.status}`); + } + + const buffer = await response.arrayBuffer(); const binaryData = new Uint8Array(buffer); diff --git a/packages/lib/universal/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts index ccfa96e7b..56ed8eb07 100644 --- a/packages/lib/universal/upload/put-file.ts +++ b/packages/lib/universal/upload/put-file.ts @@ -38,7 +38,7 @@ const putFileInS3 = async (file: File) => { const body = await file.arrayBuffer(); - await fetch(url, { + const reponse = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', @@ -46,6 +46,12 @@ const putFileInS3 = async (file: File) => { body, }); + if (!reponse.ok) { + throw new Error( + `Failed to upload file "${file.name}", failed with status code ${reponse.status}`, + ); + } + return { type: DocumentDataType.S3_PATH, data: key, diff --git a/packages/lib/universal/upload/update-file.ts b/packages/lib/universal/upload/update-file.ts index a7a227bab..e06a8fa5f 100644 --- a/packages/lib/universal/upload/update-file.ts +++ b/packages/lib/universal/upload/update-file.ts @@ -42,11 +42,15 @@ const updateFileWithBytes64 = (data: string) => { const updateFileWithS3 = async (key: string, data: string) => { const { url } = await getAbsolutePresignPostUrl(key); - await fetch(url, { + const response = await fetch(url, { method: 'PUT', body: data, }); + if (!response.ok) { + throw new Error(`Failed to update file "${key}", failed with status code ${response.status}`); + } + return { type: DocumentDataType.S3_PATH, data: key, diff --git a/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql index 371181e80..3d30cb4e1 100644 --- a/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql +++ b/packages/prisma/migrations/20230907080056_add_created_at_and_updated_at_columns/migration.sql @@ -1,5 +1,7 @@ -- AlterTable ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3); + +-- AlterTable ALTER TABLE "Document" ADD COLUMN "updatedAt" TIMESTAMP(3); -- DefaultValues @@ -9,5 +11,9 @@ SET "updatedAt" = COALESCE("created"::TIMESTAMP, NOW()); -- AlterColumn -ALTER TABLE "Document" ALTER COLUMN "createdAt" SET NOT NULL DEFAULT NOW(); -ALTER TABLE "Document" ALTER COLUMN "updatedAt" SET NOT NULL DEFAULT NOW(); +ALTER TABLE "Document" ALTER COLUMN "createdAt" SET DEFAULT NOW(); +ALTER TABLE "Document" ALTER COLUMN "createdAt" SET NOT NULL; + +-- AlterColumn +ALTER TABLE "Document" ALTER COLUMN "updatedAt" SET DEFAULT NOW(); +ALTER TABLE "Document" ALTER COLUMN "updatedAt" SET NOT NULL; diff --git a/packages/prisma/migrations/20230914031347_remove_redundant_created_column/migration.sql b/packages/prisma/migrations/20230914031347_remove_redundant_created_column/migration.sql new file mode 100644 index 000000000..79902a8e7 --- /dev/null +++ b/packages/prisma/migrations/20230914031347_remove_redundant_created_column/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `created` on the `Document` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "created"; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 6cdf90661..1ff3d7a75 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -92,7 +92,6 @@ enum DocumentStatus { model Document { id Int @id @default(autoincrement()) - created DateTime @default(now()) userId Int User User @relation(fields: [userId], references: [id], onDelete: Cascade) title String From 425db8fc1fa146e87a8ab239234829dcb6817356 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 13:32:16 +1000 Subject: [PATCH 24/32] fix: remove references to created column --- apps/marketing/src/pages/api/stripe/webhook/index.ts | 1 - apps/web/src/pages/api/stripe/webhook/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index b5feeb870..ad9bfe808 100644 --- a/apps/marketing/src/pages/api/stripe/webhook/index.ts +++ b/apps/marketing/src/pages/api/stripe/webhook/index.ts @@ -103,7 +103,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, - created: now, documentDataId, }, include: { diff --git a/apps/web/src/pages/api/stripe/webhook/index.ts b/apps/web/src/pages/api/stripe/webhook/index.ts index fb7877259..9efab2a78 100644 --- a/apps/web/src/pages/api/stripe/webhook/index.ts +++ b/apps/web/src/pages/api/stripe/webhook/index.ts @@ -101,7 +101,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) title: 'Documenso Supporter Pledge.pdf', status: DocumentStatus.COMPLETED, userId: user.id, - created: now, documentDataId, }, include: { From 0d702e918947eb8357b242fb5569f0dea647abc9 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 13:37:38 +1000 Subject: [PATCH 25/32] fix: remove further references to created column --- apps/web/src/app/(dashboard)/documents/page.tsx | 2 +- packages/lib/server-only/document/find-documents.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index d1f558806..32ec4f814 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -39,7 +39,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage userId: user.id, status, orderBy: { - column: 'created', + column: 'createdAt', direction: 'desc', }, page, diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index c9c8eaf6c..aa5410c17 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -32,7 +32,7 @@ export const findDocuments = async ({ }, }); - const orderByColumn = orderBy?.column ?? 'created'; + const orderByColumn = orderBy?.column ?? 'createdAt'; const orderByDirection = orderBy?.direction ?? 'desc'; const termFilters = !term From 8be52e2fa3bddff4d052310c6541ac53a1e32554 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Sep 2023 14:50:17 +1000 Subject: [PATCH 26/32] fix: final reference to created column --- apps/web/src/app/(dashboard)/documents/data-table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index b8c735b59..7d2f18a27 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -53,8 +53,8 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { columns={[ { header: 'Created', - accessorKey: 'created', - cell: ({ row }) => , + accessorKey: 'createdAt', + cell: ({ row }) => , }, { header: 'Title', From 9dcab76cd553f1f26ee8dd8839e4345cb874a336 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan <55143799+dephraiim@users.noreply.github.com> Date: Mon, 18 Sep 2023 01:37:17 +0000 Subject: [PATCH 27/32] feat: use description of each blog post in og image (#380) --- apps/marketing/src/app/(marketing)/blog/[post]/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index 7edf29ec2..757eb8882 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -19,6 +19,7 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { return { title: `Documenso - ${blogPost.title}`, + description: blogPost.description, }; }; From d41ca8e0e6d7519e919e7dd33251fe934ee011a9 Mon Sep 17 00:00:00 2001 From: nsylke Date: Mon, 18 Sep 2023 20:13:46 -0500 Subject: [PATCH 28/32] feat: security headers --- apps/marketing/next.config.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index 97f904cf0..2783e4063 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -18,6 +18,40 @@ const config = { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', }, }, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'x-dns-prefetch-control', + value: 'on', + }, + { + key: 'strict-transport-security', + value: 'max-age=31536000; includeSubDomains; preload', + }, + { + key: 'x-frame-options', + value: 'SAMEORIGIN', + }, + { + key: 'x-content-type-options', + value: 'nosniff', + }, + { + key: 'referrer-policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'permissions-policy', + value: + 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()', + }, + ], + }, + ]; + }, }; module.exports = withContentlayer(config); From 1be0b9e01fc1e0703228bc1431488aa409afedec Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 14 Sep 2023 15:00:14 +1000 Subject: [PATCH 29/32] feat: add vercel build script --- .env.example | 4 +- .eslintignore | 1 + apps/marketing/next.config.js | 1 + apps/marketing/process-env.d.ts | 3 +- .../src/app/(marketing)/claimed/page.tsx | 2 +- apps/marketing/src/app/layout.tsx | 4 +- .../src/pages/api/claim-plan/index.ts | 6 +- apps/web/next.config.js | 1 + apps/web/process-env.d.ts | 3 +- .../app/(dashboard)/settings/billing/page.tsx | 2 +- apps/web/src/app/layout.tsx | 4 +- apps/web/src/helpers/get-feature-flag.ts | 4 +- apps/web/src/pages/api/claim-plan/index.ts | 6 +- assets/example.pdf | Bin 0 -> 39842 bytes package-lock.json | 36 +++--- .../server-only/document/send-document.tsx | 4 +- packages/lib/universal/get-base-url.ts | 4 +- packages/prisma/helper.ts | 52 +++++++++ packages/prisma/index.ts | 10 +- packages/prisma/package.json | 14 ++- packages/prisma/seed-database.ts | 82 ++++++++++++++ packages/tsconfig/process-env.d.ts | 17 ++- scripts/remap-vercel-env.cjs | 45 ++++++++ scripts/vercel.sh | 107 ++++++++++++++++++ turbo.json | 30 +++-- 25 files changed, 391 insertions(+), 51 deletions(-) create mode 100644 assets/example.pdf create mode 100644 packages/prisma/helper.ts create mode 100644 packages/prisma/seed-database.ts create mode 100644 scripts/remap-vercel-env.cjs create mode 100755 scripts/vercel.sh diff --git a/.env.example b/.env.example index 6f32b5a63..fb22bbedf 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,8 @@ NEXT_PRIVATE_GOOGLE_CLIENT_ID="" NEXT_PRIVATE_GOOGLE_CLIENT_SECRET="" # [[APP]] -NEXT_PUBLIC_SITE_URL="http://localhost:3000" -NEXT_PUBLIC_APP_URL="http://localhost:3000" +NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" +NEXT_PUBLIC_MARKETING_URL="http://localhost:3001" # [[DATABASE]] NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" diff --git a/.eslintignore b/.eslintignore index f80dc7f80..b7f7e638f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ # Statically hosted javascript files apps/*/public/*.js apps/*/public/*.cjs +scripts/ diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index 97f904cf0..ce479838f 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const { withContentlayer } = require('next-contentlayer'); +const { remapVercelEnv } = require('../../scripts/remap-vercel-env.cjs'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), diff --git a/apps/marketing/process-env.d.ts b/apps/marketing/process-env.d.ts index ac170a616..3dfdcb30f 100644 --- a/apps/marketing/process-env.d.ts +++ b/apps/marketing/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_DATABASE_URL: string; diff --git a/apps/marketing/src/app/(marketing)/claimed/page.tsx b/apps/marketing/src/app/(marketing)/claimed/page.tsx index f56ae2b26..b1636e2be 100644 --- a/apps/marketing/src/app/(marketing)/claimed/page.tsx +++ b/apps/marketing/src/app/(marketing)/claimed/page.tsx @@ -161,7 +161,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan

diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index ea21ed3c3..46d9a3d32 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -21,12 +21,12 @@ export const metadata = { description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', type: 'website', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], }, twitter: { site: '@documenso', card: 'summary_large_image', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', }, diff --git a/apps/marketing/src/pages/api/claim-plan/index.ts b/apps/marketing/src/pages/api/claim-plan/index.ts index abad354a8..3d2d8679b 100644 --- a/apps/marketing/src/pages/api/claim-plan/index.ts +++ b/apps/marketing/src/pages/api/claim-plan/index.ts @@ -43,7 +43,7 @@ export default async function handler( if (user && user.Subscription.length > 0) { return res.status(200).json({ - redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`, + redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`, }); } @@ -103,8 +103,8 @@ export default async function handler( mode: 'subscription', metadata, allow_promotion_codes: true, - success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent( + success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent( email, )}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`, }); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index be51b51fc..a2657854a 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const { version } = require('./package.json'); +const { remapVercelEnv } = require('../../scripts/remap-vercel-env.cjs'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts index 1cb0018ac..4149423dd 100644 --- a/apps/web/process-env.d.ts +++ b/apps/web/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_DATABASE_URL: string; diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 555c645ce..bd2659e62 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -35,7 +35,7 @@ export default async function BillingSettingsPage() { if (subscription.customerId) { billingPortalUrl = await getPortalSession({ customerId: subscription.customerId, - returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`, + returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, }); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 2ce8744d4..2a1d082f9 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -33,12 +33,12 @@ export const metadata = { description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', type: 'website', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], }, twitter: { site: '@documenso', card: 'summary_large_image', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', }, diff --git a/apps/web/src/helpers/get-feature-flag.ts b/apps/web/src/helpers/get-feature-flag.ts index 3b6c66528..d5cd26c33 100644 --- a/apps/web/src/helpers/get-feature-flag.ts +++ b/apps/web/src/helpers/get-feature-flag.ts @@ -21,7 +21,7 @@ export const getFlag = async ( return LOCAL_FEATURE_FLAGS[flag] ?? true; } - const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/get`); + const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/get`); url.searchParams.set('flag', flag); const response = await fetch(url, { @@ -54,7 +54,7 @@ export const getAllFlags = async ( return LOCAL_FEATURE_FLAGS; } - const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/all`); + const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/all`); return fetch(url, { headers: { diff --git a/apps/web/src/pages/api/claim-plan/index.ts b/apps/web/src/pages/api/claim-plan/index.ts index abad354a8..3d2d8679b 100644 --- a/apps/web/src/pages/api/claim-plan/index.ts +++ b/apps/web/src/pages/api/claim-plan/index.ts @@ -43,7 +43,7 @@ export default async function handler( if (user && user.Subscription.length > 0) { return res.status(200).json({ - redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`, + redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`, }); } @@ -103,8 +103,8 @@ export default async function handler( mode: 'subscription', metadata, allow_promotion_codes: true, - success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent( + success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent( email, )}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`, }); diff --git a/assets/example.pdf b/assets/example.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f908d84e124bc485ffe198f9d5949caa12ed2d17 GIT binary patch literal 39842 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 Za5@759sL7WP9_qM#Gv`*<&6&R{TJLMdWZl3 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 348a046de..f727ea74e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3371,12 +3371,12 @@ } }, "node_modules/@prisma/client": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.0.0.tgz", - "integrity": "sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", + "integrity": "sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + "@prisma/engines-version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" }, "engines": { "node": ">=16.13" @@ -3391,15 +3391,15 @@ } }, "node_modules/@prisma/engines": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.0.0.tgz", - "integrity": "sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", + "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz", - "integrity": "sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==" + "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", + "integrity": "sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==" }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", @@ -14647,12 +14647,12 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.0.0.tgz", - "integrity": "sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz", + "integrity": "sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==", "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.0.0" + "@prisma/engines": "5.3.1" }, "bin": { "prisma": "build/index.js" @@ -18198,8 +18198,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@prisma/client": "5.0.0", - "prisma": "5.0.0" + "@prisma/client": "5.3.1", + "prisma": "5.3.1" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } }, "packages/tailwind-config": { diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 37ecc66b7..83a88f24a 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -48,8 +48,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) return; } - const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; - const signDocumentLink = `${process.env.NEXT_PUBLIC_SITE_URL}/sign/${recipient.token}`; + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`; const template = createElement(DocumentInviteEmailTemplate, { documentName: document.title, diff --git a/packages/lib/universal/get-base-url.ts b/packages/lib/universal/get-base-url.ts index aa8884088..2120c9f54 100644 --- a/packages/lib/universal/get-base-url.ts +++ b/packages/lib/universal/get-base-url.ts @@ -8,8 +8,8 @@ export const getBaseUrl = () => { return `https://${process.env.VERCEL_URL}`; } - if (process.env.NEXT_PUBLIC_SITE_URL) { - return `https://${process.env.NEXT_PUBLIC_SITE_URL}`; + if (process.env.NEXT_PUBLIC_WEBAPP_URL) { + return process.env.NEXT_PUBLIC_WEBAPP_URL; } return `http://localhost:${process.env.PORT ?? 3000}`; diff --git a/packages/prisma/helper.ts b/packages/prisma/helper.ts new file mode 100644 index 000000000..d436771d1 --- /dev/null +++ b/packages/prisma/helper.ts @@ -0,0 +1,52 @@ +/// + +export const getDatabaseUrl = () => { + if (process.env.NEXT_PRIVATE_DATABASE_URL) { + return process.env.NEXT_PRIVATE_DATABASE_URL; + } + + if (process.env.POSTGRES_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL; + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL; + } + + if (process.env.DATABASE_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.DATABASE_URL; + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.DATABASE_URL; + } + + if (process.env.POSTGRES_PRISMA_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_PRISMA_URL; + } + + if (process.env.POSTGRES_URL_NON_POOLING) { + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING; + } + + // We change the protocol from `postgres:` to `https:` so we can construct a easily + // mofifiable URL. + const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL.replace('postgres://', 'https://')); + + // If we're using a connection pool, we need to let Prisma know that + // we're using PgBouncer. + if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) { + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://'); + } + + // Support for neon.tech (Neon Database) + if (url.hostname.endsWith('neon.tech')) { + const [projectId, ...rest] = url.hostname.split('.'); + + if (!projectId.endsWith('-bouncer')) { + url.hostname = `${projectId}-pooler.${rest.join('.')}`; + } + + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://'); + } + + return process.env.NEXT_PRIVATE_DATABASE_URL; +}; diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index 93a334caa..b9e290add 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -1,5 +1,7 @@ import { PrismaClient } from '@prisma/client'; +import { getDatabaseUrl } from './helper'; + declare global { // We need `var` to declare a global variable in TypeScript // eslint-disable-next-line no-var @@ -7,9 +9,13 @@ declare global { } if (!globalThis.prisma) { - globalThis.prisma = new PrismaClient(); + globalThis.prisma = new PrismaClient({ datasourceUrl: getDatabaseUrl() }); } -export const prisma = globalThis.prisma || new PrismaClient(); +export const prisma = + globalThis.prisma || + new PrismaClient({ + datasourceUrl: getDatabaseUrl(), + }); export const getPrismaClient = () => prisma; diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 3ef12787a..958bcde17 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -9,10 +9,18 @@ "format": "prisma format", "prisma:generate": "prisma generate", "prisma:migrate-dev": "prisma migrate dev", - "prisma:migrate-deploy": "prisma migrate deploy" + "prisma:migrate-deploy": "prisma migrate deploy", + "prisma:seed": "prisma db seed" + }, + "prisma": { + "seed": "ts-node --transpileOnly --skipProject ./seed-database.ts" }, "dependencies": { - "@prisma/client": "5.0.0", - "prisma": "5.0.0" + "@prisma/client": "5.3.1", + "prisma": "5.3.1" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } } diff --git a/packages/prisma/seed-database.ts b/packages/prisma/seed-database.ts new file mode 100644 index 000000000..65daa357e --- /dev/null +++ b/packages/prisma/seed-database.ts @@ -0,0 +1,82 @@ +import { DocumentDataType, Role } from '@prisma/client'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { hashSync } from '@documenso/lib/server-only/auth/hash'; + +import { prisma } from './index'; + +const seedDatabase = async () => { + const examplePdf = fs + .readFileSync(path.join(__dirname, '../../assets/example.pdf')) + .toString('base64'); + + const exampleUser = await prisma.user.upsert({ + where: { + email: 'example@documenso.com', + }, + create: { + name: 'Example User', + email: 'example@documenso.com', + password: hashSync('password'), + roles: [Role.USER], + }, + update: {}, + }); + + const adminUser = await prisma.user.upsert({ + where: { + email: 'admin@documenso.com', + }, + create: { + name: 'Admin User', + email: 'admin@documenso.com', + password: hashSync('password'), + roles: [Role.USER, Role.ADMIN], + }, + update: {}, + }); + + const examplePdfData = await prisma.documentData.upsert({ + where: { + id: 'clmn0kv5k0000pe04vcqg5zla', + }, + create: { + id: 'clmn0kv5k0000pe04vcqg5zla', + type: DocumentDataType.BYTES_64, + data: examplePdf, + initialData: examplePdf, + }, + update: {}, + }); + + await prisma.document.upsert({ + where: { + id: 1, + }, + create: { + id: 1, + title: 'Example Document', + documentDataId: examplePdfData.id, + userId: exampleUser.id, + Recipient: { + create: { + name: String(adminUser.name), + email: adminUser.email, + token: Math.random().toString(36).slice(2, 9), + }, + }, + }, + update: {}, + }); +}; + +seedDatabase() + .then(() => { + console.log('Database seeded'); + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index b0852b4f4..d32b9984a 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_GOOGLE_CLIENT_ID?: string; NEXT_PRIVATE_GOOGLE_CLIENT_SECRET?: string; @@ -40,5 +41,19 @@ declare namespace NodeJS { NEXT_PRIVATE_SMTP_FROM_NAME?: string; NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string; + + /** + * Vercel environment variables + */ + VERCEL?: string; + VERCEL_ENV?: 'production' | 'development' | 'preview'; + VERCEL_URL?: string; + + DEPLOYMENT_TARGET?: 'webapp' | 'marketing'; + + POSTGRES_URL?: string; + DATABASE_URL?: string; + POSTGRES_PRISMA_URL?: string; + POSTGRES_URL_NON_POOLING?: string; } } diff --git a/scripts/remap-vercel-env.cjs b/scripts/remap-vercel-env.cjs new file mode 100644 index 000000000..95e60cde8 --- /dev/null +++ b/scripts/remap-vercel-env.cjs @@ -0,0 +1,45 @@ +/** @typedef {import('@documenso/tsconfig/process-env')} */ + +/** + * Remap Vercel environment variables to our defined Next.js environment variables. + * + * @deprecated This is no longer needed because we can't inject runtime environment variables via next.config.js + * + * @returns {void} + */ +const remapVercelEnv = () => { + if (!process.env.VERCEL || !process.env.DEPLOYMENT_TARGET) { + return; + } + + if (process.env.POSTGRES_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL; + } + + if (process.env.POSTGRES_URL_NON_POOLING) { + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING; + } + + // If we're using a connection pool, we need to let Prisma know that + // we're using PgBouncer. + if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) { + const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL); + + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString(); + } + + if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'webapp') { + process.env.NEXTAUTH_URL = `https://${process.env.VERCEL_URL}`; + process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`; + } + + if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'marketing') { + process.env.NEXT_PUBLIC_MARKETING_URL = `https://${process.env.VERCEL_URL}`; + } +}; + +module.exports = { + remapVercelEnv, +}; diff --git a/scripts/vercel.sh b/scripts/vercel.sh new file mode 100755 index 000000000..e4ab23622 --- /dev/null +++ b/scripts/vercel.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# Exit on error. +set -eo pipefail + +# Get the directory of this script, regardless of where it is called from. +SCRIPT_DIR="$(readlink -f "$(dirname "$0")")" + + +function log() { + echo "[VercelBuild]: $1" +} + +function build_webapp() { + log "Building webapp for $VERCEL_ENV" + + remap_database_integration + + npm run prisma:generate --workspace=@documenso/prisma + npm run prisma:migrate-deploy --workspace=@documenso/prisma + + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Seeding database for $VERCEL_ENV" + + npm run prisma:seed --workspace=@documenso/prisma + fi + + npm run build -- --filter @documenso/web +} + +function remap_webapp_env() { + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Remapping webapp environment variables for $VERCEL_ENV" + + export NEXTAUTH_URL="https://$VERCEL_URL" + export NEXT_PUBLIC_WEBAPP_URL="https://$VERCEL_URL" + fi +} + +function build_marketing() { + log "Building marketing for $VERCEL_ENV" + + remap_database_integration + + npm run prisma:generate --workspace=@documenso/prisma + npm run build -- --filter @documenso/marketing +} + +function remap_marketing_env() { + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Remapping marketing environment variables for $VERCEL_ENV" + + export NEXT_PUBLIC_MARKETING_URL="https://$VERCEL_URL" + fi +} + +function remap_database_integration() { + log "Remapping Supabase integration for $VERCEL_ENV" + + if [[ ! -z "$POSTGRES_URL" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL" + fi + + if [[ ! -z "$DATABASE_URL" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$DATABASE_URL" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$DATABASE_URL" + fi + + if [[ ! -z "$POSTGRES_URL_NON_POOLING" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL?pgbouncer=true" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL_NON_POOLING" + fi + + + if [[ "$NEXT_PRIVATE_DATABASE_URL" == *"neon.tech"* ]]; then + log "Remapping for Neon integration" + + PROJECT_ID="$(echo "$PGHOST" | cut -d'.' -f1)" + PGBOUNCER_HOST="$(echo "$PGHOST" | sed "s/${PROJECT_ID}/${PROJECT_ID}-pooler/")" + + export NEXT_PRIVATE_DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@${PGBOUNCER_HOST}/${PGDATABASE}?pgbouncer=true" + fi +} + +# Navigate to the root of the project. +cd "$SCRIPT_DIR/.." + +# Check if the script is running on Vercel. +if [[ -z "$VERCEL" ]]; then + log "ERROR - This script must be run as part of the Vercel build process." + exit 1 +fi + +case "$DEPLOYMENT_TARGET" in + "webapp") + build_webapp + ;; + "marketing") + build_marketing + ;; + *) + log "ERROR - Missing or invalid DEPLOYMENT_TARGET environment variable." + log "ERROR - DEPLOYMENT_TARGET must be either 'webapp' or 'marketing'." + exit 1 + ;; +esac diff --git a/turbo.json b/turbo.json index a5b333c66..01b8bd487 100644 --- a/turbo.json +++ b/turbo.json @@ -2,8 +2,13 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**"] + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] }, "lint": {}, "dev": { @@ -11,20 +16,22 @@ "persistent": true } }, - "globalDependencies": ["**/.env.*local"], + "globalDependencies": [ + "**/.env.*local" + ], "globalEnv": [ "APP_VERSION", "NEXTAUTH_URL", "NEXTAUTH_SECRET", - "NEXT_PUBLIC_APP_URL", - "NEXT_PUBLIC_SITE_URL", + "NEXT_PUBLIC_WEBAPP_URL", + "NEXT_PUBLIC_MARKETING_URL", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_FEATURE_BILLING_ENABLED", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", "NEXT_PRIVATE_DATABASE_URL", - "NEXT_PRIVATE_NEXT_AUTH_SECRET", + "NEXT_PRIVATE_DIRECT_DATABASE_URL", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", "NEXT_PRIVATE_GOOGLE_CLIENT_SECRET", "NEXT_PUBLIC_UPLOAD_TRANSPORT", @@ -48,6 +55,15 @@ "NEXT_PRIVATE_SMTP_SECURE", "NEXT_PRIVATE_SMTP_FROM_NAME", "NEXT_PRIVATE_SMTP_FROM_ADDRESS", - "NEXT_PRIVATE_STRIPE_API_KEY" + "NEXT_PRIVATE_STRIPE_API_KEY", + + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "DEPLOYMENT_TARGET", + "POSTGRES_URL", + "DATABASE_URL", + "POSTGRES_PRISMA_URL", + "POSTGRES_URL_NON_POOLING" ] } From b411db40dac3311958f741f5cbd0a2c2b435c468 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 19 Sep 2023 02:40:58 +0000 Subject: [PATCH 30/32] chore: tidy unused code --- apps/marketing/next.config.js | 1 - apps/web/next.config.js | 1 - packages/prisma/helper.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index ce479838f..97f904cf0 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const { withContentlayer } = require('next-contentlayer'); -const { remapVercelEnv } = require('../../scripts/remap-vercel-env.cjs'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), diff --git a/apps/web/next.config.js b/apps/web/next.config.js index a2657854a..be51b51fc 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const { version } = require('./package.json'); -const { remapVercelEnv } = require('../../scripts/remap-vercel-env.cjs'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), diff --git a/packages/prisma/helper.ts b/packages/prisma/helper.ts index d436771d1..865e16239 100644 --- a/packages/prisma/helper.ts +++ b/packages/prisma/helper.ts @@ -39,7 +39,7 @@ export const getDatabaseUrl = () => { if (url.hostname.endsWith('neon.tech')) { const [projectId, ...rest] = url.hostname.split('.'); - if (!projectId.endsWith('-bouncer')) { + if (!projectId.endsWith('-pooler')) { url.hostname = `${projectId}-pooler.${rest.join('.')}`; } From 60ef9df721cd5e5ce63bff43a38fed9f5d2d97a8 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 19 Sep 2023 13:34:38 +1000 Subject: [PATCH 31/32] chore: update ci --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/codeql-analysis.yml | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c15689123..1a5d4bbcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,12 +22,18 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 2 + - name: Install Node.js uses: actions/setup-node@v3 with: node-version: 18 cache: npm + - name: Install dependencies run: npm ci + + - name: Copy env + run: cp .env.example .env + - name: Build run: npm run build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 92a0936a5..c934272a4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,10 @@ jobs: - name: Install Dependencies run: npm ci - + + - name: Copy env + run: cp .env.example .env + - name: Build Documenso run: npm run build @@ -42,4 +45,4 @@ jobs: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 \ No newline at end of file + uses: github/codeql-action/analyze@v2 From 09c7f9dde83730600fb2ad049522450ad6999d1b Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 19 Sep 2023 15:07:43 +1000 Subject: [PATCH 32/32] chore: update devcontainer --- .devcontainer/devcontainer.json | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 59a318b7f..60b385403 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,32 @@ { - "name": "Documenso", - "image": "mcr.microsoft.com/devcontainers/base:bullseye", - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "version": "latest", - "enableNonRootDocker": "true", - "moby": "true" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "Documenso", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "enableNonRootDocker": "true", + "moby": "true" + }, + "ghcr.io/devcontainers/features/node:1": {} + }, "onCreateCommand": "./.devcontainer/on-create.sh", - "forwardPorts": [ - 3000, - 54320, - 9000, - 2500, - 1100 - ] + "forwardPorts": [3000, 54320, 9000, 2500, 1100], + "customizations": { + "vscode": { + "extensions": [ + "aaron-bond.better-comments", + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mikestead.dotenv", + "unifiedjs.vscode-mdx", + "GitHub.copilot-chat", + "GitHub.copilot-labs", + "GitHub.copilot", + "GitHub.vscode-pull-request-github", + "Prisma.prisma", + "VisualStudioExptTeam.vscodeintellicode", + ] + } + } }