diff --git a/.env.example b/.env.example index c482c128e..20e1ae2ae 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,16 @@ E2E_TEST_AUTHENTICATE_USERNAME="Test User" E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com" E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123" +# [[SIGNING]] +# OPTIONAL: Defines the signing transport to use. Available options: local (default) +NEXT_PRIVATE_SIGNING_TRANSPORT="local" +# OPTIONAL: Defines the passphrase for the signing certificate. +NEXT_PRIVATE_SIGNING_PASSPHRASE= +# OPTIONAL: Defines the file contents for the signing certificate as a base64 encoded string. +NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS= +# OPTIONAL: Defines the file path for the signing certificate. defaults to ./example/cert.p12 +NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH= + # [[STORAGE]] # OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3 NEXT_PUBLIC_UPLOAD_TRANSPORT="database" diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 1cfb7337f..f6af3a9ff 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -36,7 +36,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", - "sharp": "0.33.1", + "sharp": "^0.33.1", "typescript": "5.2.2", "zod": "^3.22.4" }, diff --git a/apps/web/package.json b/apps/web/package.json index 41caec804..e72f4898a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -43,7 +43,7 @@ "react-icons": "^4.11.0", "react-rnd": "^10.4.1", "remeda": "^1.27.1", - "sharp": "0.33.1", + "sharp": "^0.33.1", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 1adaace7b..ec595641e 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -76,14 +76,13 @@ export const DocumentsDataTable = ({ { header: 'Recipient', accessorKey: 'recipient', - cell: ({ row }) => { - return ; - }, + cell: ({ row }) => , }, { header: 'Status', accessorKey: 'status', cell: ({ row }) => , + size: 140, }, { header: 'Actions', diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx index e878d8df2..cb00ef6f6 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -25,7 +25,11 @@ type TemplateWithRecipient = Template & { }; type TemplatesDataTableProps = { - templates: TemplateWithRecipient[]; + templates: Array< + TemplateWithRecipient & { + team: { id: number; url: string } | null; + } + >; perPage: number; page: number; totalPages: number; diff --git a/apps/web/src/app/(dashboard)/templates/data-table-title.tsx b/apps/web/src/app/(dashboard)/templates/data-table-title.tsx index 31e1011be..69855ca1e 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-title.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-title.tsx @@ -1,23 +1,35 @@ +'use client'; + import Link from 'next/link'; import { useSession } from 'next-auth/react'; -import { Template } from '@documenso/prisma/client'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import type { Template } from '@documenso/prisma/client'; + +import { useOptionalCurrentTeam } from '~/providers/team'; export type DataTableTitleProps = { - row: Template; + row: Template & { + team: { id: number; url: string } | null; + }; }; export const DataTableTitle = ({ row }: DataTableTitleProps) => { const { data: session } = useSession(); + const team = useOptionalCurrentTeam(); if (!session) { return null; } + const isCurrentTeamTemplate = team?.url && row.team?.url === team?.url; + + const templatesPath = formatTemplatesPath(isCurrentTeamTemplate ? team?.url : undefined); + return ( {row.title} 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 bd7bea2b0..10f7d1e6a 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,13 +1,12 @@ +'use client'; + +import { useRef, useState } from 'react'; + import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import type { Recipient } from '@documenso/prisma/client'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@documenso/ui/primitives/tooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { AvatarWithRecipient } from './avatar-with-recipient'; import { StackAvatar } from './stack-avatar'; @@ -24,6 +23,11 @@ export const StackAvatarsWithTooltip = ({ position, children, }: StackAvatarsWithTooltipProps) => { + const [open, setOpen] = useState(false); + + const isControlled = useRef(false); + const isMouseOverTimeout = useRef(null); + const waitingRecipients = recipients.filter( (recipient) => getRecipientType(recipient) === 'waiting', ); @@ -40,66 +44,105 @@ export const StackAvatarsWithTooltip = ({ (recipient) => getRecipientType(recipient) === 'unsigned', ); + const onMouseEnter = () => { + if (isMouseOverTimeout.current) { + clearTimeout(isMouseOverTimeout.current); + } + + if (isControlled.current) { + return; + } + + isMouseOverTimeout.current = setTimeout(() => { + setOpen((o) => (!o ? true : o)); + }, 200); + }; + + const onMouseLeave = () => { + if (isMouseOverTimeout.current) { + clearTimeout(isMouseOverTimeout.current); + } + + if (isControlled.current) { + return; + } + + setTimeout(() => { + setOpen((o) => (o ? false : o)); + }, 200); + }; + + const onOpenChange = (newOpen: boolean) => { + isControlled.current = newOpen; + + setOpen(newOpen); + }; + return ( - - - - {children || } - + + + {children || } + - -
- {completedRecipients.length > 0 && ( -
-

Completed

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

{recipient.email}

-

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

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

Completed

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

{recipient.email}

+

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

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

Waiting

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

Opened

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

Uncompleted

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

Waiting

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

Opened

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

Uncompleted

+ {uncompletedRecipients.map((recipient: Recipient) => ( + + ))} +
+ )} +
+ ); }; diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx index 27560c073..d7a8f6553 100644 --- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; @@ -149,6 +149,13 @@ export const EnableAuthenticatorAppDialog = ({ } }; + useEffect(() => { + // Reset the form when the Dialog closes + if (!open) { + setupTwoFactorAuthenticationForm.reset(); + } + }, [open, setupTwoFactorAuthenticationForm]); + return ( diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 376a8939c..48e343e8d 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; @@ -92,6 +92,13 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode } }; + useEffect(() => { + // Reset the form when the Dialog closes + if (!open) { + viewRecoveryCodesForm.reset(); + } + }, [open, viewRecoveryCodesForm]); + return ( diff --git a/docker/README.md b/docker/README.md index ba942ac1c..bda1638a2 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,16 @@ NEXT_PRIVATE_SMTP_USERNAME="" NEXT_PRIVATE_SMTP_PASSWORD="" ``` -4. Run the following command to start the containers: +4. Update the volume binding for the cert file in the `compose.yml` file to point to your own key file: + +Since the `cert.p12` file is required for signing and encrypting documents, you will need to provide your own key file. Update the volume binding in the `compose.yml` file to point to your key file: + +```yaml +volumes: + - /path/to/your/keyfile.p12:/opt/documenso/cert.p12 +``` + +1. Run the following command to start the containers: ``` docker-compose --env-file ./.env -d up @@ -70,6 +79,7 @@ docker run -d \ -e NEXT_PRIVATE_SMTP_TRANSPORT="" -e NEXT_PRIVATE_SMTP_FROM_NAME="" -e NEXT_PRIVATE_SMTP_FROM_ADDRESS="" + -v /path/to/your/keyfile.p12:/opt/documenso/cert.p12 documenso/documenso ``` @@ -99,6 +109,10 @@ Here's a markdown table documenting all the provided environment variables: | `NEXT_PUBLIC_WEBAPP_URL` | The URL for the web application. | | `NEXT_PRIVATE_DATABASE_URL` | The URL for the primary database connection (with connection pooling). | | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | The URL for the direct database connection (without connection pooling). | +| `NEXT_PRIVATE_SIGNING_TRANSPORT` | The signing transport to use. Available options: local (default) | +| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | The passphrase for the key file. | +| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | The base64-encoded contents of the key file, will be used instead of file path. | +| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | The path to the key file, default `/opt/documenso/cert.p12`. | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | The transport to use for file uploads (database or s3). | | `NEXT_PRIVATE_UPLOAD_ENDPOINT` | The endpoint for the S3 storage transport (for third-party S3-compatible providers). | | `NEXT_PRIVATE_UPLOAD_REGION` | The region for the S3 storage transport (defaults to us-east-1). | diff --git a/docker/production/compose.yml b/docker/production/compose.yml index 08abcf050..02acc655d 100644 --- a/docker/production/compose.yml +++ b/docker/production/compose.yml @@ -57,8 +57,11 @@ services: - NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT} - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} - NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP} + - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?:-/opt/documenso/cert.p12} ports: - ${PORT:-3000}:${PORT:-3000} + volumes: + - /opt/documenso/cert.p12:/opt/documenso/cert.p12 volumes: database: diff --git a/docker/testing/compose.yml b/docker/testing/compose.yml index cecb5bf14..28ec055c1 100644 --- a/docker/testing/compose.yml +++ b/docker/testing/compose.yml @@ -47,5 +47,8 @@ services: - NEXT_PRIVATE_SMTP_PASSWORD=password - NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso" - NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@documenso.com + - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 ports: - 3000:3000 + volumes: + - ../../apps/web/example/cert.p12:/opt/documenso/cert.p12 diff --git a/package-lock.json b/package-lock.json index 72d06b98a..d7615e179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", - "sharp": "0.33.1", + "sharp": "^0.33.1", "typescript": "5.2.2", "zod": "^3.22.4" }, @@ -74,45 +74,6 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, - "apps/marketing/node_modules/sharp": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", - "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" - }, - "engines": { - "libvips": ">=8.15.0", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.1", - "@img/sharp-darwin-x64": "0.33.1", - "@img/sharp-libvips-darwin-arm64": "1.0.0", - "@img/sharp-libvips-darwin-x64": "1.0.0", - "@img/sharp-libvips-linux-arm": "1.0.0", - "@img/sharp-libvips-linux-arm64": "1.0.0", - "@img/sharp-libvips-linux-s390x": "1.0.0", - "@img/sharp-libvips-linux-x64": "1.0.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", - "@img/sharp-libvips-linuxmusl-x64": "1.0.0", - "@img/sharp-linux-arm": "0.33.1", - "@img/sharp-linux-arm64": "0.33.1", - "@img/sharp-linux-s390x": "0.33.1", - "@img/sharp-linux-x64": "0.33.1", - "@img/sharp-linuxmusl-arm64": "0.33.1", - "@img/sharp-linuxmusl-x64": "0.33.1", - "@img/sharp-wasm32": "0.33.1", - "@img/sharp-win32-ia32": "0.33.1", - "@img/sharp-win32-x64": "0.33.1" - } - }, "apps/marketing/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -159,7 +120,7 @@ "react-icons": "^4.11.0", "react-rnd": "^10.4.1", "remeda": "^1.27.1", - "sharp": "0.33.1", + "sharp": "^0.33.1", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", @@ -182,45 +143,6 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, - "apps/web/node_modules/sharp": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", - "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" - }, - "engines": { - "libvips": ">=8.15.0", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.1", - "@img/sharp-darwin-x64": "0.33.1", - "@img/sharp-libvips-darwin-arm64": "1.0.0", - "@img/sharp-libvips-darwin-x64": "1.0.0", - "@img/sharp-libvips-linux-arm": "1.0.0", - "@img/sharp-libvips-linux-arm64": "1.0.0", - "@img/sharp-libvips-linux-s390x": "1.0.0", - "@img/sharp-libvips-linux-x64": "1.0.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", - "@img/sharp-libvips-linuxmusl-x64": "1.0.0", - "@img/sharp-linux-arm": "0.33.1", - "@img/sharp-linux-arm64": "0.33.1", - "@img/sharp-linux-s390x": "0.33.1", - "@img/sharp-linux-x64": "0.33.1", - "@img/sharp-linuxmusl-arm64": "0.33.1", - "@img/sharp-linuxmusl-x64": "0.33.1", - "@img/sharp-wasm32": "0.33.1", - "@img/sharp-win32-ia32": "0.33.1", - "@img/sharp-win32-x64": "0.33.1" - } - }, "apps/web/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -18560,6 +18482,45 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", + "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.0", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.1", + "@img/sharp-darwin-x64": "0.33.1", + "@img/sharp-libvips-darwin-arm64": "1.0.0", + "@img/sharp-libvips-darwin-x64": "1.0.0", + "@img/sharp-libvips-linux-arm": "1.0.0", + "@img/sharp-libvips-linux-arm64": "1.0.0", + "@img/sharp-libvips-linux-s390x": "1.0.0", + "@img/sharp-libvips-linux-x64": "1.0.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", + "@img/sharp-libvips-linuxmusl-x64": "1.0.0", + "@img/sharp-linux-arm": "0.33.1", + "@img/sharp-linux-arm64": "0.33.1", + "@img/sharp-linux-s390x": "0.33.1", + "@img/sharp-linux-x64": "0.33.1", + "@img/sharp-linuxmusl-arm64": "0.33.1", + "@img/sharp-linuxmusl-x64": "0.33.1", + "@img/sharp-wasm32": "0.33.1", + "@img/sharp-win32-ia32": "0.33.1", + "@img/sharp-win32-x64": "0.33.1" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/packages/eslint-config/index.cjs b/packages/eslint-config/index.cjs index 57cecf40d..aef6a944e 100644 --- a/packages/eslint-config/index.cjs +++ b/packages/eslint-config/index.cjs @@ -20,7 +20,7 @@ module.exports = { parserOptions: { tsconfigRootDir: __dirname, - project: ['../../apps/*/tsconfig.json', '../../packages/*/tsconfig.json'], + project: ['../../tsconfig.eslint.json'], ecmaVersion: 2022, ecmaFeatures: { jsx: true, diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts index 69b43f9b9..9252d32ea 100644 --- a/packages/lib/server-only/template/find-templates.ts +++ b/packages/lib/server-only/template/find-templates.ts @@ -37,6 +37,12 @@ export const findTemplates = async ({ where: whereFilter, include: { templateDocumentData: true, + team: { + select: { + id: true, + url: true, + }, + }, Field: true, Recipient: true, }, diff --git a/packages/ui/primitives/data-table.tsx b/packages/ui/primitives/data-table.tsx index 9cc14a684..0d8556d89 100644 --- a/packages/ui/primitives/data-table.tsx +++ b/packages/ui/primitives/data-table.tsx @@ -115,7 +115,12 @@ export function DataTable({ table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} diff --git a/packages/ui/primitives/toast.tsx b/packages/ui/primitives/toast.tsx index b42cadc27..bddf33165 100644 --- a/packages/ui/primitives/toast.tsx +++ b/packages/ui/primitives/toast.tsx @@ -79,7 +79,7 @@ const ToastClose = React.forwardRef<