Compare commits

..

2 Commits

Author SHA1 Message Date
76e4807c67 v1.9.0-rc.2 2024-12-12 14:24:40 +11:00
efd6531d3e feat: add controls for sending completion emails to document owners
Adds a new `ownerDocumentCompleted` to the email settings that
controls whether a document will be sent to the owner upon completion.

This was previously the only email you couldn't disable and didn't
account for users integrating with just the API and Webhooks.

Also adds a flag to the public `sendDocument` endpoint which will
adjust this setting while sendint the document for users who aren't
using `emailSettings` on the `createDocument` endpoint.
2024-12-12 13:55:46 +11:00
94 changed files with 1365 additions and 2656 deletions

View File

@ -17,25 +17,23 @@ The default document visibility option allows you to control who can view and ac
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [team's general preferences page](/users/teams/preferences) and selecting a different visibility option.
<Callout type="warning">
If the team member uploading the document has a role lower than the default document visibility,
the document visibility will be set to a lower visibility level matching the team member's role.
</Callout>
Here's how it works:
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Everyone_", the document's visibility is set to "_EVERYONE_".
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Admin_" or "_Managers and above_" in this case).
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Everyone_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Everyone_" or "_Managers and above_" in this case).
- The user can change the visibility of the document to any of these options, except "_Admin_", in the document editor.
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Admin_".
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Admin_" role creates a document, and the default document visibility is set to "_Everyone_", "_Managers and above_", or "_Admin_", the document's visibility is set to the default document visibility.
- The user can change the visibility of the document to any of these options in the document editor.
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to "_Everyone_".
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Managers and above_".
- Otherwise, the document's visibility is set to the default document visibility.
You can change the visibility of a document at any time by editing the document and selecting a different visibility option.
![A screenshot of the Documenso's document editor page where you can update the document visibility](/teams/document-visibility-settings.webp)
<Callout type="warning">
Updating the default document visibility in the team's general preferences will not affect the
Updating the default document visibility in the team's general settings will not affect the
visibility of existing documents. You will need to update the visibility of each document
individually.
</Callout>

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -47,7 +47,7 @@
"recharts": "^2.7.2",
"sharp": "0.32.6",
"typescript": "5.2.2",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@lingui/loader": "^4.11.3",

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -56,11 +56,10 @@
"recharts": "^2.7.2",
"remeda": "^2.17.3",
"sharp": "0.32.6",
"trpc-openapi": "^1.2.0",
"ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

@ -134,7 +134,7 @@ export const LeaderboardTable = ({
startTransition(() => {
updateSearchParams({
sortBy: column,
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
sortOrder: sortOrder === 'asc' ? 'desc' : 'asc',
});
});
};

View File

@ -37,8 +37,10 @@ export const DocumentPageViewRecentActivity = ({
{
documentId,
filterForRecentActivity: true,
orderByColumn: 'createdAt',
orderByDirection: 'asc',
orderBy: {
column: 'createdAt',
direction: 'asc',
},
perPage: 10,
},
{

View File

@ -12,8 +12,8 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
className?: string;
initialDocument: TGetDocumentWithDetailsByIdResponse;
initialDocument: DocumentWithDetails;
documentRootPath: string;
isDocumentEnterprise: boolean;
};
@ -103,7 +103,7 @@ export const EditDocumentForm = ({
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: ({ fields: newFields }) => {
onSuccess: (newFields) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
@ -134,7 +134,7 @@ export const EditDocumentForm = ({
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: ({ recipients: newRecipients }) => {
onSuccess: (newRecipients) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,

View File

@ -9,8 +9,8 @@ import { DateTime } from 'luxon';
import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents';
import type { Team } from '@documenso/prisma/client';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@ -24,7 +24,13 @@ import { DataTableActionDropdown } from './data-table-action-dropdown';
import { DataTableTitle } from './data-table-title';
export type DocumentsDataTableProps = {
results: TFindDocumentsResponse;
results: FindResultResponse<
Document & {
Recipient: Recipient[];
User: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'id' | 'url'> | null;
}
>;
showSenderColumn?: boolean;
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
};

View File

@ -26,8 +26,10 @@ export const TemplatePageViewRecentActivity = ({
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
templateId,
teamId,
orderByColumn: 'createdAt',
orderByDirection: 'asc',
orderBy: {
column: 'createdAt',
direction: 'asc',
},
perPage: 5,
});

View File

@ -4,10 +4,8 @@ import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -53,20 +51,10 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
});
onOpenChange(false);
},
onError: (err) => {
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(
AppErrorCode.NOT_FOUND,
() => msg`Template not found or already associated with a team.`,
)
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not a member of this team.`)
.otherwise(() => msg`An error occurred while moving the template.`);
onError: (error) => {
toast({
title: _(msg`Error`),
description: _(errorMessage),
description: error.message || _(msg`An error occurred while moving the template.`),
variant: 'destructive',
duration: 7500,
});

View File

@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
@ -55,7 +54,6 @@ export const CheckboxField = ({
...item,
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
}));
const [checkedValues, setCheckedValues] = useState(
values
?.map((item) =>
@ -99,7 +97,7 @@ export const CheckboxField = ({
const payload: TSignFieldWithTokenMutationSchema = {
token: recipient.token,
fieldId: field.id,
value: toCheckboxValue(checkedValues),
value: checkedValues.join(','),
isBase64: true,
authOptions,
};
@ -193,7 +191,7 @@ export const CheckboxField = ({
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: toCheckboxValue(checkedValues),
value: updatedValues.join(','),
isBase64: true,
});
}
@ -230,11 +228,6 @@ export const CheckboxField = ({
}
}, [checkedValues, isLengthConditionMet, field.inserted]);
const parsedCheckedValues = useMemo(
() => fromCheckboxValue(field.customText),
[field.customText],
);
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
{isLoading && (
@ -284,7 +277,9 @@ export const CheckboxField = ({
className="h-3 w-3"
checkClassName="text-white"
id={`checkbox-${index}`}
checked={parsedCheckedValues.includes(itemValue)}
checked={field.customText
.split(',')
.some((customValue) => customValue === itemValue)}
disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>

View File

@ -53,7 +53,6 @@ const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
[ErrorCode.UNVERIFIED_EMAIL]:
'This account has not been verified. Please verify your account before signing in.',
[ErrorCode.ACCOUNT_DISABLED]: 'This account has been disabled. Please contact support.',
};
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;

View File

@ -1,47 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { createOpenApiNextHandler } from 'trpc-openapi';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildLogger } from '@documenso/lib/utils/logger';
import type { TRPCError } from '@documenso/trpc/server';
import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router';
const logger = buildLogger();
export default createOpenApiNextHandler<typeof appRouter>({
router: appRouter,
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
createTrpcContext({ req, res }),
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
// Always log the error for now.
console.error(error.message);
const appError = AppError.parseError(error.cause || error);
const isAppError = error.cause instanceof AppError;
// Only log AppErrors that are explicitly set to 500 or the error code
// is in the errorCodesToAlertOn list.
const isLoggableAppError =
isAppError && (appError.statusCode === 500 || errorCodesToAlertOn.includes(appError.code));
// Only log TRPC errors that are in the `errorCodesToAlertOn` list and is
// not an AppError.
const isLoggableTrpcError = !isAppError && errorCodesToAlertOn.includes(error.code);
if (isLoggableAppError || isLoggableTrpcError) {
logger.error(error, {
method: path,
context: {
source: '/v2/api',
appError: AppError.toJSON(appError),
},
});
}
},
responseMeta: () => {},
});
const errorCodesToAlertOn = [AppErrorCode.UNKNOWN_ERROR, 'INTERNAL_SERVER_ERROR'];

View File

@ -1,9 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { openApiDocument } from '@documenso/trpc/server/open-api';
const handler = (_req: NextApiRequest, res: NextApiResponse) => {
res.status(200).send(openApiDocument);
};
export default handler;

View File

@ -45,7 +45,6 @@ export default trpcNext.createNextApiHandler({
logger.error(error, {
method: path,
context: {
source: 'trpc',
appError: AppError.toJSON(appError),
},
});

388
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"workspaces": [
"apps/*",
"packages/*"
@ -17,10 +17,8 @@
"@lingui/core": "^4.11.3",
"inngest-cli": "^0.29.1",
"luxon": "^3.5.0",
"mupdf": "^1.0.0",
"next-runtime-env": "^3.2.0",
"react": "^18",
"zod": "3.24.1"
"react": "^18"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
@ -81,7 +79,7 @@
},
"apps/marketing": {
"name": "@documenso/marketing",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/assets": "*",
@ -117,7 +115,7 @@
"recharts": "^2.7.2",
"sharp": "0.32.6",
"typescript": "5.2.2",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@lingui/loader": "^4.11.3",
@ -494,7 +492,7 @@
},
"apps/web": {
"name": "@documenso/web",
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",
@ -538,11 +536,10 @@
"recharts": "^2.7.2",
"remeda": "^2.17.3",
"sharp": "0.32.6",
"trpc-openapi": "^1.2.0",
"ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@documenso/tailwind-config": "*",
@ -3497,11 +3494,6 @@
"node": ">=6"
}
},
"node_modules/@hapi/bourne": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
"integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w=="
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -10725,6 +10717,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@trigger.dev/cli/node_modules/zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@trigger.dev/core": {
"version": "2.3.18",
"resolved": "https://registry.npmjs.org/@trigger.dev/core/-/core-2.3.18.tgz",
@ -10746,6 +10747,14 @@
"node": ">=18.0.0"
}
},
"node_modules/@trigger.dev/core/node_modules/zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@trigger.dev/nextjs": {
"version": "2.3.18",
"resolved": "https://registry.npmjs.org/@trigger.dev/nextjs/-/nextjs-2.3.18.tgz",
@ -10809,6 +10818,14 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/@trigger.dev/sdk/node_modules/zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@trigger.dev/yalt": {
"version": "2.3.18",
"resolved": "https://registry.npmjs.org/@trigger.dev/yalt/-/yalt-2.3.18.tgz",
@ -10825,6 +10842,15 @@
"node": ">=18.0.0"
}
},
"node_modules/@trigger.dev/yalt/node_modules/zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
@ -11981,18 +12007,6 @@
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@ -14297,21 +14311,6 @@
}
}
},
"node_modules/co-body": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz",
"integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==",
"dependencies": {
"@hapi/bourne": "^3.0.0",
"inflation": "^2.0.0",
"qs": "^6.5.2",
"raw-body": "^2.3.3",
"type-is": "^1.6.16"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/code-block-writer": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
@ -14613,14 +14612,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/consola": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz",
"integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -14636,17 +14627,6 @@
"simple-wcswidth": "^1.0.1"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
@ -14732,9 +14712,9 @@
}
},
"node_modules/cookie-es": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.0.0.tgz",
"integrity": "sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ=="
},
"node_modules/copy-anything": {
"version": "3.0.5",
@ -14900,14 +14880,6 @@
"node": ">= 8"
}
},
"node_modules/crossws": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz",
"integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==",
"dependencies": {
"uncrypto": "^0.1.3"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
@ -15615,11 +15587,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
@ -15726,11 +15693,6 @@
"node": ">=6"
}
},
"node_modules/destr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
"integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ=="
},
"node_modules/detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@ -18161,14 +18123,6 @@
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
@ -18839,23 +18793,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/h3": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz",
"integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==",
"dependencies": {
"cookie-es": "^1.2.2",
"crossws": ">=0.2.0 <0.4.0",
"defu": "^6.1.4",
"destr": "^2.0.3",
"iron-webcrypto": "^1.2.1",
"ohash": "^1.1.4",
"radix3": "^1.1.2",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3",
"unenv": "^1.10.0"
}
},
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@ -19907,14 +19844,6 @@
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true
},
"node_modules/inflation": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz",
"integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/inflection": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-2.0.1.tgz",
@ -20071,6 +20000,14 @@
"node": ">=6"
}
},
"node_modules/inngest/node_modules/zod": {
"version": "3.22.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz",
"integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/input-otp": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz",
@ -20282,14 +20219,6 @@
"node": ">= 10"
}
},
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
"funding": {
"url": "https://github.com/sponsors/brc-dd"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@ -22106,7 +22035,8 @@
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -22933,14 +22863,6 @@
"esbuild": "0.*"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/memfs": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
@ -22995,14 +22917,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-refs": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz",
@ -23076,14 +22990,6 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micro": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
@ -23827,17 +23733,6 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -24244,12 +24139,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mupdf": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mupdf/-/mupdf-1.0.0.tgz",
"integrity": "sha512-AWT27abYSX5gQmWs7+jDEtmGJpWyZrqdxROpYf5BDAJBA+iYqlNztk2EMlKvuRLBzajj00kf4PtFiergDSKDTg==",
"license": "AGPL-3.0-or-later"
},
"node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
@ -24307,6 +24196,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@ -24775,11 +24665,6 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
"integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ=="
},
"node_modules/node-gyp": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz",
@ -25108,38 +24993,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/node-mocks-http": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.16.2.tgz",
"integrity": "sha512-2Sh6YItRp1oqewZNlck3LaFp5vbyW2u51HX2p1VLxQ9U/bG90XV8JY9O7Nk+HDd6OOn/oV3nA5Tx5k4Rki0qlg==",
"dependencies": {
"accepts": "^1.3.7",
"content-disposition": "^0.5.3",
"depd": "^1.1.0",
"fresh": "^0.5.2",
"merge-descriptors": "^1.0.1",
"methods": "^1.1.2",
"mime": "^1.3.4",
"parseurl": "^1.3.3",
"range-parser": "^1.2.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@types/express": "^4.17.21 || ^5.0.0",
"@types/node": "*"
},
"peerDependenciesMeta": {
"@types/express": {
"optional": true
},
"@types/node": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -25727,11 +25580,6 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true
},
"node_modules/ohash": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",
"integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="
},
"node_modules/oidc-token-hash": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
@ -25833,11 +25681,6 @@
}
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
},
"node_modules/openapi3-ts": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz",
@ -26597,14 +26440,6 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/partysocket": {
"version": "0.0.17",
"resolved": "https://registry.npmjs.org/partysocket/-/partysocket-0.0.17.tgz",
@ -26760,7 +26595,8 @@
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true
},
"node_modules/pathval": {
"version": "1.1.1",
@ -28072,11 +27908,6 @@
"node": ">=8"
}
},
"node_modules/radix3": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@ -28115,14 +27946,6 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
@ -32959,32 +32782,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/trpc-openapi": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/trpc-openapi/-/trpc-openapi-1.2.0.tgz",
"integrity": "sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==",
"workspaces": [
".",
"examples/with-nextjs",
"examples/with-express",
"examples/with-interop",
"examples/with-serverless",
"examples/with-fastify",
"examples/with-nuxtjs"
],
"dependencies": {
"co-body": "^6.1.0",
"h3": "^1.6.4",
"lodash.clonedeep": "^4.5.0",
"node-mocks-http": "^1.12.2",
"openapi-types": "^12.1.1",
"zod-to-json-schema": "^3.21.1"
},
"peerDependencies": {
"@trpc/server": "^10.0.0",
"zod": "^3.14.4"
}
},
"node_modules/ts-api-utils": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
@ -33750,18 +33547,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
@ -33880,9 +33665,10 @@
}
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz",
"integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==",
"dev": true
},
"node_modules/ulid": {
"version": "2.3.0",
@ -33917,11 +33703,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici": {
"version": "5.28.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz",
@ -33939,29 +33720,6 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/unenv": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz",
"integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==",
"dependencies": {
"consola": "^3.2.3",
"defu": "^6.1.4",
"mime": "^3.0.0",
"node-fetch-native": "^1.6.4",
"pathe": "^1.1.2"
}
},
"node_modules/unenv/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
@ -35651,9 +35409,9 @@
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@ -35696,14 +35454,6 @@
"@prisma/debug": "5.22.0"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"peerDependencies": {
"zod": "^3.24.1"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
@ -35728,7 +35478,7 @@
"superjson": "^1.13.1",
"swagger-ui-react": "^5.11.0",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
},
"packages/api/node_modules/@ts-rest/next": {
@ -35793,7 +35543,7 @@
"next-auth": "4.24.5",
"react": "^18",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
},
"packages/email": {
@ -37002,7 +36752,7 @@
"sharp": "0.32.6",
"stripe": "^12.7.0",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@playwright/browser-chromium": "1.43.0",
@ -37140,7 +36890,7 @@
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
},
"packages/trpc/node_modules/@ts-rest/next": {
@ -37220,7 +36970,7 @@
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.9.0-rc.5",
"version": "1.9.0-rc.2",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",
@ -68,14 +68,11 @@
"@lingui/core": "^4.11.3",
"inngest-cli": "^0.29.1",
"luxon": "^3.5.0",
"mupdf": "^1.0.0",
"next-runtime-env": "^3.2.0",
"react": "^18",
"zod": "3.24.1"
"react": "^18"
},
"overrides": {
"next": "14.2.6",
"zod": "3.24.1"
"next": "14.2.6"
},
"trigger.dev": {
"endpointId": "documenso-app"

View File

@ -25,6 +25,6 @@
"superjson": "^1.13.1",
"swagger-ui-react": "^5.11.0",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
}
}

View File

@ -29,7 +29,7 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
import type { TCreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
@ -345,7 +345,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
});
}
const { recipients } = await setRecipientsForDocument({
const recipients = await setRecipientsForDocument({
userId: user.id,
teamId: team?.id,
documentId: document.id,
@ -560,7 +560,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
const templateId = Number(params.templateId);
let document: TCreateDocumentFromTemplateResponse | null = null;
let document: CreateDocumentFromTemplateResponse | null = null;
try {
document = await createDocumentFromTemplate({
@ -630,6 +630,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
@ -785,7 +786,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
try {
const { recipients: newRecipients } = await setRecipientsForDocument({
const newRecipients = await setRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,

View File

@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@ -539,7 +538,7 @@ test('[TEAMS]: ensure recipient can see document regardless of visibility', asyn
await apiSignout({ page });
});
test('[TEAMS]: check that MEMBER role cannot see ADMIN-only documents', async ({ page }) => {
test('[TEAMS]: check that members cannot see ADMIN-only documents', async ({ page }) => {
const team = await seedTeam();
// Seed a member user
@ -576,46 +575,7 @@ test('[TEAMS]: check that MEMBER role cannot see ADMIN-only documents', async ({
await apiSignout({ page });
});
test('[TEAMS]: check that MEMBER role cannot see MANAGER_AND_ABOVE-only documents', async ({
page,
}) => {
const team = await seedTeam();
// Seed a member user
const memberUser = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.MEMBER,
});
// Seed an ADMIN-only document
await seedDocuments([
{
sender: team.owner,
recipients: [],
type: DocumentStatus.COMPLETED,
documentOptions: {
teamId: team.id,
visibility: 'MANAGER_AND_ABOVE',
title: 'Manager and Above Only Document',
},
},
]);
await apiSignin({
page,
email: memberUser.email,
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
});
// Check that the member user cannot see the ADMIN-only document
await expect(
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
).not.toBeVisible();
await apiSignout({ page });
});
test('[TEAMS]: check that MANAGER role cannot see ADMIN-only documents', async ({ page }) => {
test('[TEAMS]: check that managers cannot see ADMIN-only documents', async ({ page }) => {
const team = await seedTeam();
// Seed a manager user
@ -652,7 +612,7 @@ test('[TEAMS]: check that MANAGER role cannot see ADMIN-only documents', async (
await apiSignout({ page });
});
test('[TEAMS]: check that ADMIN role can see MANAGER_AND_ABOVE documents', async ({ page }) => {
test('[TEAMS]: check that admin can see MANAGER_AND_ABOVE documents', async ({ page }) => {
const team = await seedTeam();
// Seed an admin user
@ -689,187 +649,6 @@ test('[TEAMS]: check that ADMIN role can see MANAGER_AND_ABOVE documents', async
await apiSignout({ page });
});
test('[TEAMS]: check that ADMIN role can change document visibility', async ({ page }) => {
const team = await seedTeam({
createTeamOptions: {
teamGlobalSettings: {
create: {
documentVisibility: DocumentVisibility.MANAGER_AND_ABOVE,
},
},
},
});
const adminUser = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.ADMIN,
});
const document = await seedBlankDocument(adminUser, {
createDocumentOptions: {
teamId: team.id,
visibility: team.teamGlobalSettings?.documentVisibility,
},
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Admins only').click();
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
await page.getByRole('button', { name: 'Go Back' }).click();
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Admins only');
});
test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE documents', async ({
page,
}) => {
const team = await seedTeam({
createTeamOptions: {
teamGlobalSettings: {
create: {
documentVisibility: DocumentVisibility.EVERYONE,
},
},
},
});
const teamMember = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.MEMBER,
});
const document = await seedBlankDocument(teamMember, {
createDocumentOptions: {
teamId: team.id,
visibility: team.teamGlobalSettings?.documentVisibility,
},
});
await apiSignin({
page,
email: teamMember.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Everyone');
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_ABOVE documents', async ({
page,
}) => {
const team = await seedTeam({
createTeamOptions: {
teamGlobalSettings: {
create: {
documentVisibility: DocumentVisibility.MANAGER_AND_ABOVE,
},
},
},
});
const teamMember = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.MEMBER,
});
const document = await seedBlankDocument(teamMember, {
createDocumentOptions: {
teamId: team.id,
visibility: team.teamGlobalSettings?.documentVisibility,
},
});
await apiSignin({
page,
email: teamMember.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Managers and above');
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN documents', async ({
page,
}) => {
const team = await seedTeam({
createTeamOptions: {
teamGlobalSettings: {
create: {
documentVisibility: DocumentVisibility.ADMIN,
},
},
},
});
const teamMember = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.MEMBER,
});
const document = await seedBlankDocument(teamMember, {
createDocumentOptions: {
teamId: team.id,
visibility: team.teamGlobalSettings?.documentVisibility,
},
});
await apiSignin({
page,
email: teamMember.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only');
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documents', async ({
page,
}) => {
const team = await seedTeam({
createTeamOptions: {
teamGlobalSettings: {
create: {
documentVisibility: DocumentVisibility.ADMIN,
},
},
},
});
const teamManager = await seedTeamMember({
teamId: team.id,
role: TeamMemberRole.MANAGER,
});
const document = await seedBlankDocument(teamManager, {
createDocumentOptions: {
teamId: team.id,
visibility: team.teamGlobalSettings?.documentVisibility,
},
});
await apiSignin({
page,
email: teamManager.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only');
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: users cannot see documents from other teams', async ({ page }) => {
// Seed two teams with documents
const { team: teamA, teamMember2: teamAMember } = await seedTeamDocuments();

View File

@ -21,6 +21,6 @@
"next-auth": "4.24.5",
"react": "^18",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
}
}

View File

@ -1,10 +1,8 @@
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { subscriptionsContainActiveEnterprisePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getEnterprisePlanPriceIds } from '../stripe/get-enterprise-plan-prices';
export type IsUserEnterpriseOptions = {
userId: number;
teamId?: number;
@ -54,11 +52,5 @@ export const isUserEnterprise = async ({
.then((user) => user.Subscription);
}
if (subscriptions.length === 0) {
return false;
}
const enterprisePlanPriceIds = await getEnterprisePlanPriceIds();
return subscriptionsContainsActivePlan(subscriptions, enterprisePlanPriceIds, true);
return subscriptionsContainActiveEnterprisePlan(subscriptions);
};

View File

@ -1,9 +0,0 @@
import { sendConfirmationToken } from '../../../server-only/user/send-confirmation-token';
import type { TSendConfirmationEmailJobDefinition } from './send-confirmation-email';
export const run = async ({ payload }: { payload: TSendConfirmationEmailJobDefinition }) => {
await sendConfirmationToken({
email: payload.email,
force: payload.force,
});
};

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { sendConfirmationToken } from '../../../server-only/user/send-confirmation-token';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID = 'send.signup.confirmation.email';
@ -9,10 +10,6 @@ const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
force: z.boolean().optional(),
});
export type TSendConfirmationEmailJobDefinition = z.infer<
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
id: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
name: 'Send Confirmation Email',
@ -22,11 +19,12 @@ export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
schema: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload }) => {
const handler = await import('./send-confirmation-email.handler');
await handler.run({ payload });
await sendConfirmationToken({
email: payload.email,
force: payload.force,
});
},
} as const satisfies JobDefinition<
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
TSendConfirmationEmailJobDefinition
z.infer<typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,156 +0,0 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
import { prisma } from '@documenso/prisma';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import { formatDocumentsPath } from '../../../utils/teams';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendSigningRejectionEmailsJobDefinition } from './send-rejection-emails';
export const run = async ({
payload,
io,
}: {
payload: TSendSigningRejectionEmailsJobDefinition;
io: JobRunIO;
}) => {
const { documentId, recipientId } = payload;
const [document, recipient] = await Promise.all([
prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
User: true,
documentMeta: true,
team: {
select: {
teamEmail: true,
name: true,
url: true,
teamGlobalSettings: true,
},
},
},
}),
prisma.recipient.findFirstOrThrow({
where: {
id: recipientId,
signingStatus: SigningStatus.REJECTED,
},
}),
]);
const { documentMeta, team, User: documentOwner } = document;
const isEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
if (!isEmailEnabled) {
return;
}
const i18n = await getI18nInstance(documentMeta?.language);
// Send confirmation email to the recipient who rejected
await io.runTask('send-rejection-confirmation-email', async () => {
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
recipientName: recipient.name,
documentName: document.title,
documentOwnerName: document.User.name || document.User.email,
reason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(recipientTemplate, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(recipientTemplate, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: recipient.name,
address: recipient.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
html,
text,
});
});
// Send notification email to document owner
await io.runTask('send-owner-notification-email', async () => {
const ownerTemplate = createElement(DocumentRejectedEmail, {
recipientName: recipient.name,
documentName: document.title,
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
document.id
}`,
rejectionReason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(ownerTemplate, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(ownerTemplate, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: documentOwner.name || '',
address: documentOwner.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
html,
text,
});
});
await io.runTask('update-recipient', async () => {
await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
});
};

View File

@ -1,5 +1,21 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
import { prisma } from '@documenso/prisma';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import { formatDocumentsPath } from '../../../utils/teams';
import { type JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID = 'send.signing.rejected.emails';
@ -9,10 +25,6 @@ const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
recipientId: z.number(),
});
export type TSendSigningRejectionEmailsJobDefinition = z.infer<
typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA
>;
export const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION = {
id: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
name: 'Send Rejection Emails',
@ -22,11 +34,136 @@ export const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION = {
schema: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-rejection-emails.handler');
const { documentId, recipientId } = payload;
await handler.run({ payload, io });
const [document, recipient] = await Promise.all([
prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
User: true,
documentMeta: true,
team: {
select: {
teamEmail: true,
name: true,
url: true,
teamGlobalSettings: true,
},
},
},
}),
prisma.recipient.findFirstOrThrow({
where: {
id: recipientId,
signingStatus: SigningStatus.REJECTED,
},
}),
]);
const { documentMeta, team, User: documentOwner } = document;
const isEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
if (!isEmailEnabled) {
return;
}
const i18n = await getI18nInstance(documentMeta?.language);
// Send confirmation email to the recipient who rejected
await io.runTask('send-rejection-confirmation-email', async () => {
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
recipientName: recipient.name,
documentName: document.title,
documentOwnerName: document.User.name || document.User.email,
reason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(recipientTemplate, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(recipientTemplate, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: recipient.name,
address: recipient.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
html,
text,
});
});
// Send notification email to document owner
await io.runTask('send-owner-notification-email', async () => {
const ownerTemplate = createElement(DocumentRejectedEmail, {
recipientName: recipient.name,
documentName: document.title,
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
document.id
}`,
rejectionReason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(ownerTemplate, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(ownerTemplate, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: documentOwner.name || '',
address: documentOwner.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
html,
text,
});
});
await io.runTask('update-recipient', async () => {
await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
});
},
} as const satisfies JobDefinition<
typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
TSendSigningRejectionEmailsJobDefinition
z.infer<typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,215 +0,0 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
import { prisma } from '@documenso/prisma';
import {
DocumentSource,
DocumentStatus,
RecipientRole,
SendStatus,
} from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../../constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendSigningEmailJobDefinition } from './send-signing-email';
export const run = async ({
payload,
io,
}: {
payload: TSendSigningEmailJobDefinition;
io: JobRunIO;
}) => {
const { userId, documentId, recipientId, requestMetadata } = payload;
const [user, document, recipient] = await Promise.all([
prisma.user.findFirstOrThrow({
where: {
id: userId,
},
}),
prisma.document.findFirstOrThrow({
where: {
id: documentId,
status: DocumentStatus.PENDING,
},
include: {
documentMeta: true,
team: {
select: {
teamEmail: true,
name: true,
teamGlobalSettings: true,
},
},
},
}),
prisma.recipient.findFirstOrThrow({
where: {
id: recipientId,
},
}),
]);
const { documentMeta, team } = document;
if (recipient.role === RecipientRole.CC) {
return;
}
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
if (!isRecipientSigningRequestEmailEnabled) {
return;
}
const customEmail = document?.documentMeta;
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
const isTeamDocument = document.teamId !== null;
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
const { email, name } = recipient;
const selfSigner = email === user.email;
const i18n = await getI18nInstance(documentMeta?.language);
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
if (selfSigner) {
emailMessage = i18n._(
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
}
if (isDirectTemplate) {
emailMessage = i18n._(
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(
msg`Please ${recipientActionVerb} this document created by your direct template`,
);
}
if (isTeamDocument && team) {
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
emailMessage = customEmail?.message ?? '';
if (!emailMessage) {
emailMessage = i18n._(
team.teamGlobalSettings?.includeSenderDetails
? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
}
const customEmailTemplate = {
'signer.name': name,
'signer.email': email,
'document.name': document.title,
};
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
isTeamInvite: isTeamDocument,
teamName: team?.name,
teamEmail: team?.teamEmail?.email,
includeSenderDetails: team?.teamGlobalSettings?.includeSenderDetails,
});
await io.runTask('send-signing-email', async () => {
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(template, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: recipient.name,
address: recipient.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: renderCustomEmailTemplate(
documentMeta?.subject || emailSubject,
customEmailTemplate,
),
html,
text,
});
});
await io.runTask('update-recipient', async () => {
await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
});
await io.runTask('store-audit-log', async () => {
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientId: recipient.id,
recipientName: recipient.name,
recipientEmail: recipient.email,
recipientRole: recipient.role,
isResending: false,
},
}),
});
});
};

View File

@ -1,6 +1,32 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
import { prisma } from '@documenso/prisma';
import {
DocumentSource,
DocumentStatus,
RecipientRole,
SendStatus,
} from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../../constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import { type JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
@ -12,10 +38,6 @@ const SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSendSigningEmailJobDefinition = z.infer<
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
id: SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
name: 'Send Signing Email',
@ -25,11 +47,185 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
schema: SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-signing-email.handler');
const { userId, documentId, recipientId, requestMetadata } = payload;
await handler.run({ payload, io });
const [user, document, recipient] = await Promise.all([
prisma.user.findFirstOrThrow({
where: {
id: userId,
},
}),
prisma.document.findFirstOrThrow({
where: {
id: documentId,
status: DocumentStatus.PENDING,
},
include: {
documentMeta: true,
team: {
select: {
teamEmail: true,
name: true,
teamGlobalSettings: true,
},
},
},
}),
prisma.recipient.findFirstOrThrow({
where: {
id: recipientId,
},
}),
]);
const { documentMeta, team } = document;
if (recipient.role === RecipientRole.CC) {
return;
}
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
if (!isRecipientSigningRequestEmailEnabled) {
return;
}
const customEmail = document?.documentMeta;
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
const isTeamDocument = document.teamId !== null;
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
const { email, name } = recipient;
const selfSigner = email === user.email;
const i18n = await getI18nInstance(documentMeta?.language);
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
if (selfSigner) {
emailMessage = i18n._(
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
}
if (isDirectTemplate) {
emailMessage = i18n._(
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(
msg`Please ${recipientActionVerb} this document created by your direct template`,
);
}
if (isTeamDocument && team) {
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
emailMessage = customEmail?.message ?? '';
if (!emailMessage) {
emailMessage = i18n._(
team.teamGlobalSettings?.includeSenderDetails
? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
}
const customEmailTemplate = {
'signer.name': name,
'signer.email': email,
'document.name': document.title,
};
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
isTeamInvite: isTeamDocument,
teamName: team?.name,
teamEmail: team?.teamEmail?.email,
includeSenderDetails: team?.teamGlobalSettings?.includeSenderDetails,
});
await io.runTask('send-signing-email', async () => {
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(template, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: recipient.name,
address: recipient.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: renderCustomEmailTemplate(
documentMeta?.subject || emailSubject,
customEmailTemplate,
),
html,
text,
});
});
await io.runTask('update-recipient', async () => {
await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
});
await io.runTask('store-audit-log', async () => {
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientId: recipient.id,
recipientName: recipient.name,
recipientEmail: recipient.email,
recipientRole: recipient.role,
isResending: false,
},
}),
});
});
},
} as const satisfies JobDefinition<
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
TSendSigningEmailJobDefinition
z.infer<typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,23 +0,0 @@
import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendTeamDeletedEmailJobDefinition } from './send-team-deleted-email';
export const run = async ({
payload,
io,
}: {
payload: TSendTeamDeletedEmailJobDefinition;
io: JobRunIO;
}) => {
const { team, members } = payload;
for (const member of members) {
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
await sendTeamDeleteEmail({
email: member.email,
team,
isOwner: member.id === team.ownerUserId,
});
});
}
};

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { DocumentVisibility } from '@documenso/prisma/client';
import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
@ -36,10 +37,6 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
),
});
export type TSendTeamDeletedEmailJobDefinition = z.infer<
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
id: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
name: 'Send Team Deleted Email',
@ -49,11 +46,19 @@ export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
schema: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-team-deleted-email.handler');
const { team, members } = payload;
await handler.run({ payload, io });
for (const member of members) {
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
await sendTeamDeleteEmail({
email: member.email,
team,
isOwner: member.id === team.ownerUserId,
});
});
}
},
} as const satisfies JobDefinition<
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
TSendTeamDeletedEmailJobDefinition
z.infer<typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,105 +0,0 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendTeamMemberJoinedEmailJobDefinition } from './send-team-member-joined-email';
export const run = async ({
payload,
io,
}: {
payload: TSendTeamMemberJoinedEmailJobDefinition;
io: JobRunIO;
}) => {
const team = await prisma.team.findFirstOrThrow({
where: {
id: payload.teamId,
},
include: {
members: {
where: {
role: {
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
},
},
include: {
user: true,
},
},
teamGlobalSettings: true,
},
});
const invitedMember = await prisma.teamMember.findFirstOrThrow({
where: {
id: payload.memberId,
teamId: payload.teamId,
},
include: {
user: true,
},
});
for (const member of team.members) {
if (member.id === invitedMember.id) {
continue;
}
await io.runTask(
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: invitedMember.user.name || '',
memberEmail: invitedMember.user.email,
teamName: team.name,
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`A new member has joined your team`),
html,
text,
});
},
);
}
};

View File

@ -1,5 +1,18 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
@ -9,10 +22,6 @@ const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
memberId: z.number(),
});
export type TSendTeamMemberJoinedEmailJobDefinition = z.infer<
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
id: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
name: 'Send Team Member Joined Email',
@ -22,11 +31,88 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
schema: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-team-member-joined-email.handler');
const team = await prisma.team.findFirstOrThrow({
where: {
id: payload.teamId,
},
include: {
members: {
where: {
role: {
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
},
},
include: {
user: true,
},
},
teamGlobalSettings: true,
},
});
await handler.run({ payload, io });
const invitedMember = await prisma.teamMember.findFirstOrThrow({
where: {
id: payload.memberId,
teamId: payload.teamId,
},
include: {
user: true,
},
});
for (const member of team.members) {
if (member.id === invitedMember.id) {
continue;
}
await io.runTask(
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: invitedMember.user.name || '',
memberEmail: invitedMember.user.email,
teamName: team.name,
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`A new member has joined your team`),
html,
text,
});
},
);
}
},
} as const satisfies JobDefinition<
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
TSendTeamMemberJoinedEmailJobDefinition
z.infer<typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,93 +0,0 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendTeamMemberLeftEmailJobDefinition } from './send-team-member-left-email';
export const run = async ({
payload,
io,
}: {
payload: TSendTeamMemberLeftEmailJobDefinition;
io: JobRunIO;
}) => {
const team = await prisma.team.findFirstOrThrow({
where: {
id: payload.teamId,
},
include: {
members: {
where: {
role: {
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
},
},
include: {
user: true,
},
},
teamGlobalSettings: true,
},
});
const oldMember = await prisma.user.findFirstOrThrow({
where: {
id: payload.memberUserId,
},
});
for (const member of team.members) {
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: oldMember.name || '',
memberEmail: oldMember.email,
teamName: team.name,
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`A team member has left ${team.name}`),
html,
text,
});
});
}
};

View File

@ -1,5 +1,18 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
@ -9,10 +22,6 @@ const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
memberUserId: z.number(),
});
export type TSendTeamMemberLeftEmailJobDefinition = z.infer<
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
id: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
name: 'Send Team Member Left Email',
@ -22,11 +31,76 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
schema: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-team-member-left-email.handler');
const team = await prisma.team.findFirstOrThrow({
where: {
id: payload.teamId,
},
include: {
members: {
where: {
role: {
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
},
},
include: {
user: true,
},
},
teamGlobalSettings: true,
},
});
await handler.run({ payload, io });
const oldMember = await prisma.user.findFirstOrThrow({
where: {
id: payload.memberUserId,
},
});
for (const member of team.members) {
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: oldMember.name || '',
memberEmail: oldMember.email,
teamName: team.name,
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`A team member has left ${team.name}`),
html,
text,
});
});
}
},
} as const satisfies JobDefinition<
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
TSendTeamMemberLeftEmailJobDefinition
z.infer<typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA>
>;

View File

@ -1,253 +0,0 @@
import { nanoid } from 'nanoid';
import path from 'node:path';
import { PDFDocument } from 'pdf-lib';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
import { flattenForm } from '../../../server-only/pdf/flatten-form';
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import { getFile } from '../../../universal/upload/get-file';
import { putPdfFile } from '../../../universal/upload/put-file';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSealDocumentJobDefinition } from './seal-document';
export const run = async ({
payload,
io,
}: {
payload: TSealDocumentJobDefinition;
io: JobRunIO;
}) => {
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
Recipient: {
every: {
signingStatus: SigningStatus.SIGNED,
},
},
},
include: {
documentMeta: true,
Recipient: true,
team: {
select: {
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
},
},
},
},
},
});
// Seems silly but we need to do this in case the job is re-ran
// after it has already run through the update task further below.
// eslint-disable-next-line @typescript-eslint/require-await
const documentStatus = await io.runTask('get-document-status', async () => {
return document.status;
});
// This is the same case as above.
// eslint-disable-next-line @typescript-eslint/require-await
const documentDataId = await io.runTask('get-document-data-id', async () => {
return document.documentDataId;
});
const documentData = await prisma.documentData.findFirst({
where: {
id: documentDataId,
},
});
if (!documentData) {
throw new Error(`Document ${document.id} has no document data`);
}
const recipients = await prisma.recipient.findMany({
where: {
documentId: document.id,
role: {
not: RecipientRole.CC,
},
},
});
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
throw new Error(`Document ${document.id} has unsigned recipients`);
}
const fields = await prisma.field.findMany({
where: {
documentId: document.id,
},
include: {
Signature: true,
},
});
if (fields.some((field) => !field.inserted)) {
throw new Error(`Document ${document.id} has unsigned fields`);
}
if (isResealing) {
// If we're resealing we want to use the initial data for the document
// so we aren't placing fields on top of eachother.
documentData.data = documentData.initialData;
}
const pdfData = await getFile(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
const pdfDoc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
normalizeSignatureAppearances(pdfDoc);
flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
if (certificateData) {
const certificateDoc = await PDFDocument.load(certificateData);
const certificatePages = await pdfDoc.copyPages(
certificateDoc,
certificateDoc.getPageIndices(),
);
certificatePages.forEach((page) => {
pdfDoc.addPage(page);
});
}
for (const field of fields) {
await insertFieldInPDF(pdfDoc, field);
}
// Re-flatten the form to handle our checkbox and radio fields that
// create native arcoFields
flattenForm(pdfDoc);
const pdfBytes = await pdfDoc.save();
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
const { name } = path.parse(document.title);
const documentData = await putPdfFile({
name: `${name}_signed.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer),
});
return documentData.id;
});
const postHog = PostHogServerClient();
if (postHog) {
postHog.capture({
distinctId: nanoid(),
event: 'App: Document Sealed',
properties: {
documentId: document.id,
},
});
}
await io.runTask('update-document', async () => {
await prisma.$transaction(async (tx) => {
const newData = await tx.documentData.findFirstOrThrow({
where: {
id: newDataId,
},
});
await tx.document.update({
where: {
id: document.id,
},
data: {
status: DocumentStatus.COMPLETED,
completedAt: new Date(),
},
});
await tx.documentData.update({
where: {
id: documentData.id,
},
data: {
data: newData.data,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
documentId: document.id,
requestMetadata,
user: null,
data: {
transactionId: nanoid(),
},
}),
});
});
});
await io.runTask('send-completed-email', async () => {
let shouldSendCompletedEmail = sendEmail && !isResealing;
if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await sendCompletedEmail({ documentId, requestMetadata });
}
});
const updatedDocument = await prisma.document.findFirstOrThrow({
where: {
id: document.id,
},
include: {
documentData: true,
documentMeta: true,
Recipient: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(updatedDocument),
userId: updatedDocument.userId,
teamId: updatedDocument.teamId ?? undefined,
});
};

View File

@ -1,6 +1,31 @@
import { nanoid } from 'nanoid';
import path from 'node:path';
import { PDFDocument } from 'pdf-lib';
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
import { flattenForm } from '../../../server-only/pdf/flatten-form';
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { getFile } from '../../../universal/upload/get-file';
import { putPdfFile } from '../../../universal/upload/put-file';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { type JobDefinition } from '../../client/_internal/job';
const SEAL_DOCUMENT_JOB_DEFINITION_ID = 'internal.seal-document';
@ -12,8 +37,6 @@ const SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA = z.object({
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSealDocumentJobDefinition = z.infer<typeof SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA>;
export const SEAL_DOCUMENT_JOB_DEFINITION = {
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
name: 'Seal Document',
@ -23,11 +46,223 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./seal-document.handler');
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
await handler.run({ payload, io });
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
Recipient: {
every: {
signingStatus: SigningStatus.SIGNED,
},
},
},
include: {
documentMeta: true,
Recipient: true,
team: {
select: {
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
},
},
},
},
},
});
// Seems silly but we need to do this in case the job is re-ran
// after it has already run through the update task further below.
// eslint-disable-next-line @typescript-eslint/require-await
const documentStatus = await io.runTask('get-document-status', async () => {
return document.status;
});
// This is the same case as above.
// eslint-disable-next-line @typescript-eslint/require-await
const documentDataId = await io.runTask('get-document-data-id', async () => {
return document.documentDataId;
});
const documentData = await prisma.documentData.findFirst({
where: {
id: documentDataId,
},
});
if (!documentData) {
throw new Error(`Document ${document.id} has no document data`);
}
const recipients = await prisma.recipient.findMany({
where: {
documentId: document.id,
role: {
not: RecipientRole.CC,
},
},
});
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
throw new Error(`Document ${document.id} has unsigned recipients`);
}
const fields = await prisma.field.findMany({
where: {
documentId: document.id,
},
include: {
Signature: true,
},
});
if (fields.some((field) => !field.inserted)) {
throw new Error(`Document ${document.id} has unsigned fields`);
}
if (isResealing) {
// If we're resealing we want to use the initial data for the document
// so we aren't placing fields on top of eachother.
documentData.data = documentData.initialData;
}
const pdfData = await getFile(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
const pdfDoc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
normalizeSignatureAppearances(pdfDoc);
flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
if (certificateData) {
const certificateDoc = await PDFDocument.load(certificateData);
const certificatePages = await pdfDoc.copyPages(
certificateDoc,
certificateDoc.getPageIndices(),
);
certificatePages.forEach((page) => {
pdfDoc.addPage(page);
});
}
for (const field of fields) {
await insertFieldInPDF(pdfDoc, field);
}
// Re-flatten the form to handle our checkbox and radio fields that
// create native arcoFields
flattenForm(pdfDoc);
const pdfBytes = await pdfDoc.save();
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
const { name } = path.parse(document.title);
const documentData = await putPdfFile({
name: `${name}_signed.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer),
});
return documentData.id;
});
const postHog = PostHogServerClient();
if (postHog) {
postHog.capture({
distinctId: nanoid(),
event: 'App: Document Sealed',
properties: {
documentId: document.id,
},
});
}
await io.runTask('update-document', async () => {
await prisma.$transaction(async (tx) => {
const newData = await tx.documentData.findFirstOrThrow({
where: {
id: newDataId,
},
});
await tx.document.update({
where: {
id: document.id,
},
data: {
status: DocumentStatus.COMPLETED,
completedAt: new Date(),
},
});
await tx.documentData.update({
where: {
id: documentData.id,
},
data: {
data: newData.data,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
documentId: document.id,
requestMetadata,
user: null,
data: {
transactionId: nanoid(),
},
}),
});
});
});
await io.runTask('send-completed-email', async () => {
let shouldSendCompletedEmail = sendEmail && !isResealing;
if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await sendCompletedEmail({ documentId, requestMetadata });
}
});
const updatedDocument = await prisma.document.findFirstOrThrow({
where: {
id: document.id,
},
include: {
documentData: true,
documentMeta: true,
Recipient: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(updatedDocument),
userId: updatedDocument.userId,
teamId: updatedDocument.teamId ?? undefined,
});
},
} as const satisfies JobDefinition<
typeof SEAL_DOCUMENT_JOB_DEFINITION_ID,
TSealDocumentJobDefinition
z.infer<typeof SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA>
>;

View File

@ -121,10 +121,6 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
}
if (user.disabled) {
throw new Error(ErrorCode.ACCOUNT_DISABLED);
}
return {
id: Number(user.id),
email: user.email,

View File

@ -20,5 +20,4 @@ export const ErrorCode = {
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
} as const;

View File

@ -56,11 +56,11 @@
"sharp": "0.32.6",
"stripe": "^12.7.0",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
},
"devDependencies": {
"@playwright/browser-chromium": "1.43.0",
"@types/luxon": "^3.3.1",
"@types/pg": "^8.11.4"
}
}
}

View File

@ -1,5 +1,5 @@
import { prisma } from '@documenso/prisma';
import { DocumentStatus, Prisma } from '@documenso/prisma/client';
import { Prisma } from '@documenso/prisma/client';
export type SigningVolume = {
id: number;
@ -43,41 +43,34 @@ export async function getSigningVolume({
],
});
const orderByClause = getOrderByClause({ sortBy, sortOrder });
const [subscriptions, totalCount] = await Promise.all([
prisma.subscription.findMany({
where: whereClause,
include: {
User: {
select: {
name: true,
email: true,
include: {
Document: {
where: {
status: DocumentStatus.COMPLETED,
status: 'COMPLETED',
deletedAt: null,
teamId: null,
},
},
},
},
team: {
select: {
name: true,
include: {
document: {
where: {
status: DocumentStatus.COMPLETED,
status: 'COMPLETED',
deletedAt: null,
},
},
},
},
},
orderBy:
sortBy === 'name'
? [{ User: { name: sortOrder } }, { team: { name: sortOrder } }, { createdAt: 'desc' }]
: sortBy === 'createdAt'
? [{ createdAt: sortOrder }]
: undefined,
orderBy: orderByClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
}),
@ -89,8 +82,10 @@ export async function getSigningVolume({
const leaderboardWithVolume: SigningVolume[] = subscriptions.map((subscription) => {
const name =
subscription.User?.name || subscription.team?.name || subscription.User?.email || 'Unknown';
const userSignedDocs = subscription.User?.Document?.length || 0;
const teamSignedDocs = subscription.team?.document?.length || 0;
return {
id: subscription.id,
name,
@ -100,16 +95,54 @@ export async function getSigningVolume({
};
});
if (sortBy === 'signingVolume') {
leaderboardWithVolume.sort((a, b) => {
return sortOrder === 'desc'
? b.signingVolume - a.signingVolume
: a.signingVolume - b.signingVolume;
});
}
return {
leaderboard: leaderboardWithVolume,
totalPages: Math.ceil(totalCount / perPage),
};
}
function getOrderByClause(options: {
sortBy: string;
sortOrder: 'asc' | 'desc';
}): Prisma.SubscriptionOrderByWithRelationInput | Prisma.SubscriptionOrderByWithRelationInput[] {
const { sortBy, sortOrder } = options;
if (sortBy === 'name') {
return [
{
User: {
name: sortOrder,
},
},
{
team: {
name: sortOrder,
},
},
];
}
if (sortBy === 'createdAt') {
return {
createdAt: sortOrder,
};
}
// Default: sort by signing volume
return [
{
User: {
Document: {
_count: sortOrder,
},
},
},
{
team: {
document: {
_count: sortOrder,
},
},
},
];
}

View File

@ -1,9 +1,6 @@
'use server';
import type { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
@ -11,11 +8,8 @@ import { prisma } from '@documenso/prisma';
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
import { getFile } from '../../universal/upload/get-file';
import { putPdfFile } from '../../universal/upload/put-file';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = {
@ -25,24 +19,18 @@ export type CreateDocumentOptions = {
teamId?: number;
documentDataId: string;
formValues?: Record<string, string | number | boolean>;
normalizePdf?: boolean;
requestMetadata?: RequestMetadata;
};
export const ZCreateDocumentResponseSchema = DocumentSchema;
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;
export const createDocument = async ({
userId,
title,
externalId,
documentDataId,
teamId,
normalizePdf,
formValues,
requestMetadata,
}: CreateDocumentOptions): Promise<TCreateDocumentResponse> => {
}: CreateDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -94,44 +82,22 @@ export const createDocument = async ({
globalVisibility: DocumentVisibility | null | undefined,
userRole: TeamMemberRole,
): DocumentVisibility => {
if (globalVisibility) {
return globalVisibility;
}
const defaultVisibility = globalVisibility ?? DocumentVisibility.EVERYONE;
if (userRole === TeamMemberRole.ADMIN) {
return DocumentVisibility.ADMIN;
return defaultVisibility;
}
if (userRole === TeamMemberRole.MANAGER) {
return DocumentVisibility.MANAGER_AND_ABOVE;
if (defaultVisibility === DocumentVisibility.ADMIN) {
return DocumentVisibility.MANAGER_AND_ABOVE;
}
return defaultVisibility;
}
return DocumentVisibility.EVERYONE;
};
if (normalizePdf) {
const documentData = await prisma.documentData.findFirst({
where: {
id: documentDataId,
},
});
if (documentData) {
const buffer = await getFile(documentData);
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
const newDocumentData = await putPdfFile({
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(normalizedPdf),
});
// eslint-disable-next-line require-atomic-updates
documentDataId = newDocumentData.id;
}
}
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
data: {

View File

@ -1,27 +1,19 @@
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { DocumentSource, type Prisma } from '@documenso/prisma/client';
import { getDocumentWhereInput } from './get-document-by-id';
export interface DuplicateDocumentOptions {
export interface DuplicateDocumentByIdOptions {
documentId: number;
userId: number;
teamId?: number;
}
export const ZDuplicateDocumentResponseSchema = z.object({
documentId: z.number(),
});
export type TDuplicateDocumentResponse = z.infer<typeof ZDuplicateDocumentResponseSchema>;
export const duplicateDocument = async ({
export const duplicateDocumentById = async ({
documentId,
userId,
teamId,
}: DuplicateDocumentOptions): Promise<TDuplicateDocumentResponse> => {
}: DuplicateDocumentByIdOptions) => {
const documentWhereInput = await getDocumentWhereInput({
documentId,
userId,
@ -86,7 +78,5 @@ export const duplicateDocument = async ({
const createdDocument = await prisma.document.create(createDocumentArguments);
return {
documentId: createdDocument.id,
};
return createdDocument.id;
};

View File

@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type {
@ -12,16 +11,10 @@ import type {
User,
} from '@documenso/prisma/client';
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
import {
DocumentSchema,
RecipientSchema,
TeamSchema,
UserSchema,
} from '@documenso/prisma/generated/zod';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
import type { FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
@ -43,23 +36,6 @@ export type FindDocumentsOptions = {
query?: string;
};
export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
data: DocumentSchema.extend({
User: UserSchema.pick({
id: true,
name: true,
email: true,
}),
Recipient: RecipientSchema.array(),
team: TeamSchema.pick({
id: true,
url: true,
}).nullable(),
}).array(), // Todo: openapi remap.
});
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;
export const findDocuments = async ({
userId,
teamId,
@ -72,7 +48,7 @@ export const findDocuments = async ({
period,
senderIds,
query,
}: FindDocumentsOptions): Promise<TFindDocumentsResponse> => {
}: FindDocumentsOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,

View File

@ -1,15 +1,6 @@
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import {
DocumentDataSchema,
DocumentMetaSchema,
DocumentSchema,
FieldSchema,
RecipientSchema,
} from '@documenso/prisma/generated/zod';
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { getDocumentWhereInput } from './get-document-by-id';
export type GetDocumentWithDetailsByIdOptions = {
@ -18,29 +9,18 @@ export type GetDocumentWithDetailsByIdOptions = {
teamId?: number;
};
export const ZGetDocumentWithDetailsByIdResponseSchema = DocumentSchema.extend({
documentData: DocumentDataSchema,
documentMeta: DocumentMetaSchema.nullable(),
Recipient: RecipientSchema.array(),
Field: FieldSchema.array(),
});
export type TGetDocumentWithDetailsByIdResponse = z.infer<
typeof ZGetDocumentWithDetailsByIdResponseSchema
>;
export const getDocumentWithDetailsById = async ({
documentId,
userId,
teamId,
}: GetDocumentWithDetailsByIdOptions): Promise<TGetDocumentWithDetailsByIdResponse> => {
}: GetDocumentWithDetailsByIdOptions): Promise<DocumentWithDetails> => {
const documentWhereInput = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
return await prisma.document.findFirstOrThrow({
where: documentWhereInput,
include: {
documentData: true,
@ -49,12 +29,4 @@ export const getDocumentWithDetailsById = async ({
Field: true,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
return document;
};

View File

@ -1,9 +1,7 @@
import { TRPCError } from '@trpc/server';
import type { z } from 'zod';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
@ -15,16 +13,12 @@ export type MoveDocumentToTeamOptions = {
requestMetadata?: RequestMetadata;
};
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
export type TMoveDocumentToTeamResponse = z.infer<typeof ZMoveDocumentToTeamResponseSchema>;
export const moveDocumentToTeam = async ({
documentId,
teamId,
userId,
requestMetadata,
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
}: MoveDocumentToTeamOptions) => {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.findUniqueOrThrow({
where: { id: userId },

View File

@ -38,7 +38,7 @@ export const resendDocument = async ({
recipients,
teamId,
requestMetadata,
}: ResendDocumentOptions): Promise<void> => {
}: ResendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,

View File

@ -6,12 +6,8 @@ import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-po
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';

View File

@ -1,5 +1,3 @@
import type { z } from 'zod';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
@ -11,13 +9,8 @@ import {
RecipientRole,
SendStatus,
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import {
DocumentMetaSchema,
DocumentSchema,
RecipientSchema,
} from '@documenso/prisma/generated/zod';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
@ -34,20 +27,13 @@ export type SendDocumentOptions = {
requestMetadata?: RequestMetadata;
};
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
documentMeta: DocumentMetaSchema.nullable(),
Recipient: RecipientSchema.array(),
});
export type TSendDocumentResponse = z.infer<typeof ZSendDocumentResponseSchema>;
export const sendDocument = async ({
documentId,
userId,
teamId,
sendEmail,
requestMetadata,
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
}: SendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -225,7 +211,6 @@ export const sendDocument = async ({
id: documentId,
},
include: {
documentMeta: true,
Recipient: true,
},
});

View File

@ -1,7 +1,6 @@
'use server';
import { match } from 'ts-pattern';
import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -11,7 +10,6 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
import { prisma } from '@documenso/prisma';
import { DocumentVisibility } from '@documenso/prisma/client';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
@ -31,17 +29,13 @@ export type UpdateDocumentSettingsOptions = {
requestMetadata?: RequestMetadata;
};
export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema;
export type TUpdateDocumentSettingsResponse = z.infer<typeof ZUpdateDocumentSettingsResponseSchema>;
export const updateDocumentSettings = async ({
userId,
teamId,
documentId,
data,
requestMetadata,
}: UpdateDocumentSettingsOptions): Promise<TUpdateDocumentSettingsResponse> => {
}: UpdateDocumentSettingsOptions) => {
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',
@ -91,43 +85,39 @@ export const updateDocumentSettings = async ({
if (teamId) {
const currentUserRole = document.team?.members[0]?.role;
const isDocumentOwner = document.userId === userId;
const requestedVisibility = data.visibility;
if (!isDocumentOwner) {
match(currentUserRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(TeamMemberRole.MANAGER, () => {
const allowedVisibilities: DocumentVisibility[] = [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
];
match(currentUserRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(TeamMemberRole.MANAGER, () => {
const allowedVisibilities: DocumentVisibility[] = [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
];
if (
!allowedVisibilities.includes(document.visibility) ||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.with(TeamMemberRole.MEMBER, () => {
if (
document.visibility !== DocumentVisibility.EVERYONE ||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.otherwise(() => {
if (
!allowedVisibilities.includes(document.visibility) ||
(data.visibility && !allowedVisibilities.includes(data.visibility))
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document',
message: 'You do not have permission to update the document visibility',
});
}
})
.with(TeamMemberRole.MEMBER, () => {
if (
document.visibility !== DocumentVisibility.EVERYONE ||
(data.visibility && data.visibility !== DocumentVisibility.EVERYONE)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.otherwise(() => {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document',
});
}
});
}
const { documentAuthOption } = extractDocumentAuthMethods({

View File

@ -1,9 +1,4 @@
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type GetFieldByIdOptions = {
userId: number;
@ -13,17 +8,13 @@ export type GetFieldByIdOptions = {
templateId?: number;
};
export const ZGetFieldByIdResponseSchema = FieldSchema;
export type TGetFieldByIdResponse = z.infer<typeof ZGetFieldByIdResponseSchema>;
export const getFieldById = async ({
userId,
teamId,
fieldId,
documentId,
templateId,
}: GetFieldByIdOptions): Promise<TGetFieldByIdResponse> => {
}: GetFieldByIdOptions) => {
const field = await prisma.field.findFirst({
where: {
id: fieldId,
@ -54,11 +45,5 @@ export const getFieldById = async ({
},
});
if (!field) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
return field;
};

View File

@ -1,5 +1,4 @@
import { isDeepEqual } from 'remeda';
import { z } from 'zod';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
@ -24,7 +23,6 @@ import {
import { prisma } from '@documenso/prisma';
import type { Field } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
@ -36,18 +34,12 @@ export interface SetFieldsForDocumentOptions {
requestMetadata?: RequestMetadata;
}
export const ZSetFieldsForDocumentResponseSchema = z.object({
fields: z.array(FieldSchema),
});
export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocumentResponseSchema>;
export const setFieldsForDocument = async ({
userId,
documentId,
fields,
requestMetadata,
}: SetFieldsForDocumentOptions): Promise<TSetFieldsForDocumentResponse> => {
}: SetFieldsForDocumentOptions): Promise<Field[]> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -83,15 +75,11 @@ export const setFieldsForDocument = async ({
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
throw new Error('Document not found');
}
if (document.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
throw new Error('Document already complete');
}
const existingFields = await prisma.field.findMany({
@ -347,9 +335,7 @@ export const setFieldsForDocument = async ({
return !isRemoved && !isUpdated;
});
return {
fields: [...filteredFields, ...persistedFields],
};
return [...filteredFields, ...persistedFields];
};
/**

View File

@ -1,5 +1,3 @@
import { z } from 'zod';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -16,7 +14,6 @@ import {
} from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
export type SetFieldsForTemplateOptions = {
userId: number;
@ -34,17 +31,11 @@ export type SetFieldsForTemplateOptions = {
}[];
};
export const ZSetFieldsForTemplateResponseSchema = z.object({
fields: z.array(FieldSchema),
});
export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplateResponseSchema>;
export const setFieldsForTemplate = async ({
userId,
templateId,
fields,
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
}: SetFieldsForTemplateOptions) => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
@ -215,7 +206,5 @@ export const setFieldsForTemplate = async ({
return !isRemoved && !isUpdated;
});
return {
fields: [...filteredFields, ...persistedFields],
};
return [...filteredFields, ...persistedFields];
};

View File

@ -8,7 +8,6 @@ import { validateDropdownField } from '@documenso/lib/advanced-fields-validation
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
@ -120,8 +119,7 @@ export const signFieldWithToken = async ({
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkboxFieldValues: string[] = fromCheckboxValue(value);
const checkboxFieldValues = value.split(',');
const errors = validateCheckboxField(checkboxFieldValues, checkboxFieldParsedMeta, true);
if (errors.length > 0) {

View File

@ -1,11 +1,9 @@
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
import { PDFCheckBox, PDFRadioGroup, PDFRef } from 'pdf-lib';
import {
PDFCheckBox,
PDFDict,
type PDFDocument,
PDFName,
PDFRadioGroup,
PDFRef,
drawObject,
popGraphicsState,
pushGraphicsState,
@ -13,17 +11,7 @@ import {
translate,
} from 'pdf-lib';
export const removeOptionalContentGroups = (document: PDFDocument) => {
const context = document.context;
const catalog = context.lookup(context.trailerInfo.Root);
if (catalog instanceof PDFDict) {
catalog.delete(PDFName.of('OCProperties'));
}
};
export const flattenForm = (document: PDFDocument) => {
removeOptionalContentGroups(document);
const form = document.getForm();
form.updateFieldAppearances();

View File

@ -10,7 +10,6 @@ import {
MIN_HANDWRITING_FONT_SIZE,
MIN_STANDARD_FONT_SIZE,
} from '@documenso/lib/constants/pdf';
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { FieldType } from '@documenso/prisma/client';
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
@ -195,7 +194,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
}));
const selected: string[] = fromCheckboxValue(field.customText);
const selected = field.customText.split(',');
for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * 16;

View File

@ -1,18 +0,0 @@
import { PDFDocument } from 'pdf-lib';
import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
export const normalizePdf = async (pdf: Buffer) => {
const pdfDoc = await PDFDocument.load(pdf).catch(() => null);
if (!pdfDoc) {
return pdf;
}
removeOptionalContentGroups(pdfDoc);
flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
return Buffer.from(await pdfDoc.save());
};

View File

@ -1,7 +1,6 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { mailer } from '@documenso/email/mailer';
@ -22,7 +21,6 @@ import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { RecipientSchema } from '@documenso/prisma/generated/zod';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@ -41,21 +39,13 @@ export interface SetRecipientsForDocumentOptions {
requestMetadata?: RequestMetadata;
}
export const ZSetRecipientsForDocumentResponseSchema = z.object({
recipients: RecipientSchema.array(),
});
export type TSetRecipientsForDocumentResponse = z.infer<
typeof ZSetRecipientsForDocumentResponseSchema
>;
export const setRecipientsForDocument = async ({
userId,
teamId,
documentId,
recipients,
requestMetadata,
}: SetRecipientsForDocumentOptions): Promise<TSetRecipientsForDocumentResponse> => {
}: SetRecipientsForDocumentOptions): Promise<Recipient[]> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -354,9 +344,7 @@ export const setRecipientsForDocument = async ({
return !isRemoved && !isUpdated;
});
return {
recipients: [...filteredRecipients, ...persistedRecipients],
};
return [...filteredRecipients, ...persistedRecipients];
};
/**

View File

@ -1,5 +1,3 @@
import { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
@ -8,7 +6,6 @@ import {
import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { RecipientSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
@ -32,20 +29,12 @@ export type SetRecipientsForTemplateOptions = {
}[];
};
export const ZSetRecipientsForTemplateResponseSchema = z.object({
recipients: RecipientSchema.array(),
});
export type TSetRecipientsForTemplateResponse = z.infer<
typeof ZSetRecipientsForTemplateResponseSchema
>;
export const setRecipientsForTemplate = async ({
userId,
teamId,
templateId,
recipients,
}: SetRecipientsForTemplateOptions): Promise<TSetRecipientsForTemplateResponse> => {
}: SetRecipientsForTemplateOptions) => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
@ -231,7 +220,5 @@ export const setRecipientsForTemplate = async ({
return !isRemoved && !isUpdated;
});
return {
recipients: [...filteredRecipients, ...persistedRecipients],
};
return [...filteredRecipients, ...persistedRecipients];
};

View File

@ -3,7 +3,6 @@ import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
@ -68,16 +67,6 @@ type CreatedDirectRecipientField = {
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
};
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
token: z.string(),
documentId: z.number(),
recipientId: z.number(),
});
export type TCreateDocumentFromDirectTemplateResponse = z.infer<
typeof ZCreateDocumentFromDirectTemplateResponseSchema
>;
export const createDocumentFromDirectTemplate = async ({
directRecipientName: initialDirectRecipientName,
directRecipientEmail,
@ -87,7 +76,7 @@ export const createDocumentFromDirectTemplate = async ({
templateUpdatedAt,
requestMetadata,
user,
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
}: CreateDocumentFromDirectTemplateOptions) => {
const template = await prisma.template.findFirst({
where: {
directLink: {

View File

@ -1,5 +1,3 @@
import type { z } from 'zod';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
@ -13,11 +11,6 @@ import {
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import {
DocumentDataSchema,
DocumentSchema,
RecipientSchema,
} from '@documenso/prisma/generated/zod';
import type { SupportedLanguageCodes } from '../../constants/i18n';
import { AppError, AppErrorCode } from '../../errors/app-error';
@ -43,6 +36,10 @@ type FinalRecipient = Pick<
fields: Field[];
};
export type CreateDocumentFromTemplateResponse = Awaited<
ReturnType<typeof createDocumentFromTemplate>
>;
export type CreateDocumentFromTemplateOptions = {
templateId: number;
externalId?: string | null;
@ -75,15 +72,6 @@ export type CreateDocumentFromTemplateOptions = {
requestMetadata?: RequestMetadata;
};
export const ZCreateDocumentFromTemplateResponseSchema = DocumentSchema.extend({
documentData: DocumentDataSchema,
Recipient: RecipientSchema.array(),
});
export type TCreateDocumentFromTemplateResponse = z.infer<
typeof ZCreateDocumentFromTemplateResponseSchema
>;
export const createDocumentFromTemplate = async ({
templateId,
externalId,
@ -92,7 +80,7 @@ export const createDocumentFromTemplate = async ({
recipients,
override,
requestMetadata,
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
}: CreateDocumentFromTemplateOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,

View File

@ -1,15 +1,13 @@
'use server';
import { nanoid } from 'nanoid';
import type { z } from 'zod';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
DIRECT_TEMPLATE_RECIPIENT_NAME,
} from '@documenso/lib/constants/direct-templates';
import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
import type { Recipient, TemplateDirectLink } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
@ -19,17 +17,11 @@ export type CreateTemplateDirectLinkOptions = {
directRecipientId?: number;
};
export const ZCreateTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema;
export type TCreateTemplateDirectLinkResponse = z.infer<
typeof ZCreateTemplateDirectLinkResponseSchema
>;
export const createTemplateDirectLink = async ({
templateId,
userId,
directRecipientId,
}: CreateTemplateDirectLinkOptions): Promise<TCreateTemplateDirectLinkResponse> => {
}: CreateTemplateDirectLinkOptions): Promise<TemplateDirectLink> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,

View File

@ -1,7 +1,4 @@
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TemplateSchema } from '@documenso/prisma/generated/zod';
import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
@ -9,10 +6,6 @@ export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
teamId?: number;
};
export const ZCreateTemplateResponseSchema = TemplateSchema;
export type TCreateTemplateResponse = z.infer<typeof ZCreateTemplateResponseSchema>;
export const createTemplate = async ({
title,
userId,

View File

@ -1,25 +1,19 @@
import { omit } from 'remeda';
import type { z } from 'zod';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { TemplateSchema } from '@documenso/prisma/generated/zod';
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
userId: number;
};
export const ZDuplicateTemplateResponseSchema = TemplateSchema;
export type TDuplicateTemplateResponse = z.infer<typeof ZDuplicateTemplateResponseSchema>;
export const duplicateTemplate = async ({
templateId,
userId,
teamId,
}: DuplicateTemplateOptions): Promise<TDuplicateTemplateResponse> => {
}: DuplicateTemplateOptions) => {
let templateWhereFilter: Prisma.TemplateWhereUniqueInput = {
id: templateId,
userId,

View File

@ -1,18 +1,7 @@
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type { Prisma, Template } from '@documenso/prisma/client';
import {
DocumentDataSchema,
FieldSchema,
RecipientSchema,
TeamSchema,
TemplateDirectLinkSchema,
TemplateMetaSchema,
TemplateSchema,
} from '@documenso/prisma/generated/zod';
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
import type { FindResultResponse } from '../../types/search-params';
export type FindTemplatesOptions = {
userId: number;
@ -22,28 +11,8 @@ export type FindTemplatesOptions = {
perPage?: number;
};
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({
data: TemplateSchema.extend({
templateDocumentData: DocumentDataSchema,
team: TeamSchema.pick({
id: true,
url: true,
}).nullable(),
Field: FieldSchema.array(),
Recipient: RecipientSchema.array(),
templateMeta: TemplateMetaSchema.pick({
signingOrder: true,
distributionMethod: true,
}).nullable(),
directLink: TemplateDirectLinkSchema.pick({
token: true,
enabled: true,
}).nullable(),
}).array(), // Todo: openapi.
});
export type TFindTemplatesResponse = z.infer<typeof ZFindTemplatesResponseSchema>;
export type FindTemplateRow = TFindTemplatesResponse['data'][number];
export type FindTemplatesResponse = Awaited<ReturnType<typeof findTemplates>>;
export type FindTemplateRow = FindTemplatesResponse['data'][number];
export const findTemplates = async ({
userId,
@ -51,7 +20,7 @@ export const findTemplates = async ({
type,
page = 1,
perPage = 10,
}: FindTemplatesOptions): Promise<TFindTemplatesResponse> => {
}: FindTemplatesOptions) => {
let whereFilter: Prisma.TemplateWhereInput = {
userId,
teamId: null,

View File

@ -1,9 +1,6 @@
import type { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { prisma } from '@documenso/prisma';
import { TemplateSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type MoveTemplateToTeamOptions = {
templateId: number;
@ -11,15 +8,11 @@ export type MoveTemplateToTeamOptions = {
userId: number;
};
export const ZMoveTemplateToTeamResponseSchema = TemplateSchema;
export type TMoveTemplateToTeamResponse = z.infer<typeof ZMoveTemplateToTeamResponseSchema>;
export const moveTemplateToTeam = async ({
templateId,
teamId,
userId,
}: MoveTemplateToTeamOptions): Promise<TMoveTemplateToTeamResponse> => {
}: MoveTemplateToTeamOptions) => {
return await prisma.$transaction(async (tx) => {
const template = await tx.template.findFirst({
where: {
@ -30,7 +23,8 @@ export const moveTemplateToTeam = async ({
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Template not found or already associated with a team.',
});
}
@ -47,8 +41,9 @@ export const moveTemplateToTeam = async ({
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Team does not exist or you are not a member of this team.',
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You are not a member of this team.',
});
}

View File

@ -1,9 +1,6 @@
'use server';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
@ -13,17 +10,11 @@ export type ToggleTemplateDirectLinkOptions = {
enabled: boolean;
};
export const ZToggleTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema;
export type TToggleTemplateDirectLinkResponse = z.infer<
typeof ZToggleTemplateDirectLinkResponseSchema
>;
export const toggleTemplateDirectLink = async ({
templateId,
userId,
enabled,
}: ToggleTemplateDirectLinkOptions): Promise<TToggleTemplateDirectLinkResponse> => {
}: ToggleTemplateDirectLinkOptions) => {
const template = await prisma.template.findFirst({
where: {
id: templateId,

View File

@ -1,12 +1,9 @@
'use server';
import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import type { Template, TemplateMeta } from '@documenso/prisma/client';
import { TemplateSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
@ -29,17 +26,13 @@ export type UpdateTemplateSettingsOptions = {
requestMetadata?: RequestMetadata;
};
export const ZUpdateTemplateSettingsResponseSchema = TemplateSchema;
export type TUpdateTemplateSettingsResponse = z.infer<typeof ZUpdateTemplateSettingsResponseSchema>;
export const updateTemplateSettings = async ({
userId,
teamId,
templateId,
meta,
data,
}: UpdateTemplateSettingsOptions): Promise<TUpdateTemplateSettingsResponse> => {
}: UpdateTemplateSettingsOptions) => {
if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',

View File

@ -1,21 +0,0 @@
export const fromCheckboxValue = (customText: string): string[] => {
if (!customText) {
return [];
}
try {
const parsed = JSON.parse(customText);
if (!Array.isArray(parsed)) {
throw new Error('Parsed checkbox values are not an array');
}
return parsed;
} catch {
return customText.split(',').filter(Boolean);
}
};
export const toCheckboxValue = (values: string[]): string => {
return JSON.stringify(values);
};

View File

@ -24,16 +24,13 @@ export const putPdfFile = async (file: File) => {
() => false,
);
const arrayBuffer = await file.arrayBuffer();
// This will prevent uploading encrypted PDFs or anything that can't be opened.
if (!isEncryptedDocumentsAllowed) {
await PDFDocument.load(await file.arrayBuffer()).catch((e) => {
console.error(`PDF upload parse error: ${e.message}`);
const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
console.error(`PDF upload parse error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE');
});
if (!isEncryptedDocumentsAllowed && pdf.isEncrypted) {
throw new AppError('INVALID_DOCUMENT_FILE');
throw new AppError('INVALID_DOCUMENT_FILE');
});
}
if (!file.name.endsWith('.pdf')) {

View File

@ -1,3 +1,6 @@
import { env } from 'next-runtime-env';
import { IS_BILLING_ENABLED } from '../constants/app';
import type { Subscription } from '.prisma/client';
import { SubscriptionStatus } from '.prisma/client';
@ -7,18 +10,10 @@ import { SubscriptionStatus } from '.prisma/client';
export const subscriptionsContainsActivePlan = (
subscriptions: Subscription[],
priceIds: string[],
allowPastDue?: boolean,
) => {
const allowedSubscriptionStatuses: SubscriptionStatus[] = [SubscriptionStatus.ACTIVE];
if (allowPastDue) {
allowedSubscriptionStatuses.push(SubscriptionStatus.PAST_DUE);
}
return subscriptions.some(
(subscription) =>
allowedSubscriptionStatuses.includes(subscription.status) &&
priceIds.includes(subscription.priceId),
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
);
};
@ -34,3 +29,23 @@ export const subscriptionsContainsActiveProductId = (
subscription.status === SubscriptionStatus.ACTIVE && productId.includes(subscription.planId),
);
};
export const subscriptionsContainActiveEnterprisePlan = (
subscriptions?: Subscription[],
): boolean => {
const enterprisePlanId = env('NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID');
if (!enterprisePlanId || !subscriptions || !IS_BILLING_ENABLED()) {
return false;
}
const acceptableStatuses: SubscriptionStatus[] = [
SubscriptionStatus.ACTIVE,
SubscriptionStatus.PAST_DUE,
];
return subscriptions.some(
(subscription) =>
acceptableStatuses.includes(subscription.status) && enterprisePlanId === subscription.priceId,
);
};

View File

@ -78,9 +78,7 @@ class HoneybadgerLogger implements Logger {
error(error: Error, options?: LoggerDescriptionOptions): void {
const { context = {}, level = 'error', method, path } = options || {};
// const tags = [`level:${level}`];
const tags = [];
const tags = [`level:${level}`];
let errorMessage = error.message;
if (method) {

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "disabled" BOOLEAN NOT NULL DEFAULT false;

View File

@ -42,7 +42,6 @@ model User {
roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO)
avatarImageId String?
disabled Boolean @default(false)
accounts Account[]
sessions Session[]

View File

@ -1,7 +1,6 @@
import { customAlphabet } from 'nanoid';
import { prisma } from '..';
import type { Prisma } from '../client';
import { TeamMemberInviteStatus, TeamMemberRole } from '../client';
import { seedUser } from './users';
@ -11,13 +10,11 @@ const nanoid = customAlphabet('1234567890abcdef', 10);
type SeedTeamOptions = {
createTeamMembers?: number;
createTeamEmail?: true | string;
createTeamOptions?: Partial<Prisma.TeamUncheckedCreateInput>;
};
export const seedTeam = async ({
createTeamMembers = 0,
createTeamEmail,
createTeamOptions = {},
}: SeedTeamOptions = {}) => {
const teamUrl = `team-${nanoid()}`;
const teamEmail = createTeamEmail === true ? `${teamUrl}@${EMAIL_DOMAIN}` : createTeamEmail;
@ -57,7 +54,6 @@ export const seedTeam = async ({
},
}
: undefined,
...createTeamOptions,
},
});
@ -73,7 +69,6 @@ export const seedTeam = async ({
},
},
teamEmail: true,
teamGlobalSettings: true,
},
});
};

View File

@ -16,7 +16,6 @@ module.exports = {
},
colors: {
border: 'hsl(var(--border))',
'field-border': 'hsl(var(--field-border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',

View File

@ -22,6 +22,6 @@
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
}
}

View File

@ -8,40 +8,19 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { AppError } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import {
ZCreateDocumentResponseSchema,
createDocument,
} from '@documenso/lib/server-only/document/create-document';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import {
ZDuplicateDocumentResponseSchema,
duplicateDocument,
} from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import {
ZFindDocumentsResponseSchema,
findDocuments,
} from '@documenso/lib/server-only/document/find-documents';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
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 {
ZGetDocumentWithDetailsByIdResponseSchema,
getDocumentWithDetailsById,
} from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import {
ZMoveDocumentToTeamResponseSchema,
moveDocumentToTeam,
} from '@documenso/lib/server-only/document/move-document-to-team';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import {
ZSendDocumentResponseSchema,
sendDocument,
} from '@documenso/lib/server-only/document/send-document';
import {
ZUpdateDocumentSettingsResponseSchema,
updateDocumentSettings,
} from '@documenso/lib/server-only/document/update-document-settings';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@ -53,13 +32,12 @@ import {
ZDeleteDocumentMutationSchema,
ZDownloadAuditLogsMutationSchema,
ZDownloadCertificateMutationSchema,
ZDuplicateDocumentMutationSchema,
ZFindDocumentAuditLogsQuerySchema,
ZFindDocumentsQuerySchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZGetDocumentWithDetailsByIdQuerySchema,
ZMoveDocumentToTeamSchema,
ZMoveDocumentsToTeamSchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
@ -71,9 +49,7 @@ import {
} from './schema';
export const documentRouter = router({
/**
* @private
*/
// Internal endpoint for now.
getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
@ -83,9 +59,7 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
getDocumentByToken: procedure
.input(ZGetDocumentByTokenQuerySchema)
.query(async ({ input, ctx }) => {
@ -97,9 +71,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
findDocuments: authenticatedProcedure
.meta({
openapi: {
@ -111,21 +82,11 @@ export const documentRouter = router({
},
})
.input(ZFindDocumentsQuerySchema)
.output(ZFindDocumentsResponseSchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
const { user } = ctx;
const {
query,
teamId,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
} = input;
const { query, teamId, templateId, page, perPage, orderBy, source, status } = input;
const documents = await findDocuments({
userId: user.id,
@ -136,17 +97,12 @@ export const documentRouter = router({
status,
page,
perPage,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
orderBy,
});
return documents;
}),
/**
* @public
*
* Todo: Refactor to getDocumentById.
*/
getDocumentWithDetailsById: authenticatedProcedure
.meta({
openapi: {
@ -158,7 +114,7 @@ export const documentRouter = router({
},
})
.input(ZGetDocumentWithDetailsByIdQuerySchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
return await getDocumentWithDetailsById({
...input,
@ -166,9 +122,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
createDocument: authenticatedProcedure
.meta({
openapi: {
@ -179,7 +132,7 @@ export const documentRouter = router({
},
})
.input(ZCreateDocumentMutationSchema)
.output(ZCreateDocumentResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { title, documentDataId, teamId } = input;
@ -197,16 +150,11 @@ export const documentRouter = router({
teamId,
title,
documentDataId,
normalizePdf: true,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* @public
*
* Todo: Refactor to updateDocument.
*/
// Todo: Refactor to updateDocument.
setSettingsForDocument: authenticatedProcedure
.meta({
openapi: {
@ -217,7 +165,7 @@ export const documentRouter = router({
},
})
.input(ZSetSettingsForDocumentMutationSchema)
.output(ZUpdateDocumentSettingsResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, data, meta } = input;
@ -246,9 +194,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
deleteDocument: authenticatedProcedure
.meta({
openapi: {
@ -259,13 +204,13 @@ export const documentRouter = router({
},
})
.input(ZDeleteDocumentMutationSchema)
.output(z.void())
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const userId = ctx.user.id;
await deleteDocument({
return await deleteDocument({
id: documentId,
userId,
teamId,
@ -273,9 +218,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
moveDocumentToTeam: authenticatedProcedure
.meta({
openapi: {
@ -286,8 +228,8 @@ export const documentRouter = router({
tags: ['Documents'],
},
})
.input(ZMoveDocumentToTeamSchema)
.output(ZMoveDocumentToTeamResponseSchema)
.input(ZMoveDocumentsToTeamSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const userId = ctx.user.id;
@ -300,9 +242,7 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
// Should probably use `updateDocument`
setTitleForDocument: authenticatedProcedure
.input(ZSetTitleForDocumentMutationSchema)
@ -320,9 +260,7 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -347,9 +285,7 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -363,9 +299,7 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
updateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -392,12 +326,8 @@ export const documentRouter = router({
});
}),
/**
* @public
*
* Todo: Refactor to distributeDocument.
* Todo: Rework before releasing API.
*/
// Todo: Refactor to distributeDocument.
// Todo: Rework before releasing API.
sendDocument: authenticatedProcedure
.meta({
openapi: {
@ -409,7 +339,7 @@ export const documentRouter = router({
},
})
.input(ZSendDocumentMutationSchema)
.output(ZSendDocumentResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, meta } = input;
@ -444,9 +374,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
resendDocument: authenticatedProcedure
.meta({
openapi: {
@ -459,7 +386,7 @@ export const documentRouter = router({
},
})
.input(ZResendDocumentMutationSchema)
.output(z.void())
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
return await resendDocument({
userId: ctx.user.id,
@ -468,9 +395,6 @@ export const documentRouter = router({
});
}),
/**
* @public
*/
duplicateDocument: authenticatedProcedure
.meta({
openapi: {
@ -480,18 +404,16 @@ export const documentRouter = router({
tags: ['Documents'],
},
})
.input(ZDuplicateDocumentMutationSchema)
.output(ZDuplicateDocumentResponseSchema)
.input(ZGetDocumentByIdQuerySchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
return await duplicateDocument({
return await duplicateDocumentById({
userId: ctx.user.id,
...input,
});
}),
/**
* @private
*/
// Internal endpoint for now.
searchDocuments: authenticatedProcedure
.input(ZSearchDocumentsMutationSchema)
.query(async ({ input, ctx }) => {
@ -505,21 +427,11 @@ export const documentRouter = router({
return documents;
}),
/**
* @private
*/
// Internal endpoint for now.
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {
const {
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderByColumn,
orderByDirection,
} = input;
const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input;
return await findDocumentAuditLogs({
page,
@ -527,14 +439,12 @@ export const documentRouter = router({
documentId,
cursor,
filterForRecentActivity,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
orderBy,
userId: ctx.user.id,
});
}),
/**
* @private
*/
// Internal endpoint for now.
downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -563,9 +473,7 @@ export const documentRouter = router({
};
}),
/**
* @private
*/
// Internal endpoint for now.
downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -23,16 +23,24 @@ export const ZFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
templateId: z.number().min(1).optional(),
source: z.nativeEnum(DocumentSource).optional(),
status: z.nativeEnum(DocumentStatus).optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
orderBy: z
.object({
column: z.enum(['createdAt']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
});
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
documentId: z.number().min(1),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
orderBy: z
.object({
column: z.enum(['createdAt', 'type']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
});
export const ZGetDocumentByIdQuerySchema = z.object({
@ -40,11 +48,6 @@ export const ZGetDocumentByIdQuerySchema = z.object({
teamId: z.number().min(1).optional(),
});
export const ZDuplicateDocumentMutationSchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
});
export type TGetDocumentByIdQuerySchema = z.infer<typeof ZGetDocumentByIdQuerySchema>;
export const ZGetDocumentByTokenQuerySchema = z.object({
@ -220,7 +223,7 @@ export const ZDownloadCertificateMutationSchema = z.object({
teamId: z.number().optional(),
});
export const ZMoveDocumentToTeamSchema = z.object({
export const ZMoveDocumentsToTeamSchema = z.object({
documentId: z.number(),
teamId: z.number(),
});

View File

@ -1,16 +1,9 @@
import {
ZGetFieldByIdResponseSchema,
getFieldById,
} from '@documenso/lib/server-only/field/get-field-by-id';
import { z } from 'zod';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token';
import {
ZSetFieldsForDocumentResponseSchema,
setFieldsForDocument,
} from '@documenso/lib/server-only/field/set-fields-for-document';
import {
ZSetFieldsForTemplateResponseSchema,
setFieldsForTemplate,
} from '@documenso/lib/server-only/field/set-fields-for-template';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@ -24,9 +17,6 @@ import {
} from './schema';
export const fieldRouter = router({
/**
* @public
*/
getField: authenticatedProcedure
.meta({
openapi: {
@ -38,7 +28,7 @@ export const fieldRouter = router({
},
})
.input(ZGetFieldQuerySchema)
.output(ZGetFieldByIdResponseSchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
const { fieldId, teamId } = input;
@ -49,9 +39,6 @@ export const fieldRouter = router({
});
}),
/**
* @public
*/
addFields: authenticatedProcedure
.meta({
openapi: {
@ -62,7 +49,7 @@ export const fieldRouter = router({
},
})
.input(ZAddFieldsMutationSchema)
.output(ZSetFieldsForDocumentResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, fields } = input;
@ -84,9 +71,6 @@ export const fieldRouter = router({
});
}),
/**
* @public
*/
addTemplateFields: authenticatedProcedure
.meta({
openapi: {
@ -97,7 +81,7 @@ export const fieldRouter = router({
},
})
.input(ZAddTemplateFieldsMutationSchema)
.output(ZSetFieldsForTemplateResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, fields } = input;
@ -118,9 +102,7 @@ export const fieldRouter = router({
});
}),
/**
* @internal
*/
// Internal endpoint for now.
signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -137,9 +119,7 @@ export const fieldRouter = router({
});
}),
/**
* @internal
*/
// Internal endpoint for now.
removeSignedFieldWithToken: procedure
.input(ZRemovedSignedFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -1,12 +0,0 @@
import { generateOpenApiDocument } from 'trpc-openapi';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { appRouter } from './router';
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Do not use.',
version: '0.0.0',
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/beta`,
// docsUrl: '', // Todo
});

View File

@ -1,13 +1,9 @@
import { z } from 'zod';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
import {
ZSetRecipientsForDocumentResponseSchema,
setRecipientsForDocument,
} from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import {
ZSetRecipientsForTemplateResponseSchema,
setRecipientsForTemplate,
} from '@documenso/lib/server-only/recipient/set-recipients-for-template';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
@ -19,9 +15,6 @@ import {
} from './schema';
export const recipientRouter = router({
/**
* @internal
*/
addSigners: authenticatedProcedure
.meta({
openapi: {
@ -32,7 +25,7 @@ export const recipientRouter = router({
},
})
.input(ZAddSignersMutationSchema)
.output(ZSetRecipientsForDocumentResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, signers } = input;
@ -52,9 +45,6 @@ export const recipientRouter = router({
});
}),
/**
* @internal
*/
addTemplateSigners: authenticatedProcedure
.meta({
openapi: {
@ -65,7 +55,7 @@ export const recipientRouter = router({
},
})
.input(ZAddTemplateSignersMutationSchema)
.output(ZSetRecipientsForTemplateResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, signers, teamId } = input;
@ -84,9 +74,7 @@ export const recipientRouter = router({
});
}),
/**
* @internal
*/
// Internal endpoint for now.
completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -101,9 +89,7 @@ export const recipientRouter = router({
});
}),
/**
* @internal
*/
// Internal endpoint for now.
rejectDocumentWithToken: procedure
.input(ZRejectDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -79,17 +79,16 @@ export const teamRouter = router({
return await getTeams({ userId: ctx.user.id });
}),
// Todo: Public endpoint.
findTeams: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team',
// summary: 'Find teams',
// description: 'Find your teams based on a search criteria',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'GET',
path: '/team',
summary: 'Find teams',
description: 'Find your teams based on a search criteria',
tags: ['Teams'],
},
})
.input(ZFindTeamsQuerySchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
@ -99,32 +98,30 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
getTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}',
// summary: 'Get team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'GET',
path: '/team/{teamId}',
summary: 'Get team',
tags: ['Teams'],
},
})
.input(ZGetTeamQuerySchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
return await getTeamById({ teamId: input.teamId, userId: ctx.user.id });
}),
// Todo: Public endpoint.
createTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/create',
// summary: 'Create team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/create',
summary: 'Create team',
tags: ['Teams'],
},
})
.input(ZCreateTeamMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -134,16 +131,15 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
updateTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}',
// summary: 'Update team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}',
summary: 'Update team',
tags: ['Teams'],
},
})
.input(ZUpdateTeamMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -153,16 +149,15 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
deleteTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/delete',
// summary: 'Delete team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/delete',
summary: 'Delete team',
tags: ['Teams'],
},
})
.input(ZDeleteTeamMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -172,17 +167,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
leaveTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/leave',
// summary: 'Leave a team',
// description: '',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/leave',
summary: 'Leave a team',
description: '',
tags: ['Teams'],
},
})
.input(ZLeaveTeamMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -192,17 +186,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
findTeamMemberInvites: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member/invite',
// summary: 'Find member invites',
// description: 'Returns pending team member invites',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'GET',
path: '/team/{teamId}/member/invite',
summary: 'Find member invites',
description: 'Returns pending team member invites',
tags: ['Teams'],
},
})
.input(ZFindTeamMemberInvitesQuerySchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
@ -212,17 +205,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
createTeamMemberInvites: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite',
// summary: 'Invite members',
// description: 'Send email invitations to users to join the team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/member/invite',
summary: 'Invite members',
description: 'Send email invitations to users to join the team',
tags: ['Teams'],
},
})
.input(ZCreateTeamMemberInvitesMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -233,17 +225,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
resendTeamMemberInvitation: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite/{invitationId}/resend',
// summary: 'Resend member invite',
// description: 'Resend an email invitation to a user to join the team',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/member/invite/{invitationId}/resend',
summary: 'Resend member invite',
description: 'Resend an email invitation to a user to join the team',
tags: ['Teams'],
},
})
.input(ZResendTeamMemberInvitationMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -254,17 +245,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
deleteTeamMemberInvitations: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite/delete',
// summary: 'Delete member invite',
// description: 'Delete a pending team member invite',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/member/invite/delete',
summary: 'Delete member invite',
description: 'Delete a pending team member invite',
tags: ['Teams'],
},
})
.input(ZDeleteTeamMemberInvitationsMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -274,33 +264,31 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
getTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member',
// summary: 'Get members',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'GET',
path: '/team/{teamId}/member',
summary: 'Get members',
tags: ['Teams'],
},
})
.input(ZGetTeamMembersQuerySchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
return await getTeamMembers({ teamId: input.teamId, userId: ctx.user.id });
}),
// Todo: Public endpoint.
findTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member/find',
// summary: 'Find members',
// description: 'Find team members based on a search criteria',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'GET',
path: '/team/{teamId}/member/find',
summary: 'Find members',
description: 'Find team members based on a search criteria',
tags: ['Teams'],
},
})
.input(ZFindTeamMembersQuerySchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
@ -310,16 +298,15 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
updateTeamMember: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/{teamMemberId}',
// summary: 'Update member',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/member/{teamMemberId}',
summary: 'Update member',
tags: ['Teams'],
},
})
.input(ZUpdateTeamMemberMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -329,17 +316,16 @@ export const teamRouter = router({
});
}),
// Todo: Public endpoint.
deleteTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/delete',
// summary: 'Delete members',
// description: '',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/member/delete',
summary: 'Delete members',
description: '',
tags: ['Teams'],
},
})
.input(ZDeleteTeamMembersMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
@ -388,17 +374,16 @@ export const teamRouter = router({
return await getTeamInvitations({ email: ctx.user.email });
}),
// Todo: Public endpoint.
updateTeamPublicProfile: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/profile',
// summary: 'Update a team public profile',
// description: '',
// tags: ['Teams'],
// },
// })
.meta({
openapi: {
method: 'POST',
path: '/team/{teamId}/profile',
summary: 'Update a team public profile',
description: '',
tags: ['Teams'],
},
})
.input(ZUpdateTeamPublicProfileMutationSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {

View File

@ -4,50 +4,19 @@ import { z } from 'zod';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import {
ZGetDocumentWithDetailsByIdResponseSchema,
getDocumentWithDetailsById,
} from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import {
ZCreateDocumentFromDirectTemplateResponseSchema,
createDocumentFromDirectTemplate,
} from '@documenso/lib/server-only/template/create-document-from-direct-template';
import { createDocumentFromDirectTemplate } from '@documenso/lib/server-only/template/create-document-from-direct-template';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import {
ZCreateTemplateResponseSchema,
createTemplate,
} from '@documenso/lib/server-only/template/create-template';
import {
ZCreateTemplateDirectLinkResponseSchema,
createTemplateDirectLink,
} from '@documenso/lib/server-only/template/create-template-direct-link';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
import {
ZDuplicateTemplateResponseSchema,
duplicateTemplate,
} from '@documenso/lib/server-only/template/duplicate-template';
import {
ZFindTemplatesResponseSchema,
findTemplates,
} from '@documenso/lib/server-only/template/find-templates';
import {
ZGetTemplateByIdResponseSchema,
getTemplateById,
} from '@documenso/lib/server-only/template/get-template-by-id';
import {
ZMoveTemplateToTeamResponseSchema,
moveTemplateToTeam,
} from '@documenso/lib/server-only/template/move-template-to-team';
import {
ZToggleTemplateDirectLinkResponseSchema,
toggleTemplateDirectLink,
} from '@documenso/lib/server-only/template/toggle-template-direct-link';
import {
ZUpdateTemplateSettingsResponseSchema,
updateTemplateSettings,
} from '@documenso/lib/server-only/template/update-template-settings';
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { updateTemplateSettings } from '@documenso/lib/server-only/template/update-template-settings';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Document } from '@documenso/prisma/client';
@ -70,9 +39,6 @@ import {
} from './schema';
export const templateRouter = router({
/**
* @public
*/
findTemplates: authenticatedProcedure
.meta({
openapi: {
@ -84,7 +50,7 @@ export const templateRouter = router({
},
})
.input(ZFindTemplatesQuerySchema)
.output(ZFindTemplatesResponseSchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
return await findTemplates({
userId: ctx.user.id,
@ -92,9 +58,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
getTemplateById: authenticatedProcedure
.meta({
openapi: {
@ -105,7 +68,7 @@ export const templateRouter = router({
},
})
.input(ZGetTemplateByIdQuerySchema)
.output(ZGetTemplateByIdResponseSchema)
.output(z.unknown())
.query(async ({ input, ctx }) => {
const { templateId, teamId } = input;
@ -116,9 +79,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
createTemplate: authenticatedProcedure
.meta({
openapi: {
@ -130,7 +90,7 @@ export const templateRouter = router({
},
})
.input(ZCreateTemplateMutationSchema)
.output(ZCreateTemplateResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { teamId, title, templateDocumentDataId } = input;
@ -142,9 +102,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
updateTemplate: authenticatedProcedure
.meta({
openapi: {
@ -155,7 +112,7 @@ export const templateRouter = router({
},
})
.input(ZUpdateTemplateSettingsMutationSchema)
.output(ZUpdateTemplateSettingsResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, data, meta } = input;
@ -176,9 +133,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
duplicateTemplate: authenticatedProcedure
.meta({
openapi: {
@ -189,7 +143,7 @@ export const templateRouter = router({
},
})
.input(ZDuplicateTemplateMutationSchema)
.output(ZDuplicateTemplateResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { teamId, templateId } = input;
@ -200,9 +154,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
deleteTemplate: authenticatedProcedure
.meta({
openapi: {
@ -213,7 +164,7 @@ export const templateRouter = router({
},
})
.input(ZDeleteTemplateMutationSchema)
.output(z.void())
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
@ -222,9 +173,6 @@ export const templateRouter = router({
await deleteTemplate({ userId, id: templateId, teamId });
}),
/**
* @public
*/
createDocumentFromTemplate: authenticatedProcedure
.meta({
openapi: {
@ -236,9 +184,9 @@ export const templateRouter = router({
},
})
.input(ZCreateDocumentFromTemplateMutationSchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, recipients, distributeDocument } = input;
const { templateId, teamId, recipients } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
@ -248,7 +196,7 @@ export const templateRouter = router({
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
const document: Document = await createDocumentFromTemplate({
let document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
@ -256,8 +204,8 @@ export const templateRouter = router({
requestMetadata,
});
if (distributeDocument) {
await sendDocument({
if (input.distributeDocument) {
document = await sendDocument({
documentId: document.id,
userId: ctx.user.id,
teamId,
@ -269,16 +217,9 @@ export const templateRouter = router({
});
}
return getDocumentWithDetailsById({
documentId: document.id,
userId: ctx.user.id,
teamId,
});
return document;
}),
/**
* @public
*/
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
.meta({
openapi: {
@ -290,7 +231,7 @@ export const templateRouter = router({
},
})
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const {
directRecipientName,
@ -321,9 +262,7 @@ export const templateRouter = router({
});
}),
/**
* @private
*/
// Internal endpoint for now.
setSigningOrderForTemplate: authenticatedProcedure
.input(ZSetSigningOrderForTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
@ -339,9 +278,6 @@ export const templateRouter = router({
});
}),
/**
* @public
*/
createTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
@ -353,7 +289,7 @@ export const templateRouter = router({
},
})
.input(ZCreateTemplateDirectLinkMutationSchema)
.output(ZCreateTemplateDirectLinkResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, directRecipientId } = input;
@ -372,9 +308,6 @@ export const templateRouter = router({
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
}),
/**
* @public
*/
deleteTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
@ -386,7 +319,7 @@ export const templateRouter = router({
},
})
.input(ZDeleteTemplateDirectLinkMutationSchema)
.output(z.void())
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId } = input;
@ -395,9 +328,6 @@ export const templateRouter = router({
await deleteTemplateDirectLink({ userId, templateId });
}),
/**
* @public
*/
toggleTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
@ -409,7 +339,7 @@ export const templateRouter = router({
},
})
.input(ZToggleTemplateDirectLinkMutationSchema)
.output(ZToggleTemplateDirectLinkResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, enabled } = input;
@ -418,9 +348,6 @@ export const templateRouter = router({
return await toggleTemplateDirectLink({ userId, templateId, enabled });
}),
/**
* @public
*/
moveTemplateToTeam: authenticatedProcedure
.meta({
openapi: {
@ -432,7 +359,7 @@ export const templateRouter = router({
},
})
.input(ZMoveTemplatesToTeamSchema)
.output(ZMoveTemplateToTeamResponseSchema)
.output(z.unknown())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const userId = ctx.user.id;
@ -444,9 +371,7 @@ export const templateRouter = router({
});
}),
/**
* @internal
*/
// Internal endpoint for now.
updateTemplateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTemplateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -1,73 +1,43 @@
import { TRPCError, initTRPC } from '@trpc/server';
import SuperJSON from 'superjson';
import type { OpenApiMeta } from 'trpc-openapi';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { TrpcContext } from './context';
const t = initTRPC
.meta<OpenApiMeta>()
.context<TrpcContext>()
.create({
transformer: SuperJSON,
errorFormatter(opts) {
const { shape, error } = opts;
const t = initTRPC.context<TrpcContext>().create({
transformer: SuperJSON,
errorFormatter(opts) {
const { shape, error } = opts;
const originalError = error.cause;
const originalError = error.cause;
let data: Record<string, unknown> = shape.data;
let data: Record<string, unknown> = shape.data;
// Default unknown errors to 400, since if you're throwing an AppError it is expected
// that you already know what you're doing.
if (originalError instanceof AppError) {
data = {
...data,
appError: AppError.toJSON(originalError),
code: originalError.code,
httpStatus:
originalError.statusCode ??
genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ??
400,
};
}
return {
...shape,
data,
if (originalError instanceof AppError) {
data = {
...data,
appError: AppError.toJSON(originalError),
code: originalError.code,
httpStatus:
originalError.statusCode ??
genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ??
500,
};
},
});
}
return {
...shape,
data,
};
},
});
/**
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
const authorizationHeader = ctx.req.headers.authorization;
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
if (authorizationHeader) {
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
if (!token) {
throw new Error('Token was not provided for authenticated middleware');
}
const apiToken = await getApiTokenByToken({ token });
return await next({
ctx: {
...ctx,
user: apiToken.user,
session: null,
source: 'api',
},
});
}
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
@ -80,7 +50,6 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
...ctx,
user: ctx.user,
session: ctx.session,
source: 'app',
},
});
});

View File

@ -1,6 +1,5 @@
import React, { forwardRef } from 'react';
import { TeamMemberRole } from '@prisma/client';
import type { SelectProps } from '@radix-ui/react-select';
import { InfoIcon } from 'lucide-react';
@ -16,23 +15,18 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
export type DocumentVisibilitySelectType = SelectProps & {
currentTeamMemberRole?: string;
currentMemberRole?: string;
isTeamSettings?: boolean;
disabled?: boolean;
canUpdateVisibility?: boolean;
};
export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVisibilitySelectType>(
(
{ currentTeamMemberRole, isTeamSettings = false, disabled, canUpdateVisibility, ...props },
ref,
) => {
const isAdmin = currentTeamMemberRole === TeamMemberRole.ADMIN;
const isManager = currentTeamMemberRole === TeamMemberRole.MANAGER;
const canEdit = isTeamSettings || canUpdateVisibility;
({ currentMemberRole, isTeamSettings = false, disabled, ...props }, ref) => {
const canUpdateVisibility =
currentMemberRole === 'ADMIN' || currentMemberRole === 'MANAGER' || isTeamSettings;
return (
<Select {...props} disabled={!canEdit || disabled}>
<Select {...props} disabled={(!canUpdateVisibility && !isTeamSettings) || disabled}>
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
<SelectValue data-testid="documentVisibilitySelectValue" placeholder="Everyone" />
</SelectTrigger>
@ -41,13 +35,13 @@ export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVi
<SelectItem value={DocumentVisibility.EVERYONE}>
{DOCUMENT_VISIBILITY.EVERYONE.value}
</SelectItem>
<SelectItem
value={DocumentVisibility.MANAGER_AND_ABOVE}
disabled={!isAdmin && !isManager}
>
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE} disabled={!canUpdateVisibility}>
{DOCUMENT_VISIBILITY.MANAGER_AND_ABOVE.value}
</SelectItem>
<SelectItem value={DocumentVisibility.ADMIN} disabled={!isAdmin}>
<SelectItem
value={DocumentVisibility.ADMIN}
disabled={currentMemberRole !== 'ADMIN' && !isTeamSettings}
>
{DOCUMENT_VISIBILITY.ADMIN.value}
</SelectItem>
</SelectContent>

View File

@ -40,7 +40,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
return createPortal(
<div
className={cn('pointer-events-none absolute')}
className={cn('absolute')}
style={{
top: `${coords.y}px`,
left: `${coords.x}px`,

View File

@ -78,6 +78,6 @@
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5",
"zod": "3.24.1"
"zod": "^3.23.8"
}
}
}

View File

@ -129,7 +129,6 @@ export const AddFieldsFormPartial = ({
currentStep === 1 && typeof documentFlow.onBackStep === 'function' && canGoBack;
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [currentField, setCurrentField] = useState<FieldFormType>();
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
const form = useForm<TAddFieldsFormSchema>({
defaultValues: {
@ -653,9 +652,6 @@ export const AddFieldsFormPartial = ({
}}
hideRecipients={hideRecipients}
hasErrors={!!hasFieldError}
active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)}
/>
);
})}

View File

@ -6,13 +6,12 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import type { TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import {
@ -111,16 +110,6 @@ export const AddSettingsFormPartial = ({
(recipient) => recipient.sendStatus === SendStatus.SENT,
);
const canUpdateVisibility = match(currentTeamMemberRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(
TeamMemberRole.MANAGER,
() =>
document.visibility === DocumentVisibility.EVERYONE ||
document.visibility === DocumentVisibility.MANAGER_AND_ABOVE,
)
.otherwise(() => false);
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
@ -248,8 +237,7 @@ export const AddSettingsFormPartial = ({
<FormControl>
<DocumentVisibilitySelect
canUpdateVisibility={canUpdateVisibility}
currentTeamMemberRole={currentTeamMemberRole}
currentMemberRole={currentTeamMemberRole}
{...field}
onValueChange={field.onChange}
/>
@ -285,7 +273,7 @@ export const AddSettingsFormPartial = ({
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
<div className="flex flex-col space-y-6">
<div className="flex flex-col space-y-6 ">
<FormField
control={form.control}
name="externalId"

View File

@ -33,12 +33,12 @@ export const CheckboxField = ({ field }: CheckboxFieldProps) => {
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="dark:border-field-border h-3 w-3 bg-white"
className="h-3 w-3"
checkClassName="text-white"
id={`checkbox-${index}`}
checked={item.checked}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal text-black">
<Label htmlFor={`checkbox-${index}`} className="text-xs">
{item.value}
</Label>
</div>

View File

@ -34,12 +34,12 @@ export const RadioField = ({ field }: RadioFieldProps) => {
{parsedFieldMeta.values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="dark:border-field-border pointer-events-none h-3 w-3"
className="pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.checked}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal text-black">
<Label htmlFor={`option-${index}`} className="text-xs">
{item.value}
</Label>
</div>

View File

@ -47,9 +47,6 @@ export type FieldItemProps = {
recipientIndex?: number;
hideRecipients?: boolean;
hasErrors?: boolean;
active?: boolean;
onFieldActivate?: () => void;
onFieldDeactivate?: () => void;
};
export const FieldItem = ({
@ -70,10 +67,8 @@ export const FieldItem = ({
recipientIndex = 0,
hideRecipients = false,
hasErrors,
active,
onFieldActivate,
onFieldDeactivate,
}: FieldItemProps) => {
const [active, setActive] = useState(false);
const [coords, setCoords] = useState({
pageX: 0,
pageY: 0,
@ -155,8 +150,6 @@ export const FieldItem = ({
});
if (isOutsideOfField) {
setSettingsActive(false);
onFieldDeactivate?.();
onBlur?.();
}
};
@ -195,12 +188,10 @@ export const FieldItem = ({
return createPortal(
<Rnd
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
className={cn('group', {
className={cn('group z-20', {
'pointer-events-none': passive,
'pointer-events-none cursor-not-allowed opacity-75': disabled,
'z-50': active && !disabled,
'z-20': !active && !disabled,
'z-10': disabled,
'z-10': !active || disabled,
})}
minHeight={fixedSize ? '' : minHeight || 'auto'}
minWidth={fixedSize ? '' : minWidth || 'auto'}
@ -211,15 +202,15 @@ export const FieldItem = ({
width: fixedSize ? '' : coords.pageWidth,
}}
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
onDragStart={() => onFieldActivate?.()}
onResizeStart={() => onFieldActivate?.()}
onDragStart={() => setActive(true)}
onResizeStart={() => setActive(true)}
enableResizing={!fixedSize}
onResizeStop={(_e, _d, ref) => {
onFieldDeactivate?.();
setActive(false);
onResize?.(ref);
}}
onDragStop={(_e, d) => {
onFieldDeactivate?.();
setActive(false);
onMove?.(d.node);
}}
>
@ -235,10 +226,8 @@ export const FieldItem = ({
!fixedSize && '[container-type:size]',
)}
data-error={hasErrors ? 'true' : undefined}
onClick={(e) => {
e.stopPropagation();
onClick={() => {
setSettingsActive((prev) => !prev);
onFieldActivate?.();
onFocus?.();
}}
ref={$el}
@ -275,7 +264,7 @@ export const FieldItem = ({
</div>
{!disabled && settingsActive && (
<div className="z-[60] mt-1 flex justify-center">
<div className="mt-1 flex justify-center">
<div className="dark:bg-background group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
{advancedField && (
<button

View File

@ -351,17 +351,10 @@ export const SignaturePad = ({
const newValue = event.target.value;
setTypedSignature(newValue);
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
}
if (newValue.trim() !== '') {
onChange?.(newValue);
onValidityChange?.(true);
} else {
onChange?.(null);
onValidityChange?.(false);
}
};
@ -461,7 +454,7 @@ export const SignaturePad = ({
return (
<div
className={cn('relative block select-none', containerClassName, {
className={cn('relative block', containerClassName, {
'pointer-events-none opacity-50': disabled,
})}
>

View File

@ -100,7 +100,6 @@ export const AddTemplateFieldsFormPartial = ({
const { currentStep, totalSteps, previousStep } = useStep();
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [currentField, setCurrentField] = useState<FieldFormType>();
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
const form = useForm<TAddTemplateFieldsFormSchema>({
defaultValues: {
@ -476,9 +475,6 @@ export const AddTemplateFieldsFormPartial = ({
handleAdvancedSettings();
}}
hideRecipients={hideRecipients}
active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)}
/>
);
})}

View File

@ -159,8 +159,6 @@
--border: 0 0% 27.9%;
--input: 0 0% 27.9%;
--field-border: 214.3 31.8% 91.4%;
--primary: 95.08 71.08% 67.45%;
--primary-foreground: 95.08 71.08% 10%;